##// END OF EJS Templates
comments: place the left over comments (outdated/misplaced) to the left or right pane....
marcink -
r2375:edf29c73 default
parent child Browse files
Show More
@@ -1,707 +1,711 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from rhodecode.lib.helpers import (
27 from rhodecode.lib.helpers import (
28 get_lexer_for_filenode, html_escape, get_custom_lexer)
28 get_lexer_for_filenode, html_escape, get_custom_lexer)
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.lib.vcs.nodes import FileNode
30 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.diff_match_patch import diff_match_patch
31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 from rhodecode.lib.diffs import LimitedDiffContainer
32 from rhodecode.lib.diffs import LimitedDiffContainer
33 from pygments.lexers import get_lexer_by_name
33 from pygments.lexers import get_lexer_by_name
34
34
35 plain_text_lexer = get_lexer_by_name(
35 plain_text_lexer = get_lexer_by_name(
36 'text', stripall=False, stripnl=False, ensurenl=False)
36 'text', stripall=False, stripnl=False, ensurenl=False)
37
37
38
38
39 log = logging.getLogger()
39 log = logging.getLogger()
40
40
41
41
42 def filenode_as_lines_tokens(filenode, lexer=None):
42 def filenode_as_lines_tokens(filenode, lexer=None):
43 org_lexer = lexer
43 org_lexer = lexer
44 lexer = lexer or get_lexer_for_filenode(filenode)
44 lexer = lexer or get_lexer_for_filenode(filenode)
45 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
45 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
46 lexer, filenode, org_lexer)
46 lexer, filenode, org_lexer)
47 tokens = tokenize_string(filenode.content, lexer)
47 tokens = tokenize_string(filenode.content, lexer)
48 lines = split_token_stream(tokens, split_string='\n')
48 lines = split_token_stream(tokens, split_string='\n')
49 rv = list(lines)
49 rv = list(lines)
50 return rv
50 return rv
51
51
52
52
53 def tokenize_string(content, lexer):
53 def tokenize_string(content, lexer):
54 """
54 """
55 Use pygments to tokenize some content based on a lexer
55 Use pygments to tokenize some content based on a lexer
56 ensuring all original new lines and whitespace is preserved
56 ensuring all original new lines and whitespace is preserved
57 """
57 """
58
58
59 lexer.stripall = False
59 lexer.stripall = False
60 lexer.stripnl = False
60 lexer.stripnl = False
61 lexer.ensurenl = False
61 lexer.ensurenl = False
62 for token_type, token_text in lex(content, lexer):
62 for token_type, token_text in lex(content, lexer):
63 yield pygment_token_class(token_type), token_text
63 yield pygment_token_class(token_type), token_text
64
64
65
65
66 def split_token_stream(tokens, split_string=u'\n'):
66 def split_token_stream(tokens, split_string=u'\n'):
67 """
67 """
68 Take a list of (TokenType, text) tuples and split them by a string
68 Take a list of (TokenType, text) tuples and split them by a string
69
69
70 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
70 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
71 [(TEXT, 'some'), (TEXT, 'text'),
71 [(TEXT, 'some'), (TEXT, 'text'),
72 (TEXT, 'more'), (TEXT, 'text')]
72 (TEXT, 'more'), (TEXT, 'text')]
73 """
73 """
74
74
75 buffer = []
75 buffer = []
76 for token_class, token_text in tokens:
76 for token_class, token_text in tokens:
77 parts = token_text.split(split_string)
77 parts = token_text.split(split_string)
78 for part in parts[:-1]:
78 for part in parts[:-1]:
79 buffer.append((token_class, part))
79 buffer.append((token_class, part))
80 yield buffer
80 yield buffer
81 buffer = []
81 buffer = []
82
82
83 buffer.append((token_class, parts[-1]))
83 buffer.append((token_class, parts[-1]))
84
84
85 if buffer:
85 if buffer:
86 yield buffer
86 yield buffer
87
87
88
88
89 def filenode_as_annotated_lines_tokens(filenode):
89 def filenode_as_annotated_lines_tokens(filenode):
90 """
90 """
91 Take a file node and return a list of annotations => lines, if no annotation
91 Take a file node and return a list of annotations => lines, if no annotation
92 is found, it will be None.
92 is found, it will be None.
93
93
94 eg:
94 eg:
95
95
96 [
96 [
97 (annotation1, [
97 (annotation1, [
98 (1, line1_tokens_list),
98 (1, line1_tokens_list),
99 (2, line2_tokens_list),
99 (2, line2_tokens_list),
100 ]),
100 ]),
101 (annotation2, [
101 (annotation2, [
102 (3, line1_tokens_list),
102 (3, line1_tokens_list),
103 ]),
103 ]),
104 (None, [
104 (None, [
105 (4, line1_tokens_list),
105 (4, line1_tokens_list),
106 ]),
106 ]),
107 (annotation1, [
107 (annotation1, [
108 (5, line1_tokens_list),
108 (5, line1_tokens_list),
109 (6, line2_tokens_list),
109 (6, line2_tokens_list),
110 ])
110 ])
111 ]
111 ]
112 """
112 """
113
113
114 commit_cache = {} # cache commit_getter lookups
114 commit_cache = {} # cache commit_getter lookups
115
115
116 def _get_annotation(commit_id, commit_getter):
116 def _get_annotation(commit_id, commit_getter):
117 if commit_id not in commit_cache:
117 if commit_id not in commit_cache:
118 commit_cache[commit_id] = commit_getter()
118 commit_cache[commit_id] = commit_getter()
119 return commit_cache[commit_id]
119 return commit_cache[commit_id]
120
120
121 annotation_lookup = {
121 annotation_lookup = {
122 line_no: _get_annotation(commit_id, commit_getter)
122 line_no: _get_annotation(commit_id, commit_getter)
123 for line_no, commit_id, commit_getter, line_content
123 for line_no, commit_id, commit_getter, line_content
124 in filenode.annotate
124 in filenode.annotate
125 }
125 }
126
126
127 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
127 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
128 for line_no, tokens
128 for line_no, tokens
129 in enumerate(filenode_as_lines_tokens(filenode), 1))
129 in enumerate(filenode_as_lines_tokens(filenode), 1))
130
130
131 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
131 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
132
132
133 for annotation, group in grouped_annotations_lines:
133 for annotation, group in grouped_annotations_lines:
134 yield (
134 yield (
135 annotation, [(line_no, tokens)
135 annotation, [(line_no, tokens)
136 for (_, line_no, tokens) in group]
136 for (_, line_no, tokens) in group]
137 )
137 )
138
138
139
139
140 def render_tokenstream(tokenstream):
140 def render_tokenstream(tokenstream):
141 result = []
141 result = []
142 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
142 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
143
143
144 if token_class:
144 if token_class:
145 result.append(u'<span class="%s">' % token_class)
145 result.append(u'<span class="%s">' % token_class)
146 else:
146 else:
147 result.append(u'<span>')
147 result.append(u'<span>')
148
148
149 for op_tag, token_text in token_ops_texts:
149 for op_tag, token_text in token_ops_texts:
150
150
151 if op_tag:
151 if op_tag:
152 result.append(u'<%s>' % op_tag)
152 result.append(u'<%s>' % op_tag)
153
153
154 escaped_text = html_escape(token_text)
154 escaped_text = html_escape(token_text)
155
155
156 # TODO: dan: investigate showing hidden characters like space/nl/tab
156 # TODO: dan: investigate showing hidden characters like space/nl/tab
157 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
157 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
158 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
158 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
159 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
159 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
160
160
161 result.append(escaped_text)
161 result.append(escaped_text)
162
162
163 if op_tag:
163 if op_tag:
164 result.append(u'</%s>' % op_tag)
164 result.append(u'</%s>' % op_tag)
165
165
166 result.append(u'</span>')
166 result.append(u'</span>')
167
167
168 html = ''.join(result)
168 html = ''.join(result)
169 return html
169 return html
170
170
171
171
172 def rollup_tokenstream(tokenstream):
172 def rollup_tokenstream(tokenstream):
173 """
173 """
174 Group a token stream of the format:
174 Group a token stream of the format:
175
175
176 ('class', 'op', 'text')
176 ('class', 'op', 'text')
177 or
177 or
178 ('class', 'text')
178 ('class', 'text')
179
179
180 into
180 into
181
181
182 [('class1',
182 [('class1',
183 [('op1', 'text'),
183 [('op1', 'text'),
184 ('op2', 'text')]),
184 ('op2', 'text')]),
185 ('class2',
185 ('class2',
186 [('op3', 'text')])]
186 [('op3', 'text')])]
187
187
188 This is used to get the minimal tags necessary when
188 This is used to get the minimal tags necessary when
189 rendering to html eg for a token stream ie.
189 rendering to html eg for a token stream ie.
190
190
191 <span class="A"><ins>he</ins>llo</span>
191 <span class="A"><ins>he</ins>llo</span>
192 vs
192 vs
193 <span class="A"><ins>he</ins></span><span class="A">llo</span>
193 <span class="A"><ins>he</ins></span><span class="A">llo</span>
194
194
195 If a 2 tuple is passed in, the output op will be an empty string.
195 If a 2 tuple is passed in, the output op will be an empty string.
196
196
197 eg:
197 eg:
198
198
199 >>> rollup_tokenstream([('classA', '', 'h'),
199 >>> rollup_tokenstream([('classA', '', 'h'),
200 ('classA', 'del', 'ell'),
200 ('classA', 'del', 'ell'),
201 ('classA', '', 'o'),
201 ('classA', '', 'o'),
202 ('classB', '', ' '),
202 ('classB', '', ' '),
203 ('classA', '', 'the'),
203 ('classA', '', 'the'),
204 ('classA', '', 're'),
204 ('classA', '', 're'),
205 ])
205 ])
206
206
207 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
207 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
208 ('classB', [('', ' ')],
208 ('classB', [('', ' ')],
209 ('classA', [('', 'there')]]
209 ('classA', [('', 'there')]]
210
210
211 """
211 """
212 if tokenstream and len(tokenstream[0]) == 2:
212 if tokenstream and len(tokenstream[0]) == 2:
213 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
213 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
214
214
215 result = []
215 result = []
216 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
216 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
217 ops = []
217 ops = []
218 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
218 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
219 text_buffer = []
219 text_buffer = []
220 for t_class, t_op, t_text in token_text_list:
220 for t_class, t_op, t_text in token_text_list:
221 text_buffer.append(t_text)
221 text_buffer.append(t_text)
222 ops.append((token_op, ''.join(text_buffer)))
222 ops.append((token_op, ''.join(text_buffer)))
223 result.append((token_class, ops))
223 result.append((token_class, ops))
224 return result
224 return result
225
225
226
226
227 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
227 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
228 """
228 """
229 Converts a list of (token_class, token_text) tuples to a list of
229 Converts a list of (token_class, token_text) tuples to a list of
230 (token_class, token_op, token_text) tuples where token_op is one of
230 (token_class, token_op, token_text) tuples where token_op is one of
231 ('ins', 'del', '')
231 ('ins', 'del', '')
232
232
233 :param old_tokens: list of (token_class, token_text) tuples of old line
233 :param old_tokens: list of (token_class, token_text) tuples of old line
234 :param new_tokens: list of (token_class, token_text) tuples of new line
234 :param new_tokens: list of (token_class, token_text) tuples of new line
235 :param use_diff_match_patch: boolean, will use google's diff match patch
235 :param use_diff_match_patch: boolean, will use google's diff match patch
236 library which has options to 'smooth' out the character by character
236 library which has options to 'smooth' out the character by character
237 differences making nicer ins/del blocks
237 differences making nicer ins/del blocks
238 """
238 """
239
239
240 old_tokens_result = []
240 old_tokens_result = []
241 new_tokens_result = []
241 new_tokens_result = []
242
242
243 similarity = difflib.SequenceMatcher(None,
243 similarity = difflib.SequenceMatcher(None,
244 ''.join(token_text for token_class, token_text in old_tokens),
244 ''.join(token_text for token_class, token_text in old_tokens),
245 ''.join(token_text for token_class, token_text in new_tokens)
245 ''.join(token_text for token_class, token_text in new_tokens)
246 ).ratio()
246 ).ratio()
247
247
248 if similarity < 0.6: # return, the blocks are too different
248 if similarity < 0.6: # return, the blocks are too different
249 for token_class, token_text in old_tokens:
249 for token_class, token_text in old_tokens:
250 old_tokens_result.append((token_class, '', token_text))
250 old_tokens_result.append((token_class, '', token_text))
251 for token_class, token_text in new_tokens:
251 for token_class, token_text in new_tokens:
252 new_tokens_result.append((token_class, '', token_text))
252 new_tokens_result.append((token_class, '', token_text))
253 return old_tokens_result, new_tokens_result, similarity
253 return old_tokens_result, new_tokens_result, similarity
254
254
255 token_sequence_matcher = difflib.SequenceMatcher(None,
255 token_sequence_matcher = difflib.SequenceMatcher(None,
256 [x[1] for x in old_tokens],
256 [x[1] for x in old_tokens],
257 [x[1] for x in new_tokens])
257 [x[1] for x in new_tokens])
258
258
259 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
259 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
260 # check the differences by token block types first to give a more
260 # check the differences by token block types first to give a more
261 # nicer "block" level replacement vs character diffs
261 # nicer "block" level replacement vs character diffs
262
262
263 if tag == 'equal':
263 if tag == 'equal':
264 for token_class, token_text in old_tokens[o1:o2]:
264 for token_class, token_text in old_tokens[o1:o2]:
265 old_tokens_result.append((token_class, '', token_text))
265 old_tokens_result.append((token_class, '', token_text))
266 for token_class, token_text in new_tokens[n1:n2]:
266 for token_class, token_text in new_tokens[n1:n2]:
267 new_tokens_result.append((token_class, '', token_text))
267 new_tokens_result.append((token_class, '', token_text))
268 elif tag == 'delete':
268 elif tag == 'delete':
269 for token_class, token_text in old_tokens[o1:o2]:
269 for token_class, token_text in old_tokens[o1:o2]:
270 old_tokens_result.append((token_class, 'del', token_text))
270 old_tokens_result.append((token_class, 'del', token_text))
271 elif tag == 'insert':
271 elif tag == 'insert':
272 for token_class, token_text in new_tokens[n1:n2]:
272 for token_class, token_text in new_tokens[n1:n2]:
273 new_tokens_result.append((token_class, 'ins', token_text))
273 new_tokens_result.append((token_class, 'ins', token_text))
274 elif tag == 'replace':
274 elif tag == 'replace':
275 # if same type token blocks must be replaced, do a diff on the
275 # if same type token blocks must be replaced, do a diff on the
276 # characters in the token blocks to show individual changes
276 # characters in the token blocks to show individual changes
277
277
278 old_char_tokens = []
278 old_char_tokens = []
279 new_char_tokens = []
279 new_char_tokens = []
280 for token_class, token_text in old_tokens[o1:o2]:
280 for token_class, token_text in old_tokens[o1:o2]:
281 for char in token_text:
281 for char in token_text:
282 old_char_tokens.append((token_class, char))
282 old_char_tokens.append((token_class, char))
283
283
284 for token_class, token_text in new_tokens[n1:n2]:
284 for token_class, token_text in new_tokens[n1:n2]:
285 for char in token_text:
285 for char in token_text:
286 new_char_tokens.append((token_class, char))
286 new_char_tokens.append((token_class, char))
287
287
288 old_string = ''.join([token_text for
288 old_string = ''.join([token_text for
289 token_class, token_text in old_char_tokens])
289 token_class, token_text in old_char_tokens])
290 new_string = ''.join([token_text for
290 new_string = ''.join([token_text for
291 token_class, token_text in new_char_tokens])
291 token_class, token_text in new_char_tokens])
292
292
293 char_sequence = difflib.SequenceMatcher(
293 char_sequence = difflib.SequenceMatcher(
294 None, old_string, new_string)
294 None, old_string, new_string)
295 copcodes = char_sequence.get_opcodes()
295 copcodes = char_sequence.get_opcodes()
296 obuffer, nbuffer = [], []
296 obuffer, nbuffer = [], []
297
297
298 if use_diff_match_patch:
298 if use_diff_match_patch:
299 dmp = diff_match_patch()
299 dmp = diff_match_patch()
300 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
300 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
301 reps = dmp.diff_main(old_string, new_string)
301 reps = dmp.diff_main(old_string, new_string)
302 dmp.diff_cleanupEfficiency(reps)
302 dmp.diff_cleanupEfficiency(reps)
303
303
304 a, b = 0, 0
304 a, b = 0, 0
305 for op, rep in reps:
305 for op, rep in reps:
306 l = len(rep)
306 l = len(rep)
307 if op == 0:
307 if op == 0:
308 for i, c in enumerate(rep):
308 for i, c in enumerate(rep):
309 obuffer.append((old_char_tokens[a+i][0], '', c))
309 obuffer.append((old_char_tokens[a+i][0], '', c))
310 nbuffer.append((new_char_tokens[b+i][0], '', c))
310 nbuffer.append((new_char_tokens[b+i][0], '', c))
311 a += l
311 a += l
312 b += l
312 b += l
313 elif op == -1:
313 elif op == -1:
314 for i, c in enumerate(rep):
314 for i, c in enumerate(rep):
315 obuffer.append((old_char_tokens[a+i][0], 'del', c))
315 obuffer.append((old_char_tokens[a+i][0], 'del', c))
316 a += l
316 a += l
317 elif op == 1:
317 elif op == 1:
318 for i, c in enumerate(rep):
318 for i, c in enumerate(rep):
319 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
319 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
320 b += l
320 b += l
321 else:
321 else:
322 for ctag, co1, co2, cn1, cn2 in copcodes:
322 for ctag, co1, co2, cn1, cn2 in copcodes:
323 if ctag == 'equal':
323 if ctag == 'equal':
324 for token_class, token_text in old_char_tokens[co1:co2]:
324 for token_class, token_text in old_char_tokens[co1:co2]:
325 obuffer.append((token_class, '', token_text))
325 obuffer.append((token_class, '', token_text))
326 for token_class, token_text in new_char_tokens[cn1:cn2]:
326 for token_class, token_text in new_char_tokens[cn1:cn2]:
327 nbuffer.append((token_class, '', token_text))
327 nbuffer.append((token_class, '', token_text))
328 elif ctag == 'delete':
328 elif ctag == 'delete':
329 for token_class, token_text in old_char_tokens[co1:co2]:
329 for token_class, token_text in old_char_tokens[co1:co2]:
330 obuffer.append((token_class, 'del', token_text))
330 obuffer.append((token_class, 'del', token_text))
331 elif ctag == 'insert':
331 elif ctag == 'insert':
332 for token_class, token_text in new_char_tokens[cn1:cn2]:
332 for token_class, token_text in new_char_tokens[cn1:cn2]:
333 nbuffer.append((token_class, 'ins', token_text))
333 nbuffer.append((token_class, 'ins', token_text))
334 elif ctag == 'replace':
334 elif ctag == 'replace':
335 for token_class, token_text in old_char_tokens[co1:co2]:
335 for token_class, token_text in old_char_tokens[co1:co2]:
336 obuffer.append((token_class, 'del', token_text))
336 obuffer.append((token_class, 'del', token_text))
337 for token_class, token_text in new_char_tokens[cn1:cn2]:
337 for token_class, token_text in new_char_tokens[cn1:cn2]:
338 nbuffer.append((token_class, 'ins', token_text))
338 nbuffer.append((token_class, 'ins', token_text))
339
339
340 old_tokens_result.extend(obuffer)
340 old_tokens_result.extend(obuffer)
341 new_tokens_result.extend(nbuffer)
341 new_tokens_result.extend(nbuffer)
342
342
343 return old_tokens_result, new_tokens_result, similarity
343 return old_tokens_result, new_tokens_result, similarity
344
344
345
345
346 class DiffSet(object):
346 class DiffSet(object):
347 """
347 """
348 An object for parsing the diff result from diffs.DiffProcessor and
348 An object for parsing the diff result from diffs.DiffProcessor and
349 adding highlighting, side by side/unified renderings and line diffs
349 adding highlighting, side by side/unified renderings and line diffs
350 """
350 """
351
351
352 HL_REAL = 'REAL' # highlights using original file, slow
352 HL_REAL = 'REAL' # highlights using original file, slow
353 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
353 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
354 # in the case of multiline code
354 # in the case of multiline code
355 HL_NONE = 'NONE' # no highlighting, fastest
355 HL_NONE = 'NONE' # no highlighting, fastest
356
356
357 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
357 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
358 source_repo_name=None,
358 source_repo_name=None,
359 source_node_getter=lambda filename: None,
359 source_node_getter=lambda filename: None,
360 target_node_getter=lambda filename: None,
360 target_node_getter=lambda filename: None,
361 source_nodes=None, target_nodes=None,
361 source_nodes=None, target_nodes=None,
362 max_file_size_limit=150 * 1024, # files over this size will
362 max_file_size_limit=150 * 1024, # files over this size will
363 # use fast highlighting
363 # use fast highlighting
364 comments=None,
364 comments=None,
365 ):
365 ):
366
366
367 self.highlight_mode = highlight_mode
367 self.highlight_mode = highlight_mode
368 self.highlighted_filenodes = {}
368 self.highlighted_filenodes = {}
369 self.source_node_getter = source_node_getter
369 self.source_node_getter = source_node_getter
370 self.target_node_getter = target_node_getter
370 self.target_node_getter = target_node_getter
371 self.source_nodes = source_nodes or {}
371 self.source_nodes = source_nodes or {}
372 self.target_nodes = target_nodes or {}
372 self.target_nodes = target_nodes or {}
373 self.repo_name = repo_name
373 self.repo_name = repo_name
374 self.source_repo_name = source_repo_name or repo_name
374 self.source_repo_name = source_repo_name or repo_name
375 self.comments = comments or {}
375 self.comments = comments or {}
376 self.comments_store = self.comments.copy()
376 self.comments_store = self.comments.copy()
377 self.max_file_size_limit = max_file_size_limit
377 self.max_file_size_limit = max_file_size_limit
378
378
379 def render_patchset(self, patchset, source_ref=None, target_ref=None):
379 def render_patchset(self, patchset, source_ref=None, target_ref=None):
380 diffset = AttributeDict(dict(
380 diffset = AttributeDict(dict(
381 lines_added=0,
381 lines_added=0,
382 lines_deleted=0,
382 lines_deleted=0,
383 changed_files=0,
383 changed_files=0,
384 files=[],
384 files=[],
385 file_stats={},
385 file_stats={},
386 limited_diff=isinstance(patchset, LimitedDiffContainer),
386 limited_diff=isinstance(patchset, LimitedDiffContainer),
387 repo_name=self.repo_name,
387 repo_name=self.repo_name,
388 source_repo_name=self.source_repo_name,
388 source_repo_name=self.source_repo_name,
389 source_ref=source_ref,
389 source_ref=source_ref,
390 target_ref=target_ref,
390 target_ref=target_ref,
391 ))
391 ))
392 for patch in patchset:
392 for patch in patchset:
393 diffset.file_stats[patch['filename']] = patch['stats']
393 diffset.file_stats[patch['filename']] = patch['stats']
394 filediff = self.render_patch(patch)
394 filediff = self.render_patch(patch)
395 filediff.diffset = diffset
395 filediff.diffset = diffset
396 diffset.files.append(filediff)
396 diffset.files.append(filediff)
397 diffset.changed_files += 1
397 diffset.changed_files += 1
398 if not patch['stats']['binary']:
398 if not patch['stats']['binary']:
399 diffset.lines_added += patch['stats']['added']
399 diffset.lines_added += patch['stats']['added']
400 diffset.lines_deleted += patch['stats']['deleted']
400 diffset.lines_deleted += patch['stats']['deleted']
401
401
402 return diffset
402 return diffset
403
403
404 _lexer_cache = {}
404 _lexer_cache = {}
405
405 def _get_lexer_for_filename(self, filename, filenode=None):
406 def _get_lexer_for_filename(self, filename, filenode=None):
406 # cached because we might need to call it twice for source/target
407 # cached because we might need to call it twice for source/target
407 if filename not in self._lexer_cache:
408 if filename not in self._lexer_cache:
408 if filenode:
409 if filenode:
409 lexer = filenode.lexer
410 lexer = filenode.lexer
410 extension = filenode.extension
411 extension = filenode.extension
411 else:
412 else:
412 lexer = FileNode.get_lexer(filename=filename)
413 lexer = FileNode.get_lexer(filename=filename)
413 extension = filename.split('.')[-1]
414 extension = filename.split('.')[-1]
414
415
415 lexer = get_custom_lexer(extension) or lexer
416 lexer = get_custom_lexer(extension) or lexer
416 self._lexer_cache[filename] = lexer
417 self._lexer_cache[filename] = lexer
417 return self._lexer_cache[filename]
418 return self._lexer_cache[filename]
418
419
419 def render_patch(self, patch):
420 def render_patch(self, patch):
420 log.debug('rendering diff for %r' % patch['filename'])
421 log.debug('rendering diff for %r' % patch['filename'])
421
422
422 source_filename = patch['original_filename']
423 source_filename = patch['original_filename']
423 target_filename = patch['filename']
424 target_filename = patch['filename']
424
425
425 source_lexer = plain_text_lexer
426 source_lexer = plain_text_lexer
426 target_lexer = plain_text_lexer
427 target_lexer = plain_text_lexer
427
428
428 if not patch['stats']['binary']:
429 if not patch['stats']['binary']:
429 if self.highlight_mode == self.HL_REAL:
430 if self.highlight_mode == self.HL_REAL:
430 if (source_filename and patch['operation'] in ('D', 'M')
431 if (source_filename and patch['operation'] in ('D', 'M')
431 and source_filename not in self.source_nodes):
432 and source_filename not in self.source_nodes):
432 self.source_nodes[source_filename] = (
433 self.source_nodes[source_filename] = (
433 self.source_node_getter(source_filename))
434 self.source_node_getter(source_filename))
434
435
435 if (target_filename and patch['operation'] in ('A', 'M')
436 if (target_filename and patch['operation'] in ('A', 'M')
436 and target_filename not in self.target_nodes):
437 and target_filename not in self.target_nodes):
437 self.target_nodes[target_filename] = (
438 self.target_nodes[target_filename] = (
438 self.target_node_getter(target_filename))
439 self.target_node_getter(target_filename))
439
440
440 elif self.highlight_mode == self.HL_FAST:
441 elif self.highlight_mode == self.HL_FAST:
441 source_lexer = self._get_lexer_for_filename(source_filename)
442 source_lexer = self._get_lexer_for_filename(source_filename)
442 target_lexer = self._get_lexer_for_filename(target_filename)
443 target_lexer = self._get_lexer_for_filename(target_filename)
443
444
444 source_file = self.source_nodes.get(source_filename, source_filename)
445 source_file = self.source_nodes.get(source_filename, source_filename)
445 target_file = self.target_nodes.get(target_filename, target_filename)
446 target_file = self.target_nodes.get(target_filename, target_filename)
446
447
447 source_filenode, target_filenode = None, None
448 source_filenode, target_filenode = None, None
448
449
449 # TODO: dan: FileNode.lexer works on the content of the file - which
450 # TODO: dan: FileNode.lexer works on the content of the file - which
450 # can be slow - issue #4289 explains a lexer clean up - which once
451 # can be slow - issue #4289 explains a lexer clean up - which once
451 # done can allow caching a lexer for a filenode to avoid the file lookup
452 # done can allow caching a lexer for a filenode to avoid the file lookup
452 if isinstance(source_file, FileNode):
453 if isinstance(source_file, FileNode):
453 source_filenode = source_file
454 source_filenode = source_file
454 #source_lexer = source_file.lexer
455 #source_lexer = source_file.lexer
455 source_lexer = self._get_lexer_for_filename(source_filename)
456 source_lexer = self._get_lexer_for_filename(source_filename)
456 source_file.lexer = source_lexer
457 source_file.lexer = source_lexer
457
458
458 if isinstance(target_file, FileNode):
459 if isinstance(target_file, FileNode):
459 target_filenode = target_file
460 target_filenode = target_file
460 #target_lexer = target_file.lexer
461 #target_lexer = target_file.lexer
461 target_lexer = self._get_lexer_for_filename(target_filename)
462 target_lexer = self._get_lexer_for_filename(target_filename)
462 target_file.lexer = target_lexer
463 target_file.lexer = target_lexer
463
464
464 source_file_path, target_file_path = None, None
465 source_file_path, target_file_path = None, None
465
466
466 if source_filename != '/dev/null':
467 if source_filename != '/dev/null':
467 source_file_path = source_filename
468 source_file_path = source_filename
468 if target_filename != '/dev/null':
469 if target_filename != '/dev/null':
469 target_file_path = target_filename
470 target_file_path = target_filename
470
471
471 source_file_type = source_lexer.name
472 source_file_type = source_lexer.name
472 target_file_type = target_lexer.name
473 target_file_type = target_lexer.name
473
474
474 filediff = AttributeDict({
475 filediff = AttributeDict({
475 'source_file_path': source_file_path,
476 'source_file_path': source_file_path,
476 'target_file_path': target_file_path,
477 'target_file_path': target_file_path,
477 'source_filenode': source_filenode,
478 'source_filenode': source_filenode,
478 'target_filenode': target_filenode,
479 'target_filenode': target_filenode,
479 'source_file_type': target_file_type,
480 'source_file_type': target_file_type,
480 'target_file_type': source_file_type,
481 'target_file_type': source_file_type,
481 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
482 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
482 'operation': patch['operation'],
483 'operation': patch['operation'],
483 'source_mode': patch['stats']['old_mode'],
484 'source_mode': patch['stats']['old_mode'],
484 'target_mode': patch['stats']['new_mode'],
485 'target_mode': patch['stats']['new_mode'],
485 'limited_diff': isinstance(patch, LimitedDiffContainer),
486 'limited_diff': isinstance(patch, LimitedDiffContainer),
486 'hunks': [],
487 'hunks': [],
487 'diffset': self,
488 'diffset': self,
488 })
489 })
489
490
490 for hunk in patch['chunks'][1:]:
491 for hunk in patch['chunks'][1:]:
491 hunkbit = self.parse_hunk(hunk, source_file, target_file)
492 hunkbit = self.parse_hunk(hunk, source_file, target_file)
492 hunkbit.source_file_path = source_file_path
493 hunkbit.source_file_path = source_file_path
493 hunkbit.target_file_path = target_file_path
494 hunkbit.target_file_path = target_file_path
494 filediff.hunks.append(hunkbit)
495 filediff.hunks.append(hunkbit)
495
496
496 left_comments = {}
497 left_comments = {}
497 if source_file_path in self.comments_store:
498 if source_file_path in self.comments_store:
498 for lineno, comments in self.comments_store[source_file_path].items():
499 for lineno, comments in self.comments_store[source_file_path].items():
499 left_comments[lineno] = comments
500 left_comments[lineno] = comments
500
501
501 if target_file_path in self.comments_store:
502 if target_file_path in self.comments_store:
502 for lineno, comments in self.comments_store[target_file_path].items():
503 for lineno, comments in self.comments_store[target_file_path].items():
503 left_comments[lineno] = comments
504 left_comments[lineno] = comments
505 # left comments are one that we couldn't place in diff lines.
506 # could be outdated, or the diff changed and this line is no
507 # longer available
504 filediff.left_comments = left_comments
508 filediff.left_comments = left_comments
505
509
506 return filediff
510 return filediff
507
511
508 def parse_hunk(self, hunk, source_file, target_file):
512 def parse_hunk(self, hunk, source_file, target_file):
509 result = AttributeDict(dict(
513 result = AttributeDict(dict(
510 source_start=hunk['source_start'],
514 source_start=hunk['source_start'],
511 source_length=hunk['source_length'],
515 source_length=hunk['source_length'],
512 target_start=hunk['target_start'],
516 target_start=hunk['target_start'],
513 target_length=hunk['target_length'],
517 target_length=hunk['target_length'],
514 section_header=hunk['section_header'],
518 section_header=hunk['section_header'],
515 lines=[],
519 lines=[],
516 ))
520 ))
517 before, after = [], []
521 before, after = [], []
518
522
519 for line in hunk['lines']:
523 for line in hunk['lines']:
520
524
521 if line['action'] == 'unmod':
525 if line['action'] == 'unmod':
522 result.lines.extend(
526 result.lines.extend(
523 self.parse_lines(before, after, source_file, target_file))
527 self.parse_lines(before, after, source_file, target_file))
524 after.append(line)
528 after.append(line)
525 before.append(line)
529 before.append(line)
526 elif line['action'] == 'add':
530 elif line['action'] == 'add':
527 after.append(line)
531 after.append(line)
528 elif line['action'] == 'del':
532 elif line['action'] == 'del':
529 before.append(line)
533 before.append(line)
530 elif line['action'] == 'old-no-nl':
534 elif line['action'] == 'old-no-nl':
531 before.append(line)
535 before.append(line)
532 elif line['action'] == 'new-no-nl':
536 elif line['action'] == 'new-no-nl':
533 after.append(line)
537 after.append(line)
534
538
535 result.lines.extend(
539 result.lines.extend(
536 self.parse_lines(before, after, source_file, target_file))
540 self.parse_lines(before, after, source_file, target_file))
537 result.unified = self.as_unified(result.lines)
541 result.unified = self.as_unified(result.lines)
538 result.sideside = result.lines
542 result.sideside = result.lines
539
543
540 return result
544 return result
541
545
542 def parse_lines(self, before_lines, after_lines, source_file, target_file):
546 def parse_lines(self, before_lines, after_lines, source_file, target_file):
543 # TODO: dan: investigate doing the diff comparison and fast highlighting
547 # TODO: dan: investigate doing the diff comparison and fast highlighting
544 # on the entire before and after buffered block lines rather than by
548 # on the entire before and after buffered block lines rather than by
545 # line, this means we can get better 'fast' highlighting if the context
549 # line, this means we can get better 'fast' highlighting if the context
546 # allows it - eg.
550 # allows it - eg.
547 # line 4: """
551 # line 4: """
548 # line 5: this gets highlighted as a string
552 # line 5: this gets highlighted as a string
549 # line 6: """
553 # line 6: """
550
554
551 lines = []
555 lines = []
552 while before_lines or after_lines:
556 while before_lines or after_lines:
553 before, after = None, None
557 before, after = None, None
554 before_tokens, after_tokens = None, None
558 before_tokens, after_tokens = None, None
555
559
556 if before_lines:
560 if before_lines:
557 before = before_lines.pop(0)
561 before = before_lines.pop(0)
558 if after_lines:
562 if after_lines:
559 after = after_lines.pop(0)
563 after = after_lines.pop(0)
560
564
561 original = AttributeDict()
565 original = AttributeDict()
562 modified = AttributeDict()
566 modified = AttributeDict()
563
567
564 if before:
568 if before:
565 if before['action'] == 'old-no-nl':
569 if before['action'] == 'old-no-nl':
566 before_tokens = [('nonl', before['line'])]
570 before_tokens = [('nonl', before['line'])]
567 else:
571 else:
568 before_tokens = self.get_line_tokens(
572 before_tokens = self.get_line_tokens(
569 line_text=before['line'],
573 line_text=before['line'],
570 line_number=before['old_lineno'],
574 line_number=before['old_lineno'],
571 file=source_file)
575 file=source_file)
572 original.lineno = before['old_lineno']
576 original.lineno = before['old_lineno']
573 original.content = before['line']
577 original.content = before['line']
574 original.action = self.action_to_op(before['action'])
578 original.action = self.action_to_op(before['action'])
575 original.comments = self.get_comments_for('old',
579 original.comments = self.get_comments_for('old',
576 source_file, before['old_lineno'])
580 source_file, before['old_lineno'])
577
581
578 if after:
582 if after:
579 if after['action'] == 'new-no-nl':
583 if after['action'] == 'new-no-nl':
580 after_tokens = [('nonl', after['line'])]
584 after_tokens = [('nonl', after['line'])]
581 else:
585 else:
582 after_tokens = self.get_line_tokens(
586 after_tokens = self.get_line_tokens(
583 line_text=after['line'], line_number=after['new_lineno'],
587 line_text=after['line'], line_number=after['new_lineno'],
584 file=target_file)
588 file=target_file)
585 modified.lineno = after['new_lineno']
589 modified.lineno = after['new_lineno']
586 modified.content = after['line']
590 modified.content = after['line']
587 modified.action = self.action_to_op(after['action'])
591 modified.action = self.action_to_op(after['action'])
588 modified.comments = self.get_comments_for('new',
592 modified.comments = self.get_comments_for('new',
589 target_file, after['new_lineno'])
593 target_file, after['new_lineno'])
590
594
591 # diff the lines
595 # diff the lines
592 if before_tokens and after_tokens:
596 if before_tokens and after_tokens:
593 o_tokens, m_tokens, similarity = tokens_diff(
597 o_tokens, m_tokens, similarity = tokens_diff(
594 before_tokens, after_tokens)
598 before_tokens, after_tokens)
595 original.content = render_tokenstream(o_tokens)
599 original.content = render_tokenstream(o_tokens)
596 modified.content = render_tokenstream(m_tokens)
600 modified.content = render_tokenstream(m_tokens)
597 elif before_tokens:
601 elif before_tokens:
598 original.content = render_tokenstream(
602 original.content = render_tokenstream(
599 [(x[0], '', x[1]) for x in before_tokens])
603 [(x[0], '', x[1]) for x in before_tokens])
600 elif after_tokens:
604 elif after_tokens:
601 modified.content = render_tokenstream(
605 modified.content = render_tokenstream(
602 [(x[0], '', x[1]) for x in after_tokens])
606 [(x[0], '', x[1]) for x in after_tokens])
603
607
604 lines.append(AttributeDict({
608 lines.append(AttributeDict({
605 'original': original,
609 'original': original,
606 'modified': modified,
610 'modified': modified,
607 }))
611 }))
608
612
609 return lines
613 return lines
610
614
611 def get_comments_for(self, version, file, line_number):
615 def get_comments_for(self, version, filename, line_number):
612 if hasattr(file, 'unicode_path'):
616 if hasattr(filename, 'unicode_path'):
613 file = file.unicode_path
617 filename = filename.unicode_path
614
618
615 if not isinstance(file, basestring):
619 if not isinstance(filename, basestring):
616 return None
620 return None
617
621
618 line_key = {
622 line_key = {
619 'old': 'o',
623 'old': 'o',
620 'new': 'n',
624 'new': 'n',
621 }[version] + str(line_number)
625 }[version] + str(line_number)
622
626
623 if file in self.comments_store:
627 if filename in self.comments_store:
624 file_comments = self.comments_store[file]
628 file_comments = self.comments_store[filename]
625 if line_key in file_comments:
629 if line_key in file_comments:
626 return file_comments.pop(line_key)
630 return file_comments.pop(line_key)
627
631
628 def get_line_tokens(self, line_text, line_number, file=None):
632 def get_line_tokens(self, line_text, line_number, file=None):
629 filenode = None
633 filenode = None
630 filename = None
634 filename = None
631
635
632 if isinstance(file, basestring):
636 if isinstance(file, basestring):
633 filename = file
637 filename = file
634 elif isinstance(file, FileNode):
638 elif isinstance(file, FileNode):
635 filenode = file
639 filenode = file
636 filename = file.unicode_path
640 filename = file.unicode_path
637
641
638 if self.highlight_mode == self.HL_REAL and filenode:
642 if self.highlight_mode == self.HL_REAL and filenode:
639 lexer = self._get_lexer_for_filename(filename)
643 lexer = self._get_lexer_for_filename(filename)
640 file_size_allowed = file.size < self.max_file_size_limit
644 file_size_allowed = file.size < self.max_file_size_limit
641 if line_number and file_size_allowed:
645 if line_number and file_size_allowed:
642 return self.get_tokenized_filenode_line(
646 return self.get_tokenized_filenode_line(
643 file, line_number, lexer)
647 file, line_number, lexer)
644
648
645 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
649 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
646 lexer = self._get_lexer_for_filename(filename)
650 lexer = self._get_lexer_for_filename(filename)
647 return list(tokenize_string(line_text, lexer))
651 return list(tokenize_string(line_text, lexer))
648
652
649 return list(tokenize_string(line_text, plain_text_lexer))
653 return list(tokenize_string(line_text, plain_text_lexer))
650
654
651 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
655 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
652
656
653 if filenode not in self.highlighted_filenodes:
657 if filenode not in self.highlighted_filenodes:
654 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
658 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
655 self.highlighted_filenodes[filenode] = tokenized_lines
659 self.highlighted_filenodes[filenode] = tokenized_lines
656 return self.highlighted_filenodes[filenode][line_number - 1]
660 return self.highlighted_filenodes[filenode][line_number - 1]
657
661
658 def action_to_op(self, action):
662 def action_to_op(self, action):
659 return {
663 return {
660 'add': '+',
664 'add': '+',
661 'del': '-',
665 'del': '-',
662 'unmod': ' ',
666 'unmod': ' ',
663 'old-no-nl': ' ',
667 'old-no-nl': ' ',
664 'new-no-nl': ' ',
668 'new-no-nl': ' ',
665 }.get(action, action)
669 }.get(action, action)
666
670
667 def as_unified(self, lines):
671 def as_unified(self, lines):
668 """
672 """
669 Return a generator that yields the lines of a diff in unified order
673 Return a generator that yields the lines of a diff in unified order
670 """
674 """
671 def generator():
675 def generator():
672 buf = []
676 buf = []
673 for line in lines:
677 for line in lines:
674
678
675 if buf and not line.original or line.original.action == ' ':
679 if buf and not line.original or line.original.action == ' ':
676 for b in buf:
680 for b in buf:
677 yield b
681 yield b
678 buf = []
682 buf = []
679
683
680 if line.original:
684 if line.original:
681 if line.original.action == ' ':
685 if line.original.action == ' ':
682 yield (line.original.lineno, line.modified.lineno,
686 yield (line.original.lineno, line.modified.lineno,
683 line.original.action, line.original.content,
687 line.original.action, line.original.content,
684 line.original.comments)
688 line.original.comments)
685 continue
689 continue
686
690
687 if line.original.action == '-':
691 if line.original.action == '-':
688 yield (line.original.lineno, None,
692 yield (line.original.lineno, None,
689 line.original.action, line.original.content,
693 line.original.action, line.original.content,
690 line.original.comments)
694 line.original.comments)
691
695
692 if line.modified.action == '+':
696 if line.modified.action == '+':
693 buf.append((
697 buf.append((
694 None, line.modified.lineno,
698 None, line.modified.lineno,
695 line.modified.action, line.modified.content,
699 line.modified.action, line.modified.content,
696 line.modified.comments))
700 line.modified.comments))
697 continue
701 continue
698
702
699 if line.modified:
703 if line.modified:
700 yield (None, line.modified.lineno,
704 yield (None, line.modified.lineno,
701 line.modified.action, line.modified.content,
705 line.modified.action, line.modified.content,
702 line.modified.comments)
706 line.modified.comments)
703
707
704 for b in buf:
708 for b in buf:
705 yield b
709 yield b
706
710
707 return generator()
711 return generator()
@@ -1,672 +1,678 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 )">
49 )">
50
50
51 %if use_comments:
51 %if use_comments:
52 <div id="cb-comments-inline-container-template" class="js-template">
52 <div id="cb-comments-inline-container-template" class="js-template">
53 ${inline_comments_container([])}
53 ${inline_comments_container([])}
54 </div>
54 </div>
55 <div class="js-template" id="cb-comment-inline-form-template">
55 <div class="js-template" id="cb-comment-inline-form-template">
56 <div class="comment-inline-form ac">
56 <div class="comment-inline-form ac">
57
57
58 %if c.rhodecode_user.username != h.DEFAULT_USER:
58 %if c.rhodecode_user.username != h.DEFAULT_USER:
59 ## render template for inline comments
59 ## render template for inline comments
60 ${commentblock.comment_form(form_type='inline')}
60 ${commentblock.comment_form(form_type='inline')}
61 %else:
61 %else:
62 ${h.form('', class_='inline-form comment-form-login', method='get')}
62 ${h.form('', class_='inline-form comment-form-login', method='get')}
63 <div class="pull-left">
63 <div class="pull-left">
64 <div class="comment-help pull-right">
64 <div class="comment-help pull-right">
65 ${_('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>
65 ${_('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>
66 </div>
66 </div>
67 </div>
67 </div>
68 <div class="comment-button pull-right">
68 <div class="comment-button pull-right">
69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
70 ${_('Cancel')}
70 ${_('Cancel')}
71 </button>
71 </button>
72 </div>
72 </div>
73 <div class="clearfix"></div>
73 <div class="clearfix"></div>
74 ${h.end_form()}
74 ${h.end_form()}
75 %endif
75 %endif
76 </div>
76 </div>
77 </div>
77 </div>
78
78
79 %endif
79 %endif
80 <%
80 <%
81 collapse_all = len(diffset.files) > collapse_when_files_over
81 collapse_all = len(diffset.files) > collapse_when_files_over
82 %>
82 %>
83
83
84 %if c.diffmode == 'sideside':
84 %if c.diffmode == 'sideside':
85 <style>
85 <style>
86 .wrapper {
86 .wrapper {
87 max-width: 1600px !important;
87 max-width: 1600px !important;
88 }
88 }
89 </style>
89 </style>
90 %endif
90 %endif
91
91
92 %if ruler_at_chars:
92 %if ruler_at_chars:
93 <style>
93 <style>
94 .diff table.cb .cb-content:after {
94 .diff table.cb .cb-content:after {
95 content: "";
95 content: "";
96 border-left: 1px solid blue;
96 border-left: 1px solid blue;
97 position: absolute;
97 position: absolute;
98 top: 0;
98 top: 0;
99 height: 18px;
99 height: 18px;
100 opacity: .2;
100 opacity: .2;
101 z-index: 10;
101 z-index: 10;
102 //## +5 to account for diff action (+/-)
102 //## +5 to account for diff action (+/-)
103 left: ${ruler_at_chars + 5}ch;
103 left: ${ruler_at_chars + 5}ch;
104 </style>
104 </style>
105 %endif
105 %endif
106
106
107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
109 %if commit:
109 %if commit:
110 <div class="pull-right">
110 <div class="pull-right">
111 <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='')}">
111 <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='')}">
112 ${_('Browse Files')}
112 ${_('Browse Files')}
113 </a>
113 </a>
114 </div>
114 </div>
115 %endif
115 %endif
116 <h2 class="clearinner">
116 <h2 class="clearinner">
117 %if commit:
117 %if commit:
118 <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> -
118 <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> -
119 ${h.age_component(commit.date)} -
119 ${h.age_component(commit.date)} -
120 %endif
120 %endif
121
121
122 %if diffset.limited_diff:
122 %if diffset.limited_diff:
123 ${_('The requested commit is too big and content was truncated.')}
123 ${_('The requested commit is too big and content was truncated.')}
124
124
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <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>
126 <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>
127 %else:
127 %else:
128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
129 '%(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}}
129 '%(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}}
130 %endif
130 %endif
131
131
132 </h2>
132 </h2>
133 </div>
133 </div>
134
134
135 %if not diffset.files:
135 %if not diffset.files:
136 <p class="empty_data">${_('No files')}</p>
136 <p class="empty_data">${_('No files')}</p>
137 %endif
137 %endif
138
138
139 <div class="filediffs">
139 <div class="filediffs">
140 ## initial value could be marked as False later on
140 ## initial value could be marked as False later on
141 <% over_lines_changed_limit = False %>
141 <% over_lines_changed_limit = False %>
142 %for i, filediff in enumerate(diffset.files):
142 %for i, filediff in enumerate(diffset.files):
143
143
144 <%
144 <%
145 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
145 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
146 over_lines_changed_limit = lines_changed > lines_changed_limit
146 over_lines_changed_limit = lines_changed > lines_changed_limit
147 %>
147 %>
148 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
148 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
149 <div
149 <div
150 class="filediff"
150 class="filediff"
151 data-f-path="${filediff.patch['filename']}"
151 data-f-path="${filediff.patch['filename']}"
152 id="a_${h.FID('', filediff.patch['filename'])}">
152 id="a_${h.FID('', filediff.patch['filename'])}">
153 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
153 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
154 <div class="filediff-collapse-indicator"></div>
154 <div class="filediff-collapse-indicator"></div>
155 ${diff_ops(filediff)}
155 ${diff_ops(filediff)}
156 </label>
156 </label>
157 ${diff_menu(filediff, use_comments=use_comments)}
157 ${diff_menu(filediff, use_comments=use_comments)}
158 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
158 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
159 %if not filediff.hunks:
159 %if not filediff.hunks:
160 %for op_id, op_text in filediff.patch['stats']['ops'].items():
160 %for op_id, op_text in filediff.patch['stats']['ops'].items():
161 <tr>
161 <tr>
162 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
162 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
163 %if op_id == DEL_FILENODE:
163 %if op_id == DEL_FILENODE:
164 ${_('File was deleted')}
164 ${_('File was deleted')}
165 %elif op_id == BIN_FILENODE:
165 %elif op_id == BIN_FILENODE:
166 ${_('Binary file hidden')}
166 ${_('Binary file hidden')}
167 %else:
167 %else:
168 ${op_text}
168 ${op_text}
169 %endif
169 %endif
170 </td>
170 </td>
171 </tr>
171 </tr>
172 %endfor
172 %endfor
173 %endif
173 %endif
174 %if filediff.limited_diff:
174 %if filediff.limited_diff:
175 <tr class="cb-warning cb-collapser">
175 <tr class="cb-warning cb-collapser">
176 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
176 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
177 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
177 ${_('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>
178 </td>
178 </td>
179 </tr>
179 </tr>
180 %else:
180 %else:
181 %if over_lines_changed_limit:
181 %if over_lines_changed_limit:
182 <tr class="cb-warning cb-collapser">
182 <tr class="cb-warning cb-collapser">
183 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
183 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
184 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
184 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
185 <a href="#" class="cb-expand"
185 <a href="#" class="cb-expand"
186 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
186 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
187 </a>
187 </a>
188 <a href="#" class="cb-collapse"
188 <a href="#" class="cb-collapse"
189 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
189 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
190 </a>
190 </a>
191 </td>
191 </td>
192 </tr>
192 </tr>
193 %endif
193 %endif
194 %endif
194 %endif
195
195
196 %for hunk in filediff.hunks:
196 %for hunk in filediff.hunks:
197 <tr class="cb-hunk">
197 <tr class="cb-hunk">
198 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
198 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
199 ## TODO: dan: add ajax loading of more context here
199 ## TODO: dan: add ajax loading of more context here
200 ## <a href="#">
200 ## <a href="#">
201 <i class="icon-more"></i>
201 <i class="icon-more"></i>
202 ## </a>
202 ## </a>
203 </td>
203 </td>
204 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
204 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
205 @@
205 @@
206 -${hunk.source_start},${hunk.source_length}
206 -${hunk.source_start},${hunk.source_length}
207 +${hunk.target_start},${hunk.target_length}
207 +${hunk.target_start},${hunk.target_length}
208 ${hunk.section_header}
208 ${hunk.section_header}
209 </td>
209 </td>
210 </tr>
210 </tr>
211 %if c.diffmode == 'unified':
211 %if c.diffmode == 'unified':
212 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
212 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
213 %elif c.diffmode == 'sideside':
213 %elif c.diffmode == 'sideside':
214 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
214 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
215 %else:
215 %else:
216 <tr class="cb-line">
216 <tr class="cb-line">
217 <td>unknown diff mode</td>
217 <td>unknown diff mode</td>
218 </tr>
218 </tr>
219 %endif
219 %endif
220 %endfor
220 %endfor
221
221
222 ## outdated comments that do not fit into currently displayed lines
222 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in filediff.left_comments.items():
223 % for lineno, comments in filediff.left_comments.items():
224
224
225 %if c.diffmode == 'unified':
225 %if c.diffmode == 'unified':
226 <tr class="cb-line">
226 <tr class="cb-line">
227 <td class="cb-data cb-context"></td>
227 <td class="cb-data cb-context"></td>
228 <td class="cb-lineno cb-context"></td>
228 <td class="cb-lineno cb-context"></td>
229 <td class="cb-lineno cb-context"></td>
229 <td class="cb-lineno cb-context"></td>
230 <td class="cb-content cb-context">
230 <td class="cb-content cb-context">
231 ${inline_comments_container(comments)}
231 ${inline_comments_container(comments)}
232 </td>
232 </td>
233 </tr>
233 </tr>
234 %elif c.diffmode == 'sideside':
234 %elif c.diffmode == 'sideside':
235 <tr class="cb-line">
235 <tr class="cb-line">
236 <td class="cb-data cb-context"></td>
236 <td class="cb-data cb-context"></td>
237 <td class="cb-lineno cb-context"></td>
237 <td class="cb-lineno cb-context"></td>
238 <td class="cb-content cb-context"></td>
238 <td class="cb-content cb-context">
239 % if lineno.startswith('o'):
240 ${inline_comments_container(comments)}
241 % endif
242 </td>
239
243
240 <td class="cb-data cb-context"></td>
244 <td class="cb-data cb-context"></td>
241 <td class="cb-lineno cb-context"></td>
245 <td class="cb-lineno cb-context"></td>
242 <td class="cb-content cb-context">
246 <td class="cb-content cb-context">
243 ${inline_comments_container(comments)}
247 % if lineno.startswith('n'):
248 ${inline_comments_container(comments)}
249 % endif
244 </td>
250 </td>
245 </tr>
251 </tr>
246 %endif
252 %endif
247
253
248 % endfor
254 % endfor
249
255
250 </table>
256 </table>
251 </div>
257 </div>
252 %endfor
258 %endfor
253
259
254 ## outdated comments that are made for a file that has been deleted
260 ## outdated comments that are made for a file that has been deleted
255 % for filename, comments_dict in (deleted_files_comments or {}).items():
261 % for filename, comments_dict in (deleted_files_comments or {}).items():
256
262
257 <div class="filediffs filediff-outdated" style="display: none">
263 <div class="filediffs filediff-outdated" style="display: none">
258 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
264 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
259 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
265 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
260 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
266 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
261 <div class="filediff-collapse-indicator"></div>
267 <div class="filediff-collapse-indicator"></div>
262 <span class="pill">
268 <span class="pill">
263 ## file was deleted
269 ## file was deleted
264 <strong>${filename}</strong>
270 <strong>${filename}</strong>
265 </span>
271 </span>
266 <span class="pill-group" style="float: left">
272 <span class="pill-group" style="float: left">
267 ## file op, doesn't need translation
273 ## file op, doesn't need translation
268 <span class="pill" op="removed">removed in this version</span>
274 <span class="pill" op="removed">removed in this version</span>
269 </span>
275 </span>
270 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
276 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
271 <span class="pill-group" style="float: right">
277 <span class="pill-group" style="float: right">
272 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
278 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
273 </span>
279 </span>
274 </label>
280 </label>
275
281
276 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
282 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
277 <tr>
283 <tr>
278 % if c.diffmode == 'unified':
284 % if c.diffmode == 'unified':
279 <td></td>
285 <td></td>
280 %endif
286 %endif
281
287
282 <td></td>
288 <td></td>
283 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
289 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
284 ${_('File was deleted in this version, and outdated comments were made on it')}
290 ${_('File was deleted in this version, and outdated comments were made on it')}
285 </td>
291 </td>
286 </tr>
292 </tr>
287 %if c.diffmode == 'unified':
293 %if c.diffmode == 'unified':
288 <tr class="cb-line">
294 <tr class="cb-line">
289 <td class="cb-data cb-context"></td>
295 <td class="cb-data cb-context"></td>
290 <td class="cb-lineno cb-context"></td>
296 <td class="cb-lineno cb-context"></td>
291 <td class="cb-lineno cb-context"></td>
297 <td class="cb-lineno cb-context"></td>
292 <td class="cb-content cb-context">
298 <td class="cb-content cb-context">
293 ${inline_comments_container(comments_dict['comments'])}
299 ${inline_comments_container(comments_dict['comments'])}
294 </td>
300 </td>
295 </tr>
301 </tr>
296 %elif c.diffmode == 'sideside':
302 %elif c.diffmode == 'sideside':
297 <tr class="cb-line">
303 <tr class="cb-line">
298 <td class="cb-data cb-context"></td>
304 <td class="cb-data cb-context"></td>
299 <td class="cb-lineno cb-context"></td>
305 <td class="cb-lineno cb-context"></td>
300 <td class="cb-content cb-context"></td>
306 <td class="cb-content cb-context"></td>
301
307
302 <td class="cb-data cb-context"></td>
308 <td class="cb-data cb-context"></td>
303 <td class="cb-lineno cb-context"></td>
309 <td class="cb-lineno cb-context"></td>
304 <td class="cb-content cb-context">
310 <td class="cb-content cb-context">
305 ${inline_comments_container(comments_dict['comments'])}
311 ${inline_comments_container(comments_dict['comments'])}
306 </td>
312 </td>
307 </tr>
313 </tr>
308 %endif
314 %endif
309 </table>
315 </table>
310 </div>
316 </div>
311 </div>
317 </div>
312 % endfor
318 % endfor
313
319
314 </div>
320 </div>
315 </div>
321 </div>
316 </%def>
322 </%def>
317
323
318 <%def name="diff_ops(filediff)">
324 <%def name="diff_ops(filediff)">
319 <%
325 <%
320 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
326 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
321 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
327 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
322 %>
328 %>
323 <span class="pill">
329 <span class="pill">
324 %if filediff.source_file_path and filediff.target_file_path:
330 %if filediff.source_file_path and filediff.target_file_path:
325 %if filediff.source_file_path != filediff.target_file_path:
331 %if filediff.source_file_path != filediff.target_file_path:
326 ## file was renamed, or copied
332 ## file was renamed, or copied
327 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
333 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
328 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
334 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
329 <% final_path = filediff.target_file_path %>
335 <% final_path = filediff.target_file_path %>
330 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
336 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
331 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
337 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
332 <% final_path = filediff.target_file_path %>
338 <% final_path = filediff.target_file_path %>
333 %endif
339 %endif
334 %else:
340 %else:
335 ## file was modified
341 ## file was modified
336 <strong>${filediff.source_file_path}</strong>
342 <strong>${filediff.source_file_path}</strong>
337 <% final_path = filediff.source_file_path %>
343 <% final_path = filediff.source_file_path %>
338 %endif
344 %endif
339 %else:
345 %else:
340 %if filediff.source_file_path:
346 %if filediff.source_file_path:
341 ## file was deleted
347 ## file was deleted
342 <strong>${filediff.source_file_path}</strong>
348 <strong>${filediff.source_file_path}</strong>
343 <% final_path = filediff.source_file_path %>
349 <% final_path = filediff.source_file_path %>
344 %else:
350 %else:
345 ## file was added
351 ## file was added
346 <strong>${filediff.target_file_path}</strong>
352 <strong>${filediff.target_file_path}</strong>
347 <% final_path = filediff.target_file_path %>
353 <% final_path = filediff.target_file_path %>
348 %endif
354 %endif
349 %endif
355 %endif
350 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
356 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
351 </span>
357 </span>
352 <span class="pill-group" style="float: left">
358 <span class="pill-group" style="float: left">
353 %if filediff.limited_diff:
359 %if filediff.limited_diff:
354 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
360 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
355 %endif
361 %endif
356
362
357 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
363 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
358 <span class="pill" op="renamed">renamed</span>
364 <span class="pill" op="renamed">renamed</span>
359 %endif
365 %endif
360
366
361 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
362 <span class="pill" op="copied">copied</span>
368 <span class="pill" op="copied">copied</span>
363 %endif
369 %endif
364
370
365 %if NEW_FILENODE in filediff.patch['stats']['ops']:
371 %if NEW_FILENODE in filediff.patch['stats']['ops']:
366 <span class="pill" op="created">created</span>
372 <span class="pill" op="created">created</span>
367 %if filediff['target_mode'].startswith('120'):
373 %if filediff['target_mode'].startswith('120'):
368 <span class="pill" op="symlink">symlink</span>
374 <span class="pill" op="symlink">symlink</span>
369 %else:
375 %else:
370 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
376 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
371 %endif
377 %endif
372 %endif
378 %endif
373
379
374 %if DEL_FILENODE in filediff.patch['stats']['ops']:
380 %if DEL_FILENODE in filediff.patch['stats']['ops']:
375 <span class="pill" op="removed">removed</span>
381 <span class="pill" op="removed">removed</span>
376 %endif
382 %endif
377
383
378 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
384 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
379 <span class="pill" op="mode">
385 <span class="pill" op="mode">
380 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
386 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
381 </span>
387 </span>
382 %endif
388 %endif
383 </span>
389 </span>
384
390
385 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
391 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
386
392
387 <span class="pill-group" style="float: right">
393 <span class="pill-group" style="float: right">
388 %if BIN_FILENODE in filediff.patch['stats']['ops']:
394 %if BIN_FILENODE in filediff.patch['stats']['ops']:
389 <span class="pill" op="binary">binary</span>
395 <span class="pill" op="binary">binary</span>
390 %if MOD_FILENODE in filediff.patch['stats']['ops']:
396 %if MOD_FILENODE in filediff.patch['stats']['ops']:
391 <span class="pill" op="modified">modified</span>
397 <span class="pill" op="modified">modified</span>
392 %endif
398 %endif
393 %endif
399 %endif
394 %if filediff.patch['stats']['added']:
400 %if filediff.patch['stats']['added']:
395 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
401 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
396 %endif
402 %endif
397 %if filediff.patch['stats']['deleted']:
403 %if filediff.patch['stats']['deleted']:
398 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
404 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
399 %endif
405 %endif
400 </span>
406 </span>
401
407
402 </%def>
408 </%def>
403
409
404 <%def name="nice_mode(filemode)">
410 <%def name="nice_mode(filemode)">
405 ${filemode.startswith('100') and filemode[3:] or filemode}
411 ${filemode.startswith('100') and filemode[3:] or filemode}
406 </%def>
412 </%def>
407
413
408 <%def name="diff_menu(filediff, use_comments=False)">
414 <%def name="diff_menu(filediff, use_comments=False)">
409 <div class="filediff-menu">
415 <div class="filediff-menu">
410 %if filediff.diffset.source_ref:
416 %if filediff.diffset.source_ref:
411 %if filediff.operation in ['D', 'M']:
417 %if filediff.operation in ['D', 'M']:
412 <a
418 <a
413 class="tooltip"
419 class="tooltip"
414 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
420 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
415 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
421 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
416 >
422 >
417 ${_('Show file before')}
423 ${_('Show file before')}
418 </a> |
424 </a> |
419 %else:
425 %else:
420 <span
426 <span
421 class="tooltip"
427 class="tooltip"
422 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
428 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
423 >
429 >
424 ${_('Show file before')}
430 ${_('Show file before')}
425 </span> |
431 </span> |
426 %endif
432 %endif
427 %if filediff.operation in ['A', 'M']:
433 %if filediff.operation in ['A', 'M']:
428 <a
434 <a
429 class="tooltip"
435 class="tooltip"
430 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)}"
436 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)}"
431 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
437 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
432 >
438 >
433 ${_('Show file after')}
439 ${_('Show file after')}
434 </a> |
440 </a> |
435 %else:
441 %else:
436 <span
442 <span
437 class="tooltip"
443 class="tooltip"
438 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
444 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
439 >
445 >
440 ${_('Show file after')}
446 ${_('Show file after')}
441 </span> |
447 </span> |
442 %endif
448 %endif
443 <a
449 <a
444 class="tooltip"
450 class="tooltip"
445 title="${h.tooltip(_('Raw diff'))}"
451 title="${h.tooltip(_('Raw diff'))}"
446 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'))}"
452 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'))}"
447 >
453 >
448 ${_('Raw diff')}
454 ${_('Raw diff')}
449 </a> |
455 </a> |
450 <a
456 <a
451 class="tooltip"
457 class="tooltip"
452 title="${h.tooltip(_('Download diff'))}"
458 title="${h.tooltip(_('Download diff'))}"
453 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'))}"
459 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'))}"
454 >
460 >
455 ${_('Download diff')}
461 ${_('Download diff')}
456 </a>
462 </a>
457 % if use_comments:
463 % if use_comments:
458 |
464 |
459 % endif
465 % endif
460
466
461 ## 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)
467 ## 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)
462 %if hasattr(c, 'ignorews_url'):
468 %if hasattr(c, 'ignorews_url'):
463 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
469 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
464 %endif
470 %endif
465 %if hasattr(c, 'context_url'):
471 %if hasattr(c, 'context_url'):
466 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
472 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
467 %endif
473 %endif
468
474
469 %if use_comments:
475 %if use_comments:
470 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
476 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
471 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
477 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
472 </a>
478 </a>
473 %endif
479 %endif
474 %endif
480 %endif
475 </div>
481 </div>
476 </%def>
482 </%def>
477
483
478
484
479 <%def name="inline_comments_container(comments)">
485 <%def name="inline_comments_container(comments)">
480 <div class="inline-comments">
486 <div class="inline-comments">
481 %for comment in comments:
487 %for comment in comments:
482 ${commentblock.comment_block(comment, inline=True)}
488 ${commentblock.comment_block(comment, inline=True)}
483 %endfor
489 %endfor
484
490
485 % if comments and comments[-1].outdated:
491 % if comments and comments[-1].outdated:
486 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
492 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
487 style="display: none;}">
493 style="display: none;}">
488 ${_('Add another comment')}
494 ${_('Add another comment')}
489 </span>
495 </span>
490 % else:
496 % else:
491 <span onclick="return Rhodecode.comments.createComment(this)"
497 <span onclick="return Rhodecode.comments.createComment(this)"
492 class="btn btn-secondary cb-comment-add-button">
498 class="btn btn-secondary cb-comment-add-button">
493 ${_('Add another comment')}
499 ${_('Add another comment')}
494 </span>
500 </span>
495 % endif
501 % endif
496
502
497 </div>
503 </div>
498 </%def>
504 </%def>
499
505
500
506
501 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
507 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
502 %for i, line in enumerate(hunk.sideside):
508 %for i, line in enumerate(hunk.sideside):
503 <%
509 <%
504 old_line_anchor, new_line_anchor = None, None
510 old_line_anchor, new_line_anchor = None, None
505 if line.original.lineno:
511 if line.original.lineno:
506 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
512 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
507 if line.modified.lineno:
513 if line.modified.lineno:
508 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
514 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
509 %>
515 %>
510
516
511 <tr class="cb-line">
517 <tr class="cb-line">
512 <td class="cb-data ${action_class(line.original.action)}"
518 <td class="cb-data ${action_class(line.original.action)}"
513 data-line-number="${line.original.lineno}"
519 data-line-number="${line.original.lineno}"
514 >
520 >
515 <div>
521 <div>
516 %if line.original.comments:
522 %if line.original.comments:
517 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
523 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
518 %endif
524 %endif
519 </div>
525 </div>
520 </td>
526 </td>
521 <td class="cb-lineno ${action_class(line.original.action)}"
527 <td class="cb-lineno ${action_class(line.original.action)}"
522 data-line-number="${line.original.lineno}"
528 data-line-number="${line.original.lineno}"
523 %if old_line_anchor:
529 %if old_line_anchor:
524 id="${old_line_anchor}"
530 id="${old_line_anchor}"
525 %endif
531 %endif
526 >
532 >
527 %if line.original.lineno:
533 %if line.original.lineno:
528 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
534 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
529 %endif
535 %endif
530 </td>
536 </td>
531 <td class="cb-content ${action_class(line.original.action)}"
537 <td class="cb-content ${action_class(line.original.action)}"
532 data-line-number="o${line.original.lineno}"
538 data-line-number="o${line.original.lineno}"
533 >
539 >
534 %if use_comments and line.original.lineno:
540 %if use_comments and line.original.lineno:
535 ${render_add_comment_button()}
541 ${render_add_comment_button()}
536 %endif
542 %endif
537 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
543 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
538 %if use_comments and line.original.lineno and line.original.comments:
544 %if use_comments and line.original.lineno and line.original.comments:
539 ${inline_comments_container(line.original.comments)}
545 ${inline_comments_container(line.original.comments)}
540 %endif
546 %endif
541 </td>
547 </td>
542 <td class="cb-data ${action_class(line.modified.action)}"
548 <td class="cb-data ${action_class(line.modified.action)}"
543 data-line-number="${line.modified.lineno}"
549 data-line-number="${line.modified.lineno}"
544 >
550 >
545 <div>
551 <div>
546 %if line.modified.comments:
552 %if line.modified.comments:
547 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
553 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
548 %endif
554 %endif
549 </div>
555 </div>
550 </td>
556 </td>
551 <td class="cb-lineno ${action_class(line.modified.action)}"
557 <td class="cb-lineno ${action_class(line.modified.action)}"
552 data-line-number="${line.modified.lineno}"
558 data-line-number="${line.modified.lineno}"
553 %if new_line_anchor:
559 %if new_line_anchor:
554 id="${new_line_anchor}"
560 id="${new_line_anchor}"
555 %endif
561 %endif
556 >
562 >
557 %if line.modified.lineno:
563 %if line.modified.lineno:
558 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
564 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
559 %endif
565 %endif
560 </td>
566 </td>
561 <td class="cb-content ${action_class(line.modified.action)}"
567 <td class="cb-content ${action_class(line.modified.action)}"
562 data-line-number="n${line.modified.lineno}"
568 data-line-number="n${line.modified.lineno}"
563 >
569 >
564 %if use_comments and line.modified.lineno:
570 %if use_comments and line.modified.lineno:
565 ${render_add_comment_button()}
571 ${render_add_comment_button()}
566 %endif
572 %endif
567 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
573 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
568 %if use_comments and line.modified.lineno and line.modified.comments:
574 %if use_comments and line.modified.lineno and line.modified.comments:
569 ${inline_comments_container(line.modified.comments)}
575 ${inline_comments_container(line.modified.comments)}
570 %endif
576 %endif
571 </td>
577 </td>
572 </tr>
578 </tr>
573 %endfor
579 %endfor
574 </%def>
580 </%def>
575
581
576
582
577 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
583 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
578 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
584 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
579 <%
585 <%
580 old_line_anchor, new_line_anchor = None, None
586 old_line_anchor, new_line_anchor = None, None
581 if old_line_no:
587 if old_line_no:
582 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
588 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
583 if new_line_no:
589 if new_line_no:
584 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
590 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
585 %>
591 %>
586 <tr class="cb-line">
592 <tr class="cb-line">
587 <td class="cb-data ${action_class(action)}">
593 <td class="cb-data ${action_class(action)}">
588 <div>
594 <div>
589 %if comments:
595 %if comments:
590 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
596 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
591 %endif
597 %endif
592 </div>
598 </div>
593 </td>
599 </td>
594 <td class="cb-lineno ${action_class(action)}"
600 <td class="cb-lineno ${action_class(action)}"
595 data-line-number="${old_line_no}"
601 data-line-number="${old_line_no}"
596 %if old_line_anchor:
602 %if old_line_anchor:
597 id="${old_line_anchor}"
603 id="${old_line_anchor}"
598 %endif
604 %endif
599 >
605 >
600 %if old_line_anchor:
606 %if old_line_anchor:
601 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
607 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
602 %endif
608 %endif
603 </td>
609 </td>
604 <td class="cb-lineno ${action_class(action)}"
610 <td class="cb-lineno ${action_class(action)}"
605 data-line-number="${new_line_no}"
611 data-line-number="${new_line_no}"
606 %if new_line_anchor:
612 %if new_line_anchor:
607 id="${new_line_anchor}"
613 id="${new_line_anchor}"
608 %endif
614 %endif
609 >
615 >
610 %if new_line_anchor:
616 %if new_line_anchor:
611 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
617 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
612 %endif
618 %endif
613 </td>
619 </td>
614 <td class="cb-content ${action_class(action)}"
620 <td class="cb-content ${action_class(action)}"
615 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
621 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
616 >
622 >
617 %if use_comments:
623 %if use_comments:
618 ${render_add_comment_button()}
624 ${render_add_comment_button()}
619 %endif
625 %endif
620 <span class="cb-code">${action} ${content or '' | n}</span>
626 <span class="cb-code">${action} ${content or '' | n}</span>
621 %if use_comments and comments:
627 %if use_comments and comments:
622 ${inline_comments_container(comments)}
628 ${inline_comments_container(comments)}
623 %endif
629 %endif
624 </td>
630 </td>
625 </tr>
631 </tr>
626 %endfor
632 %endfor
627 </%def>
633 </%def>
628
634
629 <%def name="render_add_comment_button()">
635 <%def name="render_add_comment_button()">
630 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
636 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
631 <span><i class="icon-comment"></i></span>
637 <span><i class="icon-comment"></i></span>
632 </button>
638 </button>
633 </%def>
639 </%def>
634
640
635 <%def name="render_diffset_menu()">
641 <%def name="render_diffset_menu()">
636
642
637 <div class="diffset-menu clearinner">
643 <div class="diffset-menu clearinner">
638 <div class="pull-right">
644 <div class="pull-right">
639 <div class="btn-group">
645 <div class="btn-group">
640
646
641 <a
647 <a
642 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
648 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
643 title="${h.tooltip(_('View side by side'))}"
649 title="${h.tooltip(_('View side by side'))}"
644 href="${h.current_route_path(request, diffmode='sideside')}">
650 href="${h.current_route_path(request, diffmode='sideside')}">
645 <span>${_('Side by Side')}</span>
651 <span>${_('Side by Side')}</span>
646 </a>
652 </a>
647 <a
653 <a
648 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
654 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
649 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
655 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
650 <span>${_('Unified')}</span>
656 <span>${_('Unified')}</span>
651 </a>
657 </a>
652 </div>
658 </div>
653 </div>
659 </div>
654
660
655 <div class="pull-left">
661 <div class="pull-left">
656 <div class="btn-group">
662 <div class="btn-group">
657 <a
663 <a
658 class="btn"
664 class="btn"
659 href="#"
665 href="#"
660 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
666 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
661 <a
667 <a
662 class="btn"
668 class="btn"
663 href="#"
669 href="#"
664 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
670 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
665 <a
671 <a
666 class="btn"
672 class="btn"
667 href="#"
673 href="#"
668 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
674 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
669 </div>
675 </div>
670 </div>
676 </div>
671 </div>
677 </div>
672 </%def>
678 </%def>
General Comments 0
You need to be logged in to leave comments. Login now