##// END OF EJS Templates
Diffs: fixed missing limited diff container display on large diffs
dan -
r3409:6b48c2d1 stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,775 +1,775 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from pygments.lexers.special import TextLexer, Token
28 28 from pygments.lexers import get_lexer_by_name
29 29
30 30 from rhodecode.lib.helpers import (
31 31 get_lexer_for_filenode, html_escape, get_custom_lexer)
32 32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
33 33 from rhodecode.lib.vcs.nodes import FileNode
34 34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
35 35 from rhodecode.lib.diff_match_patch import diff_match_patch
36 36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
37 37
38 38
39 39 plain_text_lexer = get_lexer_by_name(
40 40 'text', stripall=False, stripnl=False, ensurenl=False)
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def filenode_as_lines_tokens(filenode, lexer=None):
47 47 org_lexer = lexer
48 48 lexer = lexer or get_lexer_for_filenode(filenode)
49 49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
50 50 lexer, filenode, org_lexer)
51 51 tokens = tokenize_string(filenode.content, lexer)
52 52 lines = split_token_stream(tokens)
53 53 rv = list(lines)
54 54 return rv
55 55
56 56
57 57 def tokenize_string(content, lexer):
58 58 """
59 59 Use pygments to tokenize some content based on a lexer
60 60 ensuring all original new lines and whitespace is preserved
61 61 """
62 62
63 63 lexer.stripall = False
64 64 lexer.stripnl = False
65 65 lexer.ensurenl = False
66 66
67 67 if isinstance(lexer, TextLexer):
68 68 lexed = [(Token.Text, content)]
69 69 else:
70 70 lexed = lex(content, lexer)
71 71
72 72 for token_type, token_text in lexed:
73 73 yield pygment_token_class(token_type), token_text
74 74
75 75
76 76 def split_token_stream(tokens):
77 77 """
78 78 Take a list of (TokenType, text) tuples and split them by a string
79 79
80 80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
81 81 [(TEXT, 'some'), (TEXT, 'text'),
82 82 (TEXT, 'more'), (TEXT, 'text')]
83 83 """
84 84
85 85 buffer = []
86 86 for token_class, token_text in tokens:
87 87 parts = token_text.split('\n')
88 88 for part in parts[:-1]:
89 89 buffer.append((token_class, part))
90 90 yield buffer
91 91 buffer = []
92 92
93 93 buffer.append((token_class, parts[-1]))
94 94
95 95 if buffer:
96 96 yield buffer
97 97
98 98
99 99 def filenode_as_annotated_lines_tokens(filenode):
100 100 """
101 101 Take a file node and return a list of annotations => lines, if no annotation
102 102 is found, it will be None.
103 103
104 104 eg:
105 105
106 106 [
107 107 (annotation1, [
108 108 (1, line1_tokens_list),
109 109 (2, line2_tokens_list),
110 110 ]),
111 111 (annotation2, [
112 112 (3, line1_tokens_list),
113 113 ]),
114 114 (None, [
115 115 (4, line1_tokens_list),
116 116 ]),
117 117 (annotation1, [
118 118 (5, line1_tokens_list),
119 119 (6, line2_tokens_list),
120 120 ])
121 121 ]
122 122 """
123 123
124 124 commit_cache = {} # cache commit_getter lookups
125 125
126 126 def _get_annotation(commit_id, commit_getter):
127 127 if commit_id not in commit_cache:
128 128 commit_cache[commit_id] = commit_getter()
129 129 return commit_cache[commit_id]
130 130
131 131 annotation_lookup = {
132 132 line_no: _get_annotation(commit_id, commit_getter)
133 133 for line_no, commit_id, commit_getter, line_content
134 134 in filenode.annotate
135 135 }
136 136
137 137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 138 for line_no, tokens
139 139 in enumerate(filenode_as_lines_tokens(filenode), 1))
140 140
141 141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142 142
143 143 for annotation, group in grouped_annotations_lines:
144 144 yield (
145 145 annotation, [(line_no, tokens)
146 146 for (_, line_no, tokens) in group]
147 147 )
148 148
149 149
150 150 def render_tokenstream(tokenstream):
151 151 result = []
152 152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
153 153
154 154 if token_class:
155 155 result.append(u'<span class="%s">' % token_class)
156 156 else:
157 157 result.append(u'<span>')
158 158
159 159 for op_tag, token_text in token_ops_texts:
160 160
161 161 if op_tag:
162 162 result.append(u'<%s>' % op_tag)
163 163
164 164 escaped_text = html_escape(token_text)
165 165
166 166 # TODO: dan: investigate showing hidden characters like space/nl/tab
167 167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
168 168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
169 169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
170 170
171 171 result.append(escaped_text)
172 172
173 173 if op_tag:
174 174 result.append(u'</%s>' % op_tag)
175 175
176 176 result.append(u'</span>')
177 177
178 178 html = ''.join(result)
179 179 return html
180 180
181 181
182 182 def rollup_tokenstream(tokenstream):
183 183 """
184 184 Group a token stream of the format:
185 185
186 186 ('class', 'op', 'text')
187 187 or
188 188 ('class', 'text')
189 189
190 190 into
191 191
192 192 [('class1',
193 193 [('op1', 'text'),
194 194 ('op2', 'text')]),
195 195 ('class2',
196 196 [('op3', 'text')])]
197 197
198 198 This is used to get the minimal tags necessary when
199 199 rendering to html eg for a token stream ie.
200 200
201 201 <span class="A"><ins>he</ins>llo</span>
202 202 vs
203 203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
204 204
205 205 If a 2 tuple is passed in, the output op will be an empty string.
206 206
207 207 eg:
208 208
209 209 >>> rollup_tokenstream([('classA', '', 'h'),
210 210 ('classA', 'del', 'ell'),
211 211 ('classA', '', 'o'),
212 212 ('classB', '', ' '),
213 213 ('classA', '', 'the'),
214 214 ('classA', '', 're'),
215 215 ])
216 216
217 217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
218 218 ('classB', [('', ' ')],
219 219 ('classA', [('', 'there')]]
220 220
221 221 """
222 222 if tokenstream and len(tokenstream[0]) == 2:
223 223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
224 224
225 225 result = []
226 226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
227 227 ops = []
228 228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
229 229 text_buffer = []
230 230 for t_class, t_op, t_text in token_text_list:
231 231 text_buffer.append(t_text)
232 232 ops.append((token_op, ''.join(text_buffer)))
233 233 result.append((token_class, ops))
234 234 return result
235 235
236 236
237 237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
238 238 """
239 239 Converts a list of (token_class, token_text) tuples to a list of
240 240 (token_class, token_op, token_text) tuples where token_op is one of
241 241 ('ins', 'del', '')
242 242
243 243 :param old_tokens: list of (token_class, token_text) tuples of old line
244 244 :param new_tokens: list of (token_class, token_text) tuples of new line
245 245 :param use_diff_match_patch: boolean, will use google's diff match patch
246 246 library which has options to 'smooth' out the character by character
247 247 differences making nicer ins/del blocks
248 248 """
249 249
250 250 old_tokens_result = []
251 251 new_tokens_result = []
252 252
253 253 similarity = difflib.SequenceMatcher(None,
254 254 ''.join(token_text for token_class, token_text in old_tokens),
255 255 ''.join(token_text for token_class, token_text in new_tokens)
256 256 ).ratio()
257 257
258 258 if similarity < 0.6: # return, the blocks are too different
259 259 for token_class, token_text in old_tokens:
260 260 old_tokens_result.append((token_class, '', token_text))
261 261 for token_class, token_text in new_tokens:
262 262 new_tokens_result.append((token_class, '', token_text))
263 263 return old_tokens_result, new_tokens_result, similarity
264 264
265 265 token_sequence_matcher = difflib.SequenceMatcher(None,
266 266 [x[1] for x in old_tokens],
267 267 [x[1] for x in new_tokens])
268 268
269 269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
270 270 # check the differences by token block types first to give a more
271 271 # nicer "block" level replacement vs character diffs
272 272
273 273 if tag == 'equal':
274 274 for token_class, token_text in old_tokens[o1:o2]:
275 275 old_tokens_result.append((token_class, '', token_text))
276 276 for token_class, token_text in new_tokens[n1:n2]:
277 277 new_tokens_result.append((token_class, '', token_text))
278 278 elif tag == 'delete':
279 279 for token_class, token_text in old_tokens[o1:o2]:
280 280 old_tokens_result.append((token_class, 'del', token_text))
281 281 elif tag == 'insert':
282 282 for token_class, token_text in new_tokens[n1:n2]:
283 283 new_tokens_result.append((token_class, 'ins', token_text))
284 284 elif tag == 'replace':
285 285 # if same type token blocks must be replaced, do a diff on the
286 286 # characters in the token blocks to show individual changes
287 287
288 288 old_char_tokens = []
289 289 new_char_tokens = []
290 290 for token_class, token_text in old_tokens[o1:o2]:
291 291 for char in token_text:
292 292 old_char_tokens.append((token_class, char))
293 293
294 294 for token_class, token_text in new_tokens[n1:n2]:
295 295 for char in token_text:
296 296 new_char_tokens.append((token_class, char))
297 297
298 298 old_string = ''.join([token_text for
299 299 token_class, token_text in old_char_tokens])
300 300 new_string = ''.join([token_text for
301 301 token_class, token_text in new_char_tokens])
302 302
303 303 char_sequence = difflib.SequenceMatcher(
304 304 None, old_string, new_string)
305 305 copcodes = char_sequence.get_opcodes()
306 306 obuffer, nbuffer = [], []
307 307
308 308 if use_diff_match_patch:
309 309 dmp = diff_match_patch()
310 310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
311 311 reps = dmp.diff_main(old_string, new_string)
312 312 dmp.diff_cleanupEfficiency(reps)
313 313
314 314 a, b = 0, 0
315 315 for op, rep in reps:
316 316 l = len(rep)
317 317 if op == 0:
318 318 for i, c in enumerate(rep):
319 319 obuffer.append((old_char_tokens[a+i][0], '', c))
320 320 nbuffer.append((new_char_tokens[b+i][0], '', c))
321 321 a += l
322 322 b += l
323 323 elif op == -1:
324 324 for i, c in enumerate(rep):
325 325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
326 326 a += l
327 327 elif op == 1:
328 328 for i, c in enumerate(rep):
329 329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
330 330 b += l
331 331 else:
332 332 for ctag, co1, co2, cn1, cn2 in copcodes:
333 333 if ctag == 'equal':
334 334 for token_class, token_text in old_char_tokens[co1:co2]:
335 335 obuffer.append((token_class, '', token_text))
336 336 for token_class, token_text in new_char_tokens[cn1:cn2]:
337 337 nbuffer.append((token_class, '', token_text))
338 338 elif ctag == 'delete':
339 339 for token_class, token_text in old_char_tokens[co1:co2]:
340 340 obuffer.append((token_class, 'del', token_text))
341 341 elif ctag == 'insert':
342 342 for token_class, token_text in new_char_tokens[cn1:cn2]:
343 343 nbuffer.append((token_class, 'ins', token_text))
344 344 elif ctag == 'replace':
345 345 for token_class, token_text in old_char_tokens[co1:co2]:
346 346 obuffer.append((token_class, 'del', token_text))
347 347 for token_class, token_text in new_char_tokens[cn1:cn2]:
348 348 nbuffer.append((token_class, 'ins', token_text))
349 349
350 350 old_tokens_result.extend(obuffer)
351 351 new_tokens_result.extend(nbuffer)
352 352
353 353 return old_tokens_result, new_tokens_result, similarity
354 354
355 355
356 356 def diffset_node_getter(commit):
357 357 def get_node(fname):
358 358 try:
359 359 return commit.get_node(fname)
360 360 except NodeDoesNotExistError:
361 361 return None
362 362
363 363 return get_node
364 364
365 365
366 366 class DiffSet(object):
367 367 """
368 368 An object for parsing the diff result from diffs.DiffProcessor and
369 369 adding highlighting, side by side/unified renderings and line diffs
370 370 """
371 371
372 372 HL_REAL = 'REAL' # highlights using original file, slow
373 373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
374 374 # in the case of multiline code
375 375 HL_NONE = 'NONE' # no highlighting, fastest
376 376
377 377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
378 378 source_repo_name=None,
379 379 source_node_getter=lambda filename: None,
380 380 target_repo_name=None,
381 381 target_node_getter=lambda filename: None,
382 382 source_nodes=None, target_nodes=None,
383 383 # files over this size will use fast highlighting
384 384 max_file_size_limit=150 * 1024,
385 385 ):
386 386
387 387 self.highlight_mode = highlight_mode
388 388 self.highlighted_filenodes = {}
389 389 self.source_node_getter = source_node_getter
390 390 self.target_node_getter = target_node_getter
391 391 self.source_nodes = source_nodes or {}
392 392 self.target_nodes = target_nodes or {}
393 393 self.repo_name = repo_name
394 394 self.target_repo_name = target_repo_name or repo_name
395 395 self.source_repo_name = source_repo_name or repo_name
396 396 self.max_file_size_limit = max_file_size_limit
397 397
398 398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
399 399 diffset = AttributeDict(dict(
400 400 lines_added=0,
401 401 lines_deleted=0,
402 402 changed_files=0,
403 403 files=[],
404 404 file_stats={},
405 405 limited_diff=isinstance(patchset, LimitedDiffContainer),
406 406 repo_name=self.repo_name,
407 407 target_repo_name=self.target_repo_name,
408 408 source_repo_name=self.source_repo_name,
409 409 source_ref=source_ref,
410 410 target_ref=target_ref,
411 411 ))
412 412 for patch in patchset:
413 413 diffset.file_stats[patch['filename']] = patch['stats']
414 414 filediff = self.render_patch(patch)
415 415 filediff.diffset = StrictAttributeDict(dict(
416 416 source_ref=diffset.source_ref,
417 417 target_ref=diffset.target_ref,
418 418 repo_name=diffset.repo_name,
419 419 source_repo_name=diffset.source_repo_name,
420 420 target_repo_name=diffset.target_repo_name,
421 421 ))
422 422 diffset.files.append(filediff)
423 423 diffset.changed_files += 1
424 424 if not patch['stats']['binary']:
425 425 diffset.lines_added += patch['stats']['added']
426 426 diffset.lines_deleted += patch['stats']['deleted']
427 427
428 428 return diffset
429 429
430 430 _lexer_cache = {}
431 431
432 432 def _get_lexer_for_filename(self, filename, filenode=None):
433 433 # cached because we might need to call it twice for source/target
434 434 if filename not in self._lexer_cache:
435 435 if filenode:
436 436 lexer = filenode.lexer
437 437 extension = filenode.extension
438 438 else:
439 439 lexer = FileNode.get_lexer(filename=filename)
440 440 extension = filename.split('.')[-1]
441 441
442 442 lexer = get_custom_lexer(extension) or lexer
443 443 self._lexer_cache[filename] = lexer
444 444 return self._lexer_cache[filename]
445 445
446 446 def render_patch(self, patch):
447 447 log.debug('rendering diff for %r', patch['filename'])
448 448
449 449 source_filename = patch['original_filename']
450 450 target_filename = patch['filename']
451 451
452 452 source_lexer = plain_text_lexer
453 453 target_lexer = plain_text_lexer
454 454
455 455 if not patch['stats']['binary']:
456 456 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
457 457 hl_mode = node_hl_mode or self.highlight_mode
458 458
459 459 if hl_mode == self.HL_REAL:
460 460 if (source_filename and patch['operation'] in ('D', 'M')
461 461 and source_filename not in self.source_nodes):
462 462 self.source_nodes[source_filename] = (
463 463 self.source_node_getter(source_filename))
464 464
465 465 if (target_filename and patch['operation'] in ('A', 'M')
466 466 and target_filename not in self.target_nodes):
467 467 self.target_nodes[target_filename] = (
468 468 self.target_node_getter(target_filename))
469 469
470 470 elif hl_mode == self.HL_FAST:
471 471 source_lexer = self._get_lexer_for_filename(source_filename)
472 472 target_lexer = self._get_lexer_for_filename(target_filename)
473 473
474 474 source_file = self.source_nodes.get(source_filename, source_filename)
475 475 target_file = self.target_nodes.get(target_filename, target_filename)
476 476 raw_id_uid = ''
477 477 if self.source_nodes.get(source_filename):
478 478 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
479 479
480 480 if not raw_id_uid and self.target_nodes.get(target_filename):
481 481 # in case this is a new file we only have it in target
482 482 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
483 483
484 484 source_filenode, target_filenode = None, None
485 485
486 486 # TODO: dan: FileNode.lexer works on the content of the file - which
487 487 # can be slow - issue #4289 explains a lexer clean up - which once
488 488 # done can allow caching a lexer for a filenode to avoid the file lookup
489 489 if isinstance(source_file, FileNode):
490 490 source_filenode = source_file
491 491 #source_lexer = source_file.lexer
492 492 source_lexer = self._get_lexer_for_filename(source_filename)
493 493 source_file.lexer = source_lexer
494 494
495 495 if isinstance(target_file, FileNode):
496 496 target_filenode = target_file
497 497 #target_lexer = target_file.lexer
498 498 target_lexer = self._get_lexer_for_filename(target_filename)
499 499 target_file.lexer = target_lexer
500 500
501 501 source_file_path, target_file_path = None, None
502 502
503 503 if source_filename != '/dev/null':
504 504 source_file_path = source_filename
505 505 if target_filename != '/dev/null':
506 506 target_file_path = target_filename
507 507
508 508 source_file_type = source_lexer.name
509 509 target_file_type = target_lexer.name
510 510
511 511 filediff = AttributeDict({
512 512 'source_file_path': source_file_path,
513 513 'target_file_path': target_file_path,
514 514 'source_filenode': source_filenode,
515 515 'target_filenode': target_filenode,
516 516 'source_file_type': target_file_type,
517 517 'target_file_type': source_file_type,
518 518 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
519 519 'operation': patch['operation'],
520 520 'source_mode': patch['stats']['old_mode'],
521 521 'target_mode': patch['stats']['new_mode'],
522 'limited_diff': isinstance(patch, LimitedDiffContainer),
522 'limited_diff': patch['is_limited_diff'],
523 523 'hunks': [],
524 524 'hunk_ops': None,
525 525 'diffset': self,
526 526 'raw_id': raw_id_uid,
527 527 })
528 528
529 529 file_chunks = patch['chunks'][1:]
530 530 for hunk in file_chunks:
531 531 hunkbit = self.parse_hunk(hunk, source_file, target_file)
532 532 hunkbit.source_file_path = source_file_path
533 533 hunkbit.target_file_path = target_file_path
534 534 filediff.hunks.append(hunkbit)
535 535
536 536 # Simulate hunk on OPS type line which doesn't really contain any diff
537 537 # this allows commenting on those
538 538 if not file_chunks:
539 539 actions = []
540 540 for op_id, op_text in filediff.patch['stats']['ops'].items():
541 541 if op_id == DEL_FILENODE:
542 542 actions.append(u'file was removed')
543 543 elif op_id == BIN_FILENODE:
544 544 actions.append(u'binary diff hidden')
545 545 else:
546 546 actions.append(safe_unicode(op_text))
547 547 action_line = u'NO CONTENT: ' + \
548 548 u', '.join(actions) or u'UNDEFINED_ACTION'
549 549
550 550 hunk_ops = {'source_length': 0, 'source_start': 0,
551 551 'lines': [
552 552 {'new_lineno': 0, 'old_lineno': 1,
553 553 'action': 'unmod-no-hl', 'line': action_line}
554 554 ],
555 555 'section_header': u'', 'target_start': 1, 'target_length': 1}
556 556
557 557 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
558 558 hunkbit.source_file_path = source_file_path
559 559 hunkbit.target_file_path = target_file_path
560 560 filediff.hunk_ops = hunkbit
561 561 return filediff
562 562
563 563 def parse_hunk(self, hunk, source_file, target_file):
564 564 result = AttributeDict(dict(
565 565 source_start=hunk['source_start'],
566 566 source_length=hunk['source_length'],
567 567 target_start=hunk['target_start'],
568 568 target_length=hunk['target_length'],
569 569 section_header=hunk['section_header'],
570 570 lines=[],
571 571 ))
572 572 before, after = [], []
573 573
574 574 for line in hunk['lines']:
575 575 if line['action'] in ['unmod', 'unmod-no-hl']:
576 576 no_hl = line['action'] == 'unmod-no-hl'
577 577 result.lines.extend(
578 578 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
579 579 after.append(line)
580 580 before.append(line)
581 581 elif line['action'] == 'add':
582 582 after.append(line)
583 583 elif line['action'] == 'del':
584 584 before.append(line)
585 585 elif line['action'] == 'old-no-nl':
586 586 before.append(line)
587 587 elif line['action'] == 'new-no-nl':
588 588 after.append(line)
589 589
590 590 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
591 591 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
592 592 result.lines.extend(
593 593 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
594 594 # NOTE(marcink): we must keep list() call here so we can cache the result...
595 595 result.unified = list(self.as_unified(result.lines))
596 596 result.sideside = result.lines
597 597
598 598 return result
599 599
600 600 def parse_lines(self, before_lines, after_lines, source_file, target_file,
601 601 no_hl=False):
602 602 # TODO: dan: investigate doing the diff comparison and fast highlighting
603 603 # on the entire before and after buffered block lines rather than by
604 604 # line, this means we can get better 'fast' highlighting if the context
605 605 # allows it - eg.
606 606 # line 4: """
607 607 # line 5: this gets highlighted as a string
608 608 # line 6: """
609 609
610 610 lines = []
611 611
612 612 before_newline = AttributeDict()
613 613 after_newline = AttributeDict()
614 614 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
615 615 before_newline_line = before_lines.pop(-1)
616 616 before_newline.content = '\n {}'.format(
617 617 render_tokenstream(
618 618 [(x[0], '', x[1])
619 619 for x in [('nonl', before_newline_line['line'])]]))
620 620
621 621 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
622 622 after_newline_line = after_lines.pop(-1)
623 623 after_newline.content = '\n {}'.format(
624 624 render_tokenstream(
625 625 [(x[0], '', x[1])
626 626 for x in [('nonl', after_newline_line['line'])]]))
627 627
628 628 while before_lines or after_lines:
629 629 before, after = None, None
630 630 before_tokens, after_tokens = None, None
631 631
632 632 if before_lines:
633 633 before = before_lines.pop(0)
634 634 if after_lines:
635 635 after = after_lines.pop(0)
636 636
637 637 original = AttributeDict()
638 638 modified = AttributeDict()
639 639
640 640 if before:
641 641 if before['action'] == 'old-no-nl':
642 642 before_tokens = [('nonl', before['line'])]
643 643 else:
644 644 before_tokens = self.get_line_tokens(
645 645 line_text=before['line'], line_number=before['old_lineno'],
646 646 input_file=source_file, no_hl=no_hl)
647 647 original.lineno = before['old_lineno']
648 648 original.content = before['line']
649 649 original.action = self.action_to_op(before['action'])
650 650
651 651 original.get_comment_args = (
652 652 source_file, 'o', before['old_lineno'])
653 653
654 654 if after:
655 655 if after['action'] == 'new-no-nl':
656 656 after_tokens = [('nonl', after['line'])]
657 657 else:
658 658 after_tokens = self.get_line_tokens(
659 659 line_text=after['line'], line_number=after['new_lineno'],
660 660 input_file=target_file, no_hl=no_hl)
661 661 modified.lineno = after['new_lineno']
662 662 modified.content = after['line']
663 663 modified.action = self.action_to_op(after['action'])
664 664
665 665 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
666 666
667 667 # diff the lines
668 668 if before_tokens and after_tokens:
669 669 o_tokens, m_tokens, similarity = tokens_diff(
670 670 before_tokens, after_tokens)
671 671 original.content = render_tokenstream(o_tokens)
672 672 modified.content = render_tokenstream(m_tokens)
673 673 elif before_tokens:
674 674 original.content = render_tokenstream(
675 675 [(x[0], '', x[1]) for x in before_tokens])
676 676 elif after_tokens:
677 677 modified.content = render_tokenstream(
678 678 [(x[0], '', x[1]) for x in after_tokens])
679 679
680 680 if not before_lines and before_newline:
681 681 original.content += before_newline.content
682 682 before_newline = None
683 683 if not after_lines and after_newline:
684 684 modified.content += after_newline.content
685 685 after_newline = None
686 686
687 687 lines.append(AttributeDict({
688 688 'original': original,
689 689 'modified': modified,
690 690 }))
691 691
692 692 return lines
693 693
694 694 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
695 695 filenode = None
696 696 filename = None
697 697
698 698 if isinstance(input_file, basestring):
699 699 filename = input_file
700 700 elif isinstance(input_file, FileNode):
701 701 filenode = input_file
702 702 filename = input_file.unicode_path
703 703
704 704 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
705 705 if hl_mode == self.HL_REAL and filenode:
706 706 lexer = self._get_lexer_for_filename(filename)
707 707 file_size_allowed = input_file.size < self.max_file_size_limit
708 708 if line_number and file_size_allowed:
709 709 return self.get_tokenized_filenode_line(
710 710 input_file, line_number, lexer)
711 711
712 712 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
713 713 lexer = self._get_lexer_for_filename(filename)
714 714 return list(tokenize_string(line_text, lexer))
715 715
716 716 return list(tokenize_string(line_text, plain_text_lexer))
717 717
718 718 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
719 719
720 720 if filenode not in self.highlighted_filenodes:
721 721 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
722 722 self.highlighted_filenodes[filenode] = tokenized_lines
723 723 return self.highlighted_filenodes[filenode][line_number - 1]
724 724
725 725 def action_to_op(self, action):
726 726 return {
727 727 'add': '+',
728 728 'del': '-',
729 729 'unmod': ' ',
730 730 'unmod-no-hl': ' ',
731 731 'old-no-nl': ' ',
732 732 'new-no-nl': ' ',
733 733 }.get(action, action)
734 734
735 735 def as_unified(self, lines):
736 736 """
737 737 Return a generator that yields the lines of a diff in unified order
738 738 """
739 739 def generator():
740 740 buf = []
741 741 for line in lines:
742 742
743 743 if buf and not line.original or line.original.action == ' ':
744 744 for b in buf:
745 745 yield b
746 746 buf = []
747 747
748 748 if line.original:
749 749 if line.original.action == ' ':
750 750 yield (line.original.lineno, line.modified.lineno,
751 751 line.original.action, line.original.content,
752 752 line.original.get_comment_args)
753 753 continue
754 754
755 755 if line.original.action == '-':
756 756 yield (line.original.lineno, None,
757 757 line.original.action, line.original.content,
758 758 line.original.get_comment_args)
759 759
760 760 if line.modified.action == '+':
761 761 buf.append((
762 762 None, line.modified.lineno,
763 763 line.modified.action, line.modified.content,
764 764 line.modified.get_comment_args))
765 765 continue
766 766
767 767 if line.modified:
768 768 yield (None, line.modified.lineno,
769 769 line.modified.action, line.modified.content,
770 770 line.modified.get_comment_args)
771 771
772 772 for b in buf:
773 773 yield b
774 774
775 775 return generator()
@@ -1,1025 +1,1025 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 52 )">
53 53 %if use_comments:
54 54 <div id="cb-comments-inline-container-template" class="js-template">
55 55 ${inline_comments_container([], inline_comments)}
56 56 </div>
57 57 <div class="js-template" id="cb-comment-inline-form-template">
58 58 <div class="comment-inline-form ac">
59 59
60 60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 61 ## render template for inline comments
62 62 ${commentblock.comment_form(form_type='inline')}
63 63 %else:
64 64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 65 <div class="pull-left">
66 66 <div class="comment-help pull-right">
67 67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button pull-right">
71 71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 72 ${_('Cancel')}
73 73 </button>
74 74 </div>
75 75 <div class="clearfix"></div>
76 76 ${h.end_form()}
77 77 %endif
78 78 </div>
79 79 </div>
80 80
81 81 %endif
82 82 <%
83 83 collapse_all = len(diffset.files) > collapse_when_files_over
84 84 %>
85 85
86 86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 87 <style>
88 88 .wrapper {
89 89 max-width: 1600px !important;
90 90 }
91 91 </style>
92 92 %endif
93 93
94 94 %if ruler_at_chars:
95 95 <style>
96 96 .diff table.cb .cb-content:after {
97 97 content: "";
98 98 border-left: 1px solid blue;
99 99 position: absolute;
100 100 top: 0;
101 101 height: 18px;
102 102 opacity: .2;
103 103 z-index: 10;
104 104 //## +5 to account for diff action (+/-)
105 105 left: ${ruler_at_chars + 5}ch;
106 106 </style>
107 107 %endif
108 108
109 109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 111 %if commit:
112 112 <div class="pull-right">
113 113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 114 ${_('Browse Files')}
115 115 </a>
116 116 </div>
117 117 %endif
118 118 <h2 class="clearinner">
119 119 ## invidual commit
120 120 % if commit:
121 121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 122 ${h.age_component(commit.date)}
123 123 % if diffset.limited_diff:
124 - ${_('The requested commit is too big and content was truncated.')}
124 - ${_('The requested changes are too big and content was truncated.')}
125 125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 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 127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 128 ## compare diff, has no file-selector and we want to show stats anyway
129 129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 132 % endif
133 133 % else:
134 134 ## pull requests/compare
135 135 ${_('File Changes')}
136 136 % endif
137 137
138 138 </h2>
139 139 </div>
140 140
141 141 %if diffset.has_hidden_changes:
142 142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 143 %elif not diffset.files:
144 144 <p class="empty_data">${_('No files')}</p>
145 145 %endif
146 146
147 147 <div class="filediffs">
148 148
149 149 ## initial value could be marked as False later on
150 150 <% over_lines_changed_limit = False %>
151 151 %for i, filediff in enumerate(diffset.files):
152 152
153 153 <%
154 154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 156 %>
157 157 ## anchor with support of sticky header
158 158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159 159
160 160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 161 <div
162 162 class="filediff"
163 163 data-f-path="${filediff.patch['filename']}"
164 164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 165 >
166 166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 167 <div class="filediff-collapse-indicator"></div>
168 168 ${diff_ops(filediff)}
169 169 </label>
170 170
171 171 ${diff_menu(filediff, use_comments=use_comments)}
172 172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
173 173
174 174 ## new/deleted/empty content case
175 175 % if not filediff.hunks:
176 176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 178 % endif
179 179
180 180 %if filediff.limited_diff:
181 181 <tr class="cb-warning cb-collapser">
182 182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('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>
183 ${_('The requested commit or file 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>
184 184 </td>
185 185 </tr>
186 186 %else:
187 187 %if over_lines_changed_limit:
188 188 <tr class="cb-warning cb-collapser">
189 189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 191 <a href="#" class="cb-expand"
192 192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 193 </a>
194 194 <a href="#" class="cb-collapse"
195 195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 196 </a>
197 197 </td>
198 198 </tr>
199 199 %endif
200 200 %endif
201 201
202 202 % for hunk in filediff.hunks:
203 203 <tr class="cb-hunk">
204 204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 205 ## TODO: dan: add ajax loading of more context here
206 206 ## <a href="#">
207 207 <i class="icon-more"></i>
208 208 ## </a>
209 209 </td>
210 210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 211 @@
212 212 -${hunk.source_start},${hunk.source_length}
213 213 +${hunk.target_start},${hunk.target_length}
214 214 ${hunk.section_header}
215 215 </td>
216 216 </tr>
217 217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 218 % endfor
219 219
220 220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221 221
222 222 ## outdated comments that do not fit into currently displayed lines
223 223 % for lineno, comments in unmatched_comments.items():
224 224
225 225 %if c.user_session_attrs["diffmode"] == 'unified':
226 226 % if loop.index == 0:
227 227 <tr class="cb-hunk">
228 228 <td colspan="3"></td>
229 229 <td>
230 230 <div>
231 231 ${_('Unmatched inline comments below')}
232 232 </div>
233 233 </td>
234 234 </tr>
235 235 % endif
236 236 <tr class="cb-line">
237 237 <td class="cb-data cb-context"></td>
238 238 <td class="cb-lineno cb-context"></td>
239 239 <td class="cb-lineno cb-context"></td>
240 240 <td class="cb-content cb-context">
241 241 ${inline_comments_container(comments, inline_comments)}
242 242 </td>
243 243 </tr>
244 244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 245 % if loop.index == 0:
246 246 <tr class="cb-comment-info">
247 247 <td colspan="2"></td>
248 248 <td class="cb-line">
249 249 <div>
250 250 ${_('Unmatched inline comments below')}
251 251 </div>
252 252 </td>
253 253 <td colspan="2"></td>
254 254 <td class="cb-line">
255 255 <div>
256 256 ${_('Unmatched comments below')}
257 257 </div>
258 258 </td>
259 259 </tr>
260 260 % endif
261 261 <tr class="cb-line">
262 262 <td class="cb-data cb-context"></td>
263 263 <td class="cb-lineno cb-context"></td>
264 264 <td class="cb-content cb-context">
265 265 % if lineno.startswith('o'):
266 266 ${inline_comments_container(comments, inline_comments)}
267 267 % endif
268 268 </td>
269 269
270 270 <td class="cb-data cb-context"></td>
271 271 <td class="cb-lineno cb-context"></td>
272 272 <td class="cb-content cb-context">
273 273 % if lineno.startswith('n'):
274 274 ${inline_comments_container(comments, inline_comments)}
275 275 % endif
276 276 </td>
277 277 </tr>
278 278 %endif
279 279
280 280 % endfor
281 281
282 282 </table>
283 283 </div>
284 284 %endfor
285 285
286 286 ## outdated comments that are made for a file that has been deleted
287 287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 288 <%
289 289 display_state = 'display: none'
290 290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 291 if open_comments_in_file:
292 292 display_state = ''
293 293 %>
294 294 <div class="filediffs filediff-outdated" style="${display_state}">
295 295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 298 <div class="filediff-collapse-indicator"></div>
299 299 <span class="pill">
300 300 ## file was deleted
301 301 <strong>${filename}</strong>
302 302 </span>
303 303 <span class="pill-group" style="float: left">
304 304 ## file op, doesn't need translation
305 305 <span class="pill" op="removed">removed in this version</span>
306 306 </span>
307 307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}"></a>
308 308 <span class="pill-group" style="float: right">
309 309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 310 </span>
311 311 </label>
312 312
313 313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 314 <tr>
315 315 % if c.user_session_attrs["diffmode"] == 'unified':
316 316 <td></td>
317 317 %endif
318 318
319 319 <td></td>
320 320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 322 </td>
323 323 </tr>
324 324 %if c.user_session_attrs["diffmode"] == 'unified':
325 325 <tr class="cb-line">
326 326 <td class="cb-data cb-context"></td>
327 327 <td class="cb-lineno cb-context"></td>
328 328 <td class="cb-lineno cb-context"></td>
329 329 <td class="cb-content cb-context">
330 330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 331 </td>
332 332 </tr>
333 333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 334 <tr class="cb-line">
335 335 <td class="cb-data cb-context"></td>
336 336 <td class="cb-lineno cb-context"></td>
337 337 <td class="cb-content cb-context"></td>
338 338
339 339 <td class="cb-data cb-context"></td>
340 340 <td class="cb-lineno cb-context"></td>
341 341 <td class="cb-content cb-context">
342 342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 343 </td>
344 344 </tr>
345 345 %endif
346 346 </table>
347 347 </div>
348 348 </div>
349 349 % endfor
350 350
351 351 </div>
352 352 </div>
353 353 </%def>
354 354
355 355 <%def name="diff_ops(filediff)">
356 356 <%
357 357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 359 %>
360 360 <span class="pill">
361 361 %if filediff.source_file_path and filediff.target_file_path:
362 362 %if filediff.source_file_path != filediff.target_file_path:
363 363 ## file was renamed, or copied
364 364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 365 <strong>${filediff.target_file_path}</strong><del>${filediff.source_file_path}</del>
366 366 <% final_path = filediff.target_file_path %>
367 367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 368 <strong>${filediff.target_file_path}</strong>${filediff.source_file_path}
369 369 <% final_path = filediff.target_file_path %>
370 370 %endif
371 371 %else:
372 372 ## file was modified
373 373 <strong>${filediff.source_file_path}</strong>
374 374 <% final_path = filediff.source_file_path %>
375 375 %endif
376 376 %else:
377 377 %if filediff.source_file_path:
378 378 ## file was deleted
379 379 <strong>${filediff.source_file_path}</strong>
380 380 <% final_path = filediff.source_file_path %>
381 381 %else:
382 382 ## file was added
383 383 <strong>${filediff.target_file_path}</strong>
384 384 <% final_path = filediff.target_file_path %>
385 385 %endif
386 386 %endif
387 387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 388 </span>
389 389 ## anchor link
390 390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></a>
391 391
392 392 <span class="pill-group" style="float: right">
393 393
394 394 ## ops pills
395 395 %if filediff.limited_diff:
396 396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 397 %endif
398 398
399 399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 400 <span class="pill" op="created">created</span>
401 401 %if filediff['target_mode'].startswith('120'):
402 402 <span class="pill" op="symlink">symlink</span>
403 403 %else:
404 404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 405 %endif
406 406 %endif
407 407
408 408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 409 <span class="pill" op="renamed">renamed</span>
410 410 %endif
411 411
412 412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 413 <span class="pill" op="copied">copied</span>
414 414 %endif
415 415
416 416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 417 <span class="pill" op="removed">removed</span>
418 418 %endif
419 419
420 420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 421 <span class="pill" op="mode">
422 422 ${nice_mode(filediff['source_mode'])}${nice_mode(filediff['target_mode'])}
423 423 </span>
424 424 %endif
425 425
426 426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 427 <span class="pill" op="binary">binary</span>
428 428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 429 <span class="pill" op="modified">modified</span>
430 430 %endif
431 431 %endif
432 432
433 433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435 435
436 436 </span>
437 437
438 438 </%def>
439 439
440 440 <%def name="nice_mode(filemode)">
441 441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 442 </%def>
443 443
444 444 <%def name="diff_menu(filediff, use_comments=False)">
445 445 <div class="filediff-menu">
446 446
447 447 %if filediff.diffset.source_ref:
448 448
449 449 ## FILE BEFORE CHANGES
450 450 %if filediff.operation in ['D', 'M']:
451 451 <a
452 452 class="tooltip"
453 453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
454 454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
455 455 >
456 456 ${_('Show file before')}
457 457 </a> |
458 458 %else:
459 459 <span
460 460 class="tooltip"
461 461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
462 462 >
463 463 ${_('Show file before')}
464 464 </span> |
465 465 %endif
466 466
467 467 ## FILE AFTER CHANGES
468 468 %if filediff.operation in ['A', 'M']:
469 469 <a
470 470 class="tooltip"
471 471 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)}"
472 472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
473 473 >
474 474 ${_('Show file after')}
475 475 </a>
476 476 %else:
477 477 <span
478 478 class="tooltip"
479 479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
480 480 >
481 481 ${_('Show file after')}
482 482 </span>
483 483 %endif
484 484
485 485 % if use_comments:
486 486 |
487 487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
488 488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
489 489 </a>
490 490 % endif
491 491
492 492 %endif
493 493
494 494 </div>
495 495 </%def>
496 496
497 497
498 498 <%def name="inline_comments_container(comments, inline_comments)">
499 499 <div class="inline-comments">
500 500 %for comment in comments:
501 501 ${commentblock.comment_block(comment, inline=True)}
502 502 %endfor
503 503 % if comments and comments[-1].outdated:
504 504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
505 505 style="display: none;}">
506 506 ${_('Add another comment')}
507 507 </span>
508 508 % else:
509 509 <span onclick="return Rhodecode.comments.createComment(this)"
510 510 class="btn btn-secondary cb-comment-add-button">
511 511 ${_('Add another comment')}
512 512 </span>
513 513 % endif
514 514
515 515 </div>
516 516 </%def>
517 517
518 518 <%!
519 519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
520 520 if hasattr(filename, 'unicode_path'):
521 521 filename = filename.unicode_path
522 522
523 523 if not isinstance(filename, basestring):
524 524 return None
525 525
526 526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
527 527
528 528 if comments and filename in comments:
529 529 file_comments = comments[filename]
530 530 if line_key in file_comments:
531 531 data = file_comments.pop(line_key)
532 532 return data
533 533 %>
534 534
535 535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
536 536 %for i, line in enumerate(hunk.sideside):
537 537 <%
538 538 old_line_anchor, new_line_anchor = None, None
539 539
540 540 if line.original.lineno:
541 541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
542 542 if line.modified.lineno:
543 543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
544 544 %>
545 545
546 546 <tr class="cb-line">
547 547 <td class="cb-data ${action_class(line.original.action)}"
548 548 data-line-no="${line.original.lineno}"
549 549 >
550 550 <div>
551 551
552 552 <% line_old_comments = None %>
553 553 %if line.original.get_comment_args:
554 554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
555 555 %endif
556 556 %if line_old_comments:
557 557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
558 558 % if has_outdated:
559 559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
560 560 % else:
561 561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
562 562 % endif
563 563 %endif
564 564 </div>
565 565 </td>
566 566 <td class="cb-lineno ${action_class(line.original.action)}"
567 567 data-line-no="${line.original.lineno}"
568 568 %if old_line_anchor:
569 569 id="${old_line_anchor}"
570 570 %endif
571 571 >
572 572 %if line.original.lineno:
573 573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
574 574 %endif
575 575 </td>
576 576 <td class="cb-content ${action_class(line.original.action)}"
577 577 data-line-no="o${line.original.lineno}"
578 578 >
579 579 %if use_comments and line.original.lineno:
580 580 ${render_add_comment_button()}
581 581 %endif
582 582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
583 583
584 584 %if use_comments and line.original.lineno and line_old_comments:
585 585 ${inline_comments_container(line_old_comments, inline_comments)}
586 586 %endif
587 587
588 588 </td>
589 589 <td class="cb-data ${action_class(line.modified.action)}"
590 590 data-line-no="${line.modified.lineno}"
591 591 >
592 592 <div>
593 593
594 594 %if line.modified.get_comment_args:
595 595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
596 596 %else:
597 597 <% line_new_comments = None%>
598 598 %endif
599 599 %if line_new_comments:
600 600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
601 601 % if has_outdated:
602 602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
603 603 % else:
604 604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
605 605 % endif
606 606 %endif
607 607 </div>
608 608 </td>
609 609 <td class="cb-lineno ${action_class(line.modified.action)}"
610 610 data-line-no="${line.modified.lineno}"
611 611 %if new_line_anchor:
612 612 id="${new_line_anchor}"
613 613 %endif
614 614 >
615 615 %if line.modified.lineno:
616 616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
617 617 %endif
618 618 </td>
619 619 <td class="cb-content ${action_class(line.modified.action)}"
620 620 data-line-no="n${line.modified.lineno}"
621 621 >
622 622 %if use_comments and line.modified.lineno:
623 623 ${render_add_comment_button()}
624 624 %endif
625 625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
626 626 %if use_comments and line.modified.lineno and line_new_comments:
627 627 ${inline_comments_container(line_new_comments, inline_comments)}
628 628 %endif
629 629 </td>
630 630 </tr>
631 631 %endfor
632 632 </%def>
633 633
634 634
635 635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
636 636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
637 637
638 638 <%
639 639 old_line_anchor, new_line_anchor = None, None
640 640 if old_line_no:
641 641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
642 642 if new_line_no:
643 643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
644 644 %>
645 645 <tr class="cb-line">
646 646 <td class="cb-data ${action_class(action)}">
647 647 <div>
648 648
649 649 %if comments_args:
650 650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
651 651 %else:
652 652 <% comments = None %>
653 653 %endif
654 654
655 655 % if comments:
656 656 <% has_outdated = any([x.outdated for x in comments]) %>
657 657 % if has_outdated:
658 658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
659 659 % else:
660 660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
661 661 % endif
662 662 % endif
663 663 </div>
664 664 </td>
665 665 <td class="cb-lineno ${action_class(action)}"
666 666 data-line-no="${old_line_no}"
667 667 %if old_line_anchor:
668 668 id="${old_line_anchor}"
669 669 %endif
670 670 >
671 671 %if old_line_anchor:
672 672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
673 673 %endif
674 674 </td>
675 675 <td class="cb-lineno ${action_class(action)}"
676 676 data-line-no="${new_line_no}"
677 677 %if new_line_anchor:
678 678 id="${new_line_anchor}"
679 679 %endif
680 680 >
681 681 %if new_line_anchor:
682 682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
683 683 %endif
684 684 </td>
685 685 <td class="cb-content ${action_class(action)}"
686 686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
687 687 >
688 688 %if use_comments:
689 689 ${render_add_comment_button()}
690 690 %endif
691 691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
692 692 %if use_comments and comments:
693 693 ${inline_comments_container(comments, inline_comments)}
694 694 %endif
695 695 </td>
696 696 </tr>
697 697 %endfor
698 698 </%def>
699 699
700 700
701 701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
702 702 % if diff_mode == 'unified':
703 703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
704 704 % elif diff_mode == 'sideside':
705 705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
706 706 % else:
707 707 <tr class="cb-line">
708 708 <td>unknown diff mode</td>
709 709 </tr>
710 710 % endif
711 711 </%def>file changes
712 712
713 713
714 714 <%def name="render_add_comment_button()">
715 715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
716 716 <span><i class="icon-comment"></i></span>
717 717 </button>
718 718 </%def>
719 719
720 720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
721 721
722 722 <div id="diff-file-sticky" class="diffset-menu clearinner">
723 723 ## auto adjustable
724 724 <div class="sidebar__inner">
725 725 <div class="sidebar__bar">
726 726 <div class="pull-right">
727 727 <div class="btn-group">
728 728
729 729 ## DIFF OPTIONS via Select2
730 730 <div class="pull-left">
731 731 ${h.hidden('diff_menu')}
732 732 </div>
733 733
734 734 <a
735 735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
736 736 title="${h.tooltip(_('View side by side'))}"
737 737 href="${h.current_route_path(request, diffmode='sideside')}">
738 738 <span>${_('Side by Side')}</span>
739 739 </a>
740 740
741 741 <a
742 742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
743 743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
744 744 <span>${_('Unified')}</span>
745 745 </a>
746 746
747 747 % if range_diff_on is True:
748 748 <a
749 749 title="${_('Turn off: Show the diff as commit range')}"
750 750 class="btn btn-primary"
751 751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
752 752 <span>${_('Range Diff')}</span>
753 753 </a>
754 754 % elif range_diff_on is False:
755 755 <a
756 756 title="${_('Show the diff as commit range')}"
757 757 class="btn"
758 758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
759 759 <span>${_('Range Diff')}</span>
760 760 </a>
761 761 % endif
762 762 </div>
763 763 </div>
764 764 <div class="pull-left">
765 765 <div class="btn-group">
766 766 <div class="pull-left">
767 767 ${h.hidden('file_filter')}
768 768 </div>
769 769 <a
770 770 class="btn"
771 771 href="#"
772 772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
773 773 <a
774 774 class="btn"
775 775 href="#"
776 776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
777 777 </div>
778 778 </div>
779 779 </div>
780 780 <div class="fpath-placeholder">
781 781 <i class="icon-file-text"></i>
782 782 <strong class="fpath-placeholder-text">
783 783 Context file:
784 784 </strong>
785 785 </div>
786 786 <div class="sidebar_inner_shadow"></div>
787 787 </div>
788 788 </div>
789 789
790 790 % if diffset:
791 791
792 792 %if diffset.limited_diff:
793 793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
794 794 %else:
795 795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(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}%>
796 796 %endif
797 797 ## case on range-diff placeholder needs to be updated
798 798 % if range_diff_on is True:
799 799 <% file_placeholder = _('Disabled on range diff') %>
800 800 % endif
801 801
802 802 <script>
803 803
804 804 var feedFilesOptions = function (query, initialData) {
805 805 var data = {results: []};
806 806 var isQuery = typeof query.term !== 'undefined';
807 807
808 808 var section = _gettext('Changed files');
809 809 var filteredData = [];
810 810
811 811 //filter results
812 812 $.each(initialData.results, function (idx, value) {
813 813
814 814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
815 815 filteredData.push({
816 816 'id': this.id,
817 817 'text': this.text,
818 818 "ops": this.ops,
819 819 })
820 820 }
821 821
822 822 });
823 823
824 824 data.results = filteredData;
825 825
826 826 query.callback(data);
827 827 };
828 828
829 829 var formatFileResult = function(result, container, query, escapeMarkup) {
830 830 return function(data, escapeMarkup) {
831 831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
832 832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
833 833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
834 834 '<span class="pill" op="added">{0}</span>' +
835 835 '<span class="pill" op="deleted">{1}</span>' +
836 836 '</span>'
837 837 ;
838 838 var added = data['ops']['added'];
839 839 if (added === 0) {
840 840 // don't show +0
841 841 added = 0;
842 842 } else {
843 843 added = '+' + added;
844 844 }
845 845
846 846 var deleted = -1*data['ops']['deleted'];
847 847
848 848 tmpl += pill.format(added, deleted);
849 849 return container.format(tmpl);
850 850
851 851 }(result, escapeMarkup);
852 852 };
853 853
854 854 var preloadFileFilterData = {
855 855 results: [
856 856 % for filediff in diffset.files:
857 857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
858 858 text:"${filediff.patch['filename']}",
859 859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
860 860 % endfor
861 861 ]
862 862 };
863 863
864 864 $(document).ready(function () {
865 865
866 866 var fileFilter = $("#file_filter").select2({
867 867 'dropdownAutoWidth': true,
868 868 'width': 'auto',
869 869 'placeholder': "${file_placeholder}",
870 870 containerCssClass: "drop-menu",
871 871 dropdownCssClass: "drop-menu-dropdown",
872 872 data: preloadFileFilterData,
873 873 query: function(query) {
874 874 feedFilesOptions(query, preloadFileFilterData);
875 875 },
876 876 formatResult: formatFileResult
877 877 });
878 878
879 879 % if range_diff_on is True:
880 880 fileFilter.select2("enable", false);
881 881 % endif
882 882
883 883 $("#file_filter").on('click', function (e) {
884 884 e.preventDefault();
885 885 var selected = $('#file_filter').select2('data');
886 886 var idSelector = "#"+selected.id;
887 887 window.location.hash = idSelector;
888 888 // expand the container if we quick-select the field
889 889 $(idSelector).next().prop('checked', false);
890 890 updateSticky()
891 891 });
892 892
893 893 var contextPrefix = _gettext('Context file: ');
894 894 ## sticky sidebar
895 895 var sidebarElement = document.getElementById('diff-file-sticky');
896 896 sidebar = new StickySidebar(sidebarElement, {
897 897 topSpacing: 0,
898 898 bottomSpacing: 0,
899 899 innerWrapperSelector: '.sidebar__inner'
900 900 });
901 901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
902 902 // reset our file so it's not holding new value
903 903 $('.fpath-placeholder-text').html(contextPrefix)
904 904 });
905 905
906 906 updateSticky = function () {
907 907 sidebar.updateSticky();
908 908 Waypoint.refreshAll();
909 909 };
910 910
911 911 var animateText = $.debounce(100, function(fPath, anchorId) {
912 912 fPath = Select2.util.escapeMarkup(fPath);
913 913
914 914 // animate setting the text
915 915 var callback = function () {
916 916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
917 917 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
918 918 };
919 919 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
920 920 });
921 921
922 922 ## dynamic file waypoints
923 923 var setFPathInfo = function(fPath, anchorId){
924 924 animateText(fPath, anchorId)
925 925 };
926 926
927 927 var codeBlock = $('.filediff');
928 928 // forward waypoint
929 929 codeBlock.waypoint(
930 930 function(direction) {
931 931 if (direction === "down"){
932 932 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
933 933 }
934 934 }, {
935 935 offset: 70,
936 936 context: '.fpath-placeholder'
937 937 }
938 938 );
939 939
940 940 // backward waypoint
941 941 codeBlock.waypoint(
942 942 function(direction) {
943 943 if (direction === "up"){
944 944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
945 945 }
946 946 }, {
947 947 offset: function () {
948 948 return -this.element.clientHeight + 90
949 949 },
950 950 context: '.fpath-placeholder'
951 951 }
952 952 );
953 953
954 954 var preloadDiffMenuData = {
955 955 results: [
956 956 ## Wide diff mode
957 957 {
958 958 id: 1,
959 959 text: _gettext('Toggle Wide Mode diff'),
960 960 action: function () {
961 961 updateSticky();
962 962 Rhodecode.comments.toggleWideMode(this);
963 963 return null;
964 964 },
965 965 url: null,
966 966 },
967 967
968 968 ## Whitespace change
969 969 % if request.GET.get('ignorews', '') == '1':
970 970 {
971 971 id: 2,
972 972 text: _gettext('Show whitespace changes'),
973 973 action: function () {},
974 974 url: "${h.current_route_path(request, ignorews=0)|n}"
975 975 },
976 976 % else:
977 977 {
978 978 id: 2,
979 979 text: _gettext('Hide whitespace changes'),
980 980 action: function () {},
981 981 url: "${h.current_route_path(request, ignorews=1)|n}"
982 982 },
983 983 % endif
984 984
985 985 ## FULL CONTEXT
986 986 % if request.GET.get('fullcontext', '') == '1':
987 987 {
988 988 id: 3,
989 989 text: _gettext('Hide full context diff'),
990 990 action: function () {},
991 991 url: "${h.current_route_path(request, fullcontext=0)|n}"
992 992 },
993 993 % else:
994 994 {
995 995 id: 3,
996 996 text: _gettext('Show full context diff'),
997 997 action: function () {},
998 998 url: "${h.current_route_path(request, fullcontext=1)|n}"
999 999 },
1000 1000 % endif
1001 1001
1002 1002 ]
1003 1003 };
1004 1004
1005 1005 $("#diff_menu").select2({
1006 1006 minimumResultsForSearch: -1,
1007 1007 containerCssClass: "drop-menu",
1008 1008 dropdownCssClass: "drop-menu-dropdown",
1009 1009 dropdownAutoWidth: true,
1010 1010 data: preloadDiffMenuData,
1011 1011 placeholder: "${_('Diff Options')}",
1012 1012 });
1013 1013 $("#diff_menu").on('select2-selecting', function (e) {
1014 1014 e.choice.action();
1015 1015 if (e.choice.url !== null) {
1016 1016 window.location = e.choice.url
1017 1017 }
1018 1018 });
1019 1019
1020 1020 });
1021 1021
1022 1022 </script>
1023 1023 % endif
1024 1024
1025 1025 </%def>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now