##// END OF EJS Templates
events handling: minimize the amount of events attached per page
ergo -
r383:68fbf148 default
parent child Browse files
Show More
@@ -1,872 +1,872 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 26 import collections
27 27 import re
28 28 import difflib
29 29 import logging
30 30
31 31 from itertools import tee, imap
32 32
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.vcs.exceptions import VCSError
36 36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib.helpers import escape
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class OPS(object):
45 45 ADD = 'A'
46 46 MOD = 'M'
47 47 DEL = 'D'
48 48
49 49 def wrap_to_table(str_):
50 50 return '''<table class="code-difftable">
51 51 <tr class="line no-comment">
52 <td class="add-comment-line"><span class="add-comment-content"></span></td>
52 <td class="add-comment-line tooltip" title="Click to comment"><span class="add-comment-content"></span></td>
53 53 <td class="lineno new"></td>
54 54 <td class="code no-comment"><pre>%s</pre></td>
55 55 </tr>
56 56 </table>''' % str_
57 57
58 58
59 59 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
60 60 show_full_diff=False, ignore_whitespace=True, line_context=3,
61 61 enable_comments=False):
62 62 """
63 63 returns a wrapped diff into a table, checks for cut_off_limit for file and
64 64 whole diff and presents proper message
65 65 """
66 66
67 67 if filenode_old is None:
68 68 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
69 69
70 70 if filenode_old.is_binary or filenode_new.is_binary:
71 71 diff = wrap_to_table(_('Binary file'))
72 72 stats = None
73 73 size = 0
74 74 data = None
75 75
76 76 elif diff_limit != -1 and (diff_limit is None or
77 77 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
78 78
79 79 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
80 80 ignore_whitespace=ignore_whitespace,
81 81 context=line_context)
82 82 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff', diff_limit=diff_limit,
83 83 file_limit=file_limit, show_full_diff=show_full_diff)
84 84 _parsed = diff_processor.prepare()
85 85
86 86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 87 stats = _parsed[0]['stats'] if _parsed else None
88 88 size = len(diff or '')
89 89 data = _parsed[0] if _parsed else None
90 90 else:
91 91 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
92 92 'diff menu to display this diff'))
93 93 stats = None
94 94 size = 0
95 95 data = None
96 96 if not diff:
97 97 submodules = filter(lambda o: isinstance(o, SubModuleNode),
98 98 [filenode_new, filenode_old])
99 99 if submodules:
100 100 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
101 101 else:
102 102 diff = wrap_to_table(_('No changes detected'))
103 103
104 104 cs1 = filenode_old.commit.raw_id
105 105 cs2 = filenode_new.commit.raw_id
106 106
107 107 return size, cs1, cs2, diff, stats, data
108 108
109 109
110 110 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
111 111 """
112 112 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
113 113
114 114 :param ignore_whitespace: ignore whitespaces in diff
115 115 """
116 116 # make sure we pass in default context
117 117 context = context or 3
118 118 submodules = filter(lambda o: isinstance(o, SubModuleNode),
119 119 [filenode_new, filenode_old])
120 120 if submodules:
121 121 return ''
122 122
123 123 for filenode in (filenode_old, filenode_new):
124 124 if not isinstance(filenode, FileNode):
125 125 raise VCSError(
126 126 "Given object should be FileNode object, not %s"
127 127 % filenode.__class__)
128 128
129 129 repo = filenode_new.commit.repository
130 130 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
131 131 new_commit = filenode_new.commit
132 132
133 133 vcs_gitdiff = repo.get_diff(
134 134 old_commit, new_commit, filenode_new.path,
135 135 ignore_whitespace, context, path1=filenode_old.path)
136 136 return vcs_gitdiff
137 137
138 138 NEW_FILENODE = 1
139 139 DEL_FILENODE = 2
140 140 MOD_FILENODE = 3
141 141 RENAMED_FILENODE = 4
142 142 COPIED_FILENODE = 5
143 143 CHMOD_FILENODE = 6
144 144 BIN_FILENODE = 7
145 145
146 146
147 147 class LimitedDiffContainer(object):
148 148
149 149 def __init__(self, diff_limit, cur_diff_size, diff):
150 150 self.diff = diff
151 151 self.diff_limit = diff_limit
152 152 self.cur_diff_size = cur_diff_size
153 153
154 154 def __getitem__(self, key):
155 155 return self.diff.__getitem__(key)
156 156
157 157 def __iter__(self):
158 158 for l in self.diff:
159 159 yield l
160 160
161 161
162 162 class Action(object):
163 163 """
164 164 Contains constants for the action value of the lines in a parsed diff.
165 165 """
166 166
167 167 ADD = 'add'
168 168 DELETE = 'del'
169 169 UNMODIFIED = 'unmod'
170 170
171 171 CONTEXT = 'context'
172 172
173 173
174 174 class DiffProcessor(object):
175 175 """
176 176 Give it a unified or git diff and it returns a list of the files that were
177 177 mentioned in the diff together with a dict of meta information that
178 178 can be used to render it in a HTML template.
179 179
180 180 .. note:: Unicode handling
181 181
182 182 The original diffs are a byte sequence and can contain filenames
183 183 in mixed encodings. This class generally returns `unicode` objects
184 184 since the result is intended for presentation to the user.
185 185
186 186 """
187 187 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
188 188 _newline_marker = re.compile(r'^\\ No newline at end of file')
189 189
190 190 # used for inline highlighter word split
191 191 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
192 192
193 193 def __init__(self, diff, format='gitdiff', diff_limit=None, file_limit=None, show_full_diff=True):
194 194 """
195 195 :param diff: A `Diff` object representing a diff from a vcs backend
196 196 :param format: format of diff passed, `udiff` or `gitdiff`
197 197 :param diff_limit: define the size of diff that is considered "big"
198 198 based on that parameter cut off will be triggered, set to None
199 199 to show full diff
200 200 """
201 201 self._diff = diff
202 202 self._format = format
203 203 self.adds = 0
204 204 self.removes = 0
205 205 # calculate diff size
206 206 self.diff_limit = diff_limit
207 207 self.file_limit = file_limit
208 208 self.show_full_diff = show_full_diff
209 209 self.cur_diff_size = 0
210 210 self.parsed = False
211 211 self.parsed_diff = []
212 212
213 213 if format == 'gitdiff':
214 214 self.differ = self._highlight_line_difflib
215 215 self._parser = self._parse_gitdiff
216 216 else:
217 217 self.differ = self._highlight_line_udiff
218 218 self._parser = self._parse_udiff
219 219
220 220 def _copy_iterator(self):
221 221 """
222 222 make a fresh copy of generator, we should not iterate thru
223 223 an original as it's needed for repeating operations on
224 224 this instance of DiffProcessor
225 225 """
226 226 self.__udiff, iterator_copy = tee(self.__udiff)
227 227 return iterator_copy
228 228
229 229 def _escaper(self, string):
230 230 """
231 231 Escaper for diff escapes special chars and checks the diff limit
232 232
233 233 :param string:
234 234 """
235 235
236 236 self.cur_diff_size += len(string)
237 237
238 238 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
239 239 raise DiffLimitExceeded('Diff Limit Exceeded')
240 240
241 241 return safe_unicode(string)\
242 242 .replace('&', '&amp;')\
243 243 .replace('<', '&lt;')\
244 244 .replace('>', '&gt;')
245 245
246 246 def _line_counter(self, l):
247 247 """
248 248 Checks each line and bumps total adds/removes for this diff
249 249
250 250 :param l:
251 251 """
252 252 if l.startswith('+') and not l.startswith('+++'):
253 253 self.adds += 1
254 254 elif l.startswith('-') and not l.startswith('---'):
255 255 self.removes += 1
256 256 return safe_unicode(l)
257 257
258 258 def _highlight_line_difflib(self, line, next_):
259 259 """
260 260 Highlight inline changes in both lines.
261 261 """
262 262
263 263 if line['action'] == Action.DELETE:
264 264 old, new = line, next_
265 265 else:
266 266 old, new = next_, line
267 267
268 268 oldwords = self._token_re.split(old['line'])
269 269 newwords = self._token_re.split(new['line'])
270 270 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
271 271
272 272 oldfragments, newfragments = [], []
273 273 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
274 274 oldfrag = ''.join(oldwords[i1:i2])
275 275 newfrag = ''.join(newwords[j1:j2])
276 276 if tag != 'equal':
277 277 if oldfrag:
278 278 oldfrag = '<del>%s</del>' % oldfrag
279 279 if newfrag:
280 280 newfrag = '<ins>%s</ins>' % newfrag
281 281 oldfragments.append(oldfrag)
282 282 newfragments.append(newfrag)
283 283
284 284 old['line'] = "".join(oldfragments)
285 285 new['line'] = "".join(newfragments)
286 286
287 287 def _highlight_line_udiff(self, line, next_):
288 288 """
289 289 Highlight inline changes in both lines.
290 290 """
291 291 start = 0
292 292 limit = min(len(line['line']), len(next_['line']))
293 293 while start < limit and line['line'][start] == next_['line'][start]:
294 294 start += 1
295 295 end = -1
296 296 limit -= start
297 297 while -end <= limit and line['line'][end] == next_['line'][end]:
298 298 end -= 1
299 299 end += 1
300 300 if start or end:
301 301 def do(l):
302 302 last = end + len(l['line'])
303 303 if l['action'] == Action.ADD:
304 304 tag = 'ins'
305 305 else:
306 306 tag = 'del'
307 307 l['line'] = '%s<%s>%s</%s>%s' % (
308 308 l['line'][:start],
309 309 tag,
310 310 l['line'][start:last],
311 311 tag,
312 312 l['line'][last:]
313 313 )
314 314 do(line)
315 315 do(next_)
316 316
317 317 def _clean_line(self, line, command):
318 318 if command in ['+', '-', ' ']:
319 319 # only modify the line if it's actually a diff thing
320 320 line = line[1:]
321 321 return line
322 322
323 323 def _parse_gitdiff(self, inline_diff=True):
324 324 _files = []
325 325 diff_container = lambda arg: arg
326 326
327 327 for chunk in self._diff.chunks():
328 328 head = chunk.header
329 329
330 330 diff = imap(self._escaper, chunk.diff.splitlines(1))
331 331 raw_diff = chunk.raw
332 332 limited_diff = False
333 333 exceeds_limit = False
334 334
335 335 op = None
336 336 stats = {
337 337 'added': 0,
338 338 'deleted': 0,
339 339 'binary': False,
340 340 'ops': {},
341 341 }
342 342
343 343 if head['deleted_file_mode']:
344 344 op = OPS.DEL
345 345 stats['binary'] = True
346 346 stats['ops'][DEL_FILENODE] = 'deleted file'
347 347
348 348 elif head['new_file_mode']:
349 349 op = OPS.ADD
350 350 stats['binary'] = True
351 351 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
352 352 else: # modify operation, can be copy, rename or chmod
353 353
354 354 # CHMOD
355 355 if head['new_mode'] and head['old_mode']:
356 356 op = OPS.MOD
357 357 stats['binary'] = True
358 358 stats['ops'][CHMOD_FILENODE] = (
359 359 'modified file chmod %s => %s' % (
360 360 head['old_mode'], head['new_mode']))
361 361 # RENAME
362 362 if head['rename_from'] != head['rename_to']:
363 363 op = OPS.MOD
364 364 stats['binary'] = True
365 365 stats['ops'][RENAMED_FILENODE] = (
366 366 'file renamed from %s to %s' % (
367 367 head['rename_from'], head['rename_to']))
368 368 # COPY
369 369 if head.get('copy_from') and head.get('copy_to'):
370 370 op = OPS.MOD
371 371 stats['binary'] = True
372 372 stats['ops'][COPIED_FILENODE] = (
373 373 'file copied from %s to %s' % (
374 374 head['copy_from'], head['copy_to']))
375 375
376 376 # If our new parsed headers didn't match anything fallback to
377 377 # old style detection
378 378 if op is None:
379 379 if not head['a_file'] and head['b_file']:
380 380 op = OPS.ADD
381 381 stats['binary'] = True
382 382 stats['ops'][NEW_FILENODE] = 'new file'
383 383
384 384 elif head['a_file'] and not head['b_file']:
385 385 op = OPS.DEL
386 386 stats['binary'] = True
387 387 stats['ops'][DEL_FILENODE] = 'deleted file'
388 388
389 389 # it's not ADD not DELETE
390 390 if op is None:
391 391 op = OPS.MOD
392 392 stats['binary'] = True
393 393 stats['ops'][MOD_FILENODE] = 'modified file'
394 394
395 395 # a real non-binary diff
396 396 if head['a_file'] or head['b_file']:
397 397 try:
398 398 raw_diff, chunks, _stats = self._parse_lines(diff)
399 399 stats['binary'] = False
400 400 stats['added'] = _stats[0]
401 401 stats['deleted'] = _stats[1]
402 402 # explicit mark that it's a modified file
403 403 if op == OPS.MOD:
404 404 stats['ops'][MOD_FILENODE] = 'modified file'
405 405 exceeds_limit = len(raw_diff) > self.file_limit
406 406
407 407 # changed from _escaper function so we validate size of
408 408 # each file instead of the whole diff
409 409 # diff will hide big files but still show small ones
410 410 # from my tests, big files are fairly safe to be parsed
411 411 # but the browser is the bottleneck
412 412 if not self.show_full_diff and exceeds_limit:
413 413 raise DiffLimitExceeded('File Limit Exceeded')
414 414
415 415 except DiffLimitExceeded:
416 416 diff_container = lambda _diff: \
417 417 LimitedDiffContainer(
418 418 self.diff_limit, self.cur_diff_size, _diff)
419 419
420 420 exceeds_limit = len(raw_diff) > self.file_limit
421 421 limited_diff = True
422 422 chunks = []
423 423
424 424 else: # GIT format binary patch, or possibly empty diff
425 425 if head['bin_patch']:
426 426 # we have operation already extracted, but we mark simply
427 427 # it's a diff we wont show for binary files
428 428 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
429 429 chunks = []
430 430
431 431 if chunks and not self.show_full_diff and op == OPS.DEL:
432 432 # if not full diff mode show deleted file contents
433 433 # TODO: anderson: if the view is not too big, there is no way
434 434 # to see the content of the file
435 435 chunks = []
436 436
437 437 chunks.insert(0, [{
438 438 'old_lineno': '',
439 439 'new_lineno': '',
440 440 'action': Action.CONTEXT,
441 441 'line': msg,
442 442 } for _op, msg in stats['ops'].iteritems()
443 443 if _op not in [MOD_FILENODE]])
444 444
445 445 _files.append({
446 446 'filename': safe_unicode(head['b_path']),
447 447 'old_revision': head['a_blob_id'],
448 448 'new_revision': head['b_blob_id'],
449 449 'chunks': chunks,
450 450 'raw_diff': safe_unicode(raw_diff),
451 451 'operation': op,
452 452 'stats': stats,
453 453 'exceeds_limit': exceeds_limit,
454 454 'is_limited_diff': limited_diff,
455 455 })
456 456
457 457 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
458 458 OPS.DEL: 2}.get(info['operation'])
459 459
460 460 if not inline_diff:
461 461 return diff_container(sorted(_files, key=sorter))
462 462
463 463 # highlight inline changes
464 464 for diff_data in _files:
465 465 for chunk in diff_data['chunks']:
466 466 lineiter = iter(chunk)
467 467 try:
468 468 while 1:
469 469 line = lineiter.next()
470 470 if line['action'] not in (
471 471 Action.UNMODIFIED, Action.CONTEXT):
472 472 nextline = lineiter.next()
473 473 if nextline['action'] in ['unmod', 'context'] or \
474 474 nextline['action'] == line['action']:
475 475 continue
476 476 self.differ(line, nextline)
477 477 except StopIteration:
478 478 pass
479 479
480 480 return diff_container(sorted(_files, key=sorter))
481 481
482 482 def _parse_udiff(self, inline_diff=True):
483 483 raise NotImplementedError()
484 484
485 485 def _parse_lines(self, diff):
486 486 """
487 487 Parse the diff an return data for the template.
488 488 """
489 489
490 490 lineiter = iter(diff)
491 491 stats = [0, 0]
492 492 chunks = []
493 493 raw_diff = []
494 494
495 495 try:
496 496 line = lineiter.next()
497 497
498 498 while line:
499 499 raw_diff.append(line)
500 500 lines = []
501 501 chunks.append(lines)
502 502
503 503 match = self._chunk_re.match(line)
504 504
505 505 if not match:
506 506 break
507 507
508 508 gr = match.groups()
509 509 (old_line, old_end,
510 510 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
511 511 old_line -= 1
512 512 new_line -= 1
513 513
514 514 context = len(gr) == 5
515 515 old_end += old_line
516 516 new_end += new_line
517 517
518 518 if context:
519 519 # skip context only if it's first line
520 520 if int(gr[0]) > 1:
521 521 lines.append({
522 522 'old_lineno': '...',
523 523 'new_lineno': '...',
524 524 'action': Action.CONTEXT,
525 525 'line': line,
526 526 })
527 527
528 528 line = lineiter.next()
529 529
530 530 while old_line < old_end or new_line < new_end:
531 531 command = ' '
532 532 if line:
533 533 command = line[0]
534 534
535 535 affects_old = affects_new = False
536 536
537 537 # ignore those if we don't expect them
538 538 if command in '#@':
539 539 continue
540 540 elif command == '+':
541 541 affects_new = True
542 542 action = Action.ADD
543 543 stats[0] += 1
544 544 elif command == '-':
545 545 affects_old = True
546 546 action = Action.DELETE
547 547 stats[1] += 1
548 548 else:
549 549 affects_old = affects_new = True
550 550 action = Action.UNMODIFIED
551 551
552 552 if not self._newline_marker.match(line):
553 553 old_line += affects_old
554 554 new_line += affects_new
555 555 lines.append({
556 556 'old_lineno': affects_old and old_line or '',
557 557 'new_lineno': affects_new and new_line or '',
558 558 'action': action,
559 559 'line': self._clean_line(line, command)
560 560 })
561 561 raw_diff.append(line)
562 562
563 563 line = lineiter.next()
564 564
565 565 if self._newline_marker.match(line):
566 566 # we need to append to lines, since this is not
567 567 # counted in the line specs of diff
568 568 lines.append({
569 569 'old_lineno': '...',
570 570 'new_lineno': '...',
571 571 'action': Action.CONTEXT,
572 572 'line': self._clean_line(line, command)
573 573 })
574 574
575 575 except StopIteration:
576 576 pass
577 577 return ''.join(raw_diff), chunks, stats
578 578
579 579 def _safe_id(self, idstring):
580 580 """Make a string safe for including in an id attribute.
581 581
582 582 The HTML spec says that id attributes 'must begin with
583 583 a letter ([A-Za-z]) and may be followed by any number
584 584 of letters, digits ([0-9]), hyphens ("-"), underscores
585 585 ("_"), colons (":"), and periods (".")'. These regexps
586 586 are slightly over-zealous, in that they remove colons
587 587 and periods unnecessarily.
588 588
589 589 Whitespace is transformed into underscores, and then
590 590 anything which is not a hyphen or a character that
591 591 matches \w (alphanumerics and underscore) is removed.
592 592
593 593 """
594 594 # Transform all whitespace to underscore
595 595 idstring = re.sub(r'\s', "_", '%s' % idstring)
596 596 # Remove everything that is not a hyphen or a member of \w
597 597 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
598 598 return idstring
599 599
600 600 def prepare(self, inline_diff=True):
601 601 """
602 602 Prepare the passed udiff for HTML rendering.
603 603
604 604 :return: A list of dicts with diff information.
605 605 """
606 606 parsed = self._parser(inline_diff=inline_diff)
607 607 self.parsed = True
608 608 self.parsed_diff = parsed
609 609 return parsed
610 610
611 611 def as_raw(self, diff_lines=None):
612 612 """
613 613 Returns raw diff as a byte string
614 614 """
615 615 return self._diff.raw
616 616
617 617 def as_html(self, table_class='code-difftable', line_class='line',
618 618 old_lineno_class='lineno old', new_lineno_class='lineno new',
619 619 code_class='code', enable_comments=False, parsed_lines=None):
620 620 """
621 621 Return given diff as html table with customized css classes
622 622 """
623 623 def _link_to_if(condition, label, url):
624 624 """
625 625 Generates a link if condition is meet or just the label if not.
626 626 """
627 627
628 628 if condition:
629 return '''<a href="%(url)s">%(label)s</a>''' % {
629 return '''<a href="%(url)s" class="tooltip" title="Click to select line">%(label)s</a>''' % {
630 630 'url': url,
631 631 'label': label
632 632 }
633 633 else:
634 634 return label
635 635 if not self.parsed:
636 636 self.prepare()
637 637
638 638 diff_lines = self.parsed_diff
639 639 if parsed_lines:
640 640 diff_lines = parsed_lines
641 641
642 642 _html_empty = True
643 643 _html = []
644 644 _html.append('''<table class="%(table_class)s">\n''' % {
645 645 'table_class': table_class
646 646 })
647 647
648 648 for diff in diff_lines:
649 649 for line in diff['chunks']:
650 650 _html_empty = False
651 651 for change in line:
652 652 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
653 653 'lc': line_class,
654 654 'action': change['action']
655 655 })
656 656 anchor_old_id = ''
657 657 anchor_new_id = ''
658 658 anchor_old = "%(filename)s_o%(oldline_no)s" % {
659 659 'filename': self._safe_id(diff['filename']),
660 660 'oldline_no': change['old_lineno']
661 661 }
662 662 anchor_new = "%(filename)s_n%(oldline_no)s" % {
663 663 'filename': self._safe_id(diff['filename']),
664 664 'oldline_no': change['new_lineno']
665 665 }
666 666 cond_old = (change['old_lineno'] != '...' and
667 667 change['old_lineno'])
668 668 cond_new = (change['new_lineno'] != '...' and
669 669 change['new_lineno'])
670 670 if cond_old:
671 671 anchor_old_id = 'id="%s"' % anchor_old
672 672 if cond_new:
673 673 anchor_new_id = 'id="%s"' % anchor_new
674 674
675 675 if change['action'] != Action.CONTEXT:
676 676 anchor_link = True
677 677 else:
678 678 anchor_link = False
679 679
680 680 ###########################################################
681 681 # COMMENT ICON
682 682 ###########################################################
683 683 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
684 684
685 685 if enable_comments and change['action'] != Action.CONTEXT:
686 686 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
687 687
688 688 _html.append('''</span></td>\n''')
689 689
690 690 ###########################################################
691 691 # OLD LINE NUMBER
692 692 ###########################################################
693 693 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
694 694 'a_id': anchor_old_id,
695 695 'olc': old_lineno_class
696 696 })
697 697
698 698 _html.append('''%(link)s''' % {
699 699 'link': _link_to_if(anchor_link, change['old_lineno'],
700 700 '#%s' % anchor_old)
701 701 })
702 702 _html.append('''</td>\n''')
703 703 ###########################################################
704 704 # NEW LINE NUMBER
705 705 ###########################################################
706 706
707 707 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
708 708 'a_id': anchor_new_id,
709 709 'nlc': new_lineno_class
710 710 })
711 711
712 712 _html.append('''%(link)s''' % {
713 713 'link': _link_to_if(anchor_link, change['new_lineno'],
714 714 '#%s' % anchor_new)
715 715 })
716 716 _html.append('''</td>\n''')
717 717 ###########################################################
718 718 # CODE
719 719 ###########################################################
720 720 code_classes = [code_class]
721 721 if (not enable_comments or
722 722 change['action'] == Action.CONTEXT):
723 723 code_classes.append('no-comment')
724 724 _html.append('\t<td class="%s">' % ' '.join(code_classes))
725 725 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
726 726 'code': change['line']
727 727 })
728 728
729 729 _html.append('''\t</td>''')
730 730 _html.append('''\n</tr>\n''')
731 731 _html.append('''</table>''')
732 732 if _html_empty:
733 733 return None
734 734 return ''.join(_html)
735 735
736 736 def stat(self):
737 737 """
738 738 Returns tuple of added, and removed lines for this instance
739 739 """
740 740 return self.adds, self.removes
741 741
742 742 def get_context_of_line(
743 743 self, path, diff_line=None, context_before=3, context_after=3):
744 744 """
745 745 Returns the context lines for the specified diff line.
746 746
747 747 :type diff_line: :class:`DiffLineNumber`
748 748 """
749 749 assert self.parsed, "DiffProcessor is not initialized."
750 750
751 751 if None not in diff_line:
752 752 raise ValueError(
753 753 "Cannot specify both line numbers: {}".format(diff_line))
754 754
755 755 file_diff = self._get_file_diff(path)
756 756 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
757 757
758 758 first_line_to_include = max(idx - context_before, 0)
759 759 first_line_after_context = idx + context_after + 1
760 760 context_lines = chunk[first_line_to_include:first_line_after_context]
761 761
762 762 line_contents = [
763 763 _context_line(line) for line in context_lines
764 764 if _is_diff_content(line)]
765 765 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
766 766 # Once they are fixed, we can drop this line here.
767 767 if line_contents:
768 768 line_contents[-1] = (
769 769 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
770 770 return line_contents
771 771
772 772 def find_context(self, path, context, offset=0):
773 773 """
774 774 Finds the given `context` inside of the diff.
775 775
776 776 Use the parameter `offset` to specify which offset the target line has
777 777 inside of the given `context`. This way the correct diff line will be
778 778 returned.
779 779
780 780 :param offset: Shall be used to specify the offset of the main line
781 781 within the given `context`.
782 782 """
783 783 if offset < 0 or offset >= len(context):
784 784 raise ValueError(
785 785 "Only positive values up to the length of the context "
786 786 "minus one are allowed.")
787 787
788 788 matches = []
789 789 file_diff = self._get_file_diff(path)
790 790
791 791 for chunk in file_diff['chunks']:
792 792 context_iter = iter(context)
793 793 for line_idx, line in enumerate(chunk):
794 794 try:
795 795 if _context_line(line) == context_iter.next():
796 796 continue
797 797 except StopIteration:
798 798 matches.append((line_idx, chunk))
799 799 context_iter = iter(context)
800 800
801 801 # Increment position and triger StopIteration
802 802 # if we had a match at the end
803 803 line_idx += 1
804 804 try:
805 805 context_iter.next()
806 806 except StopIteration:
807 807 matches.append((line_idx, chunk))
808 808
809 809 effective_offset = len(context) - offset
810 810 found_at_diff_lines = [
811 811 _line_to_diff_line_number(chunk[idx - effective_offset])
812 812 for idx, chunk in matches]
813 813
814 814 return found_at_diff_lines
815 815
816 816 def _get_file_diff(self, path):
817 817 for file_diff in self.parsed_diff:
818 818 if file_diff['filename'] == path:
819 819 break
820 820 else:
821 821 raise FileNotInDiffException("File {} not in diff".format(path))
822 822 return file_diff
823 823
824 824 def _find_chunk_line_index(self, file_diff, diff_line):
825 825 for chunk in file_diff['chunks']:
826 826 for idx, line in enumerate(chunk):
827 827 if line['old_lineno'] == diff_line.old:
828 828 return chunk, idx
829 829 if line['new_lineno'] == diff_line.new:
830 830 return chunk, idx
831 831 raise LineNotInDiffException(
832 832 "The line {} is not part of the diff.".format(diff_line))
833 833
834 834
835 835 def _is_diff_content(line):
836 836 return line['action'] in (
837 837 Action.UNMODIFIED, Action.ADD, Action.DELETE)
838 838
839 839
840 840 def _context_line(line):
841 841 return (line['action'], line['line'])
842 842
843 843
844 844 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
845 845
846 846
847 847 def _line_to_diff_line_number(line):
848 848 new_line_no = line['new_lineno'] or None
849 849 old_line_no = line['old_lineno'] or None
850 850 return DiffLineNumber(old=old_line_no, new=new_line_no)
851 851
852 852
853 853 class FileNotInDiffException(Exception):
854 854 """
855 855 Raised when the context for a missing file is requested.
856 856
857 857 If you request the context for a line in a file which is not part of the
858 858 given diff, then this exception is raised.
859 859 """
860 860
861 861
862 862 class LineNotInDiffException(Exception):
863 863 """
864 864 Raised when the context for a missing line is requested.
865 865
866 866 If you request the context for a line in a file and this line is not
867 867 part of the given diff, then this exception is raised.
868 868 """
869 869
870 870
871 871 class DiffLimitExceeded(Exception):
872 872 pass
@@ -1,399 +1,386 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('compare_url', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 if (!container.hasClass('loaded')) {
80 80 $.ajax({url: url})
81 81 .complete(function (data) {
82 82 var responseJSON = data.responseJSON;
83 83 container.addClass('loaded');
84 84 container.html(responseJSON.size);
85 85 callback(responseJSON.code_stats)
86 86 })
87 87 .fail(function (data) {
88 88 console.log('failed to load repo stats');
89 89 });
90 90 }
91 91
92 92 };
93 93
94 94 var showRepoStats = function(target, data){
95 95 var container = $('#' + target);
96 96
97 97 if (container.hasClass('loaded')) {
98 98 return
99 99 }
100 100
101 101 var total = 0;
102 102 var no_data = true;
103 103 var tbl = document.createElement('table');
104 104 tbl.setAttribute('class', 'trending_language_tbl');
105 105
106 106 $.each(data, function(key, val){
107 107 total += val.count;
108 108 });
109 109
110 110 var sortedStats = [];
111 111 for (var obj in data){
112 112 sortedStats.push([obj, data[obj]])
113 113 }
114 114 var sortedData = sortedStats.sort(function (a, b) {
115 115 return b[1].count - a[1].count
116 116 });
117 117 var cnt = 0;
118 118 $.each(sortedData, function(idx, val){
119 119 cnt += 1;
120 120 no_data = false;
121 121
122 122 var hide = cnt > 2;
123 123 var tr = document.createElement('tr');
124 124 if (hide) {
125 125 tr.setAttribute('style', 'display:none');
126 126 tr.setAttribute('class', 'stats_hidden');
127 127 }
128 128
129 129 var key = val[0];
130 130 var obj = {"desc": val[1].desc, "count": val[1].count};
131 131
132 132 var percentage = Math.round((obj.count / total * 100), 2);
133 133
134 134 var td1 = document.createElement('td');
135 135 td1.width = 300;
136 136 var trending_language_label = document.createElement('div');
137 137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 138 td1.appendChild(trending_language_label);
139 139
140 140 var td2 = document.createElement('td');
141 141 var trending_language = document.createElement('div');
142 142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143 143
144 144 trending_language.title = key + " " + nr_files;
145 145
146 146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148 148
149 149 trending_language.setAttribute("class", 'trending_language');
150 150 $('b', trending_language)[0].style.width = percentage + "%";
151 151 td2.appendChild(trending_language);
152 152
153 153 tr.appendChild(td1);
154 154 tr.appendChild(td2);
155 155 tbl.appendChild(tr);
156 156 if (cnt == 3) {
157 157 var show_more = document.createElement('tr');
158 158 var td = document.createElement('td');
159 159 lnk = document.createElement('a');
160 160
161 161 lnk.href = '#';
162 162 lnk.innerHTML = _ngettext('Show more');
163 163 lnk.id = 'code_stats_show_more';
164 164 td.appendChild(lnk);
165 165
166 166 show_more.appendChild(td);
167 167 show_more.appendChild(document.createElement('td'));
168 168 tbl.appendChild(show_more);
169 169 }
170 170 });
171 171
172 172 $(container).html(tbl);
173 173 $(container).addClass('loaded');
174 174
175 175 $('#code_stats_show_more').on('click', function (e) {
176 176 e.preventDefault();
177 177 $('.stats_hidden').each(function (idx) {
178 178 $(this).css("display", "");
179 179 });
180 180 $('#code_stats_show_more').hide();
181 181 });
182 182
183 183 };
184 184
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207 // Formatting values in a Select2 dropdown of commit references
208 208 var formatSelect2SelectionRefs = function(commit_ref){
209 209 var tmpl = '';
210 210 if (!commit_ref.text || commit_ref.type === 'sha'){
211 211 return commit_ref.text;
212 212 }
213 213 if (commit_ref.type === 'branch'){
214 214 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
215 215 } else if (commit_ref.type === 'tag'){
216 216 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
217 217 } else if (commit_ref.type === 'book'){
218 218 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
219 219 }
220 220 return tmpl.concat(commit_ref.text);
221 221 };
222 222
223 223 // takes a given html element and scrolls it down offset pixels
224 224 function offsetScroll(element, offset){
225 225 setTimeout(function(){
226 226 console.log(element);
227 227 var location = element.offset().top;
228 228 // some browsers use body, some use html
229 229 $('html, body').animate({ scrollTop: (location - offset) });
230 230 }, 100);
231 231 }
232 232
233 233 /**
234 234 * global hooks after DOM is loaded
235 235 */
236 236 $(document).ready(function() {
237 237 firefoxAnchorFix();
238 238
239 239 $('.navigation a.menulink').on('click', function(e){
240 240 var menuitem = $(this).parent('li');
241 241 if (menuitem.hasClass('open')) {
242 242 menuitem.removeClass('open');
243 243 } else {
244 244 menuitem.addClass('open');
245 245 $(document).on('click', function(event) {
246 246 if (!$(event.target).closest(menuitem).length) {
247 247 menuitem.removeClass('open');
248 248 }
249 249 });
250 250 }
251 251 });
252 // Add tooltips
253 $('tr.line .lineno a').attr("title","Click to select line").addClass('tooltip');
254 $('tr.line .add-comment-line a').attr("title","Click to comment").addClass('tooltip');
252 $('.compare_view_files').on(
253 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
254 if (event.type === "mouseenter") {
255 $(this).parents('tr.line').addClass('hover');
256 } else {
257 $(this).parents('tr.line').removeClass('hover');
258 }
259 });
255 260
256 // Set colors and styles
257 $('tr.line .lineno a').hover(
258 function(){
259 $(this).parents('tr.line').addClass('hover');
260 }, function(){
261 $(this).parents('tr.line').removeClass('hover');
262 }
263 );
264
265 $('tr.line .lineno a').click(
266 function(){
261 $('.compare_view_files').on(
262 'click', 'tr.line .lineno a',function(event) {
267 263 if ($(this).text() != ""){
268 264 $('tr.line').removeClass('selected');
269 265 $(this).parents("tr.line").addClass('selected');
270 266
271 267 // Replace URL without jumping to it if browser supports.
272 268 // Default otherwise
273 269 if (history.pushState) {
274 var new_location = location.href
270 var new_location = location.href;
275 271 if (location.hash){
276 272 new_location = new_location.replace(location.hash, "");
277 273 }
278 274
279 275 // Make new anchor url
280 276 var new_location = new_location+$(this).attr('href');
281 277 history.pushState(true, document.title, new_location);
282 278
283 279 return false;
284 280 }
285 281 }
286 }
287 );
282 });
288 283
289 $('tr.line .add-comment-line a').hover(
290 function(){
291 $(this).parents('tr.line').addClass('commenting');
292 }, function(){
293 $(this).parents('tr.line').removeClass('commenting');
294 }
295 );
296
297 $('tr.line .add-comment-line a').on('click', function(e){
298 var tr = $(e.currentTarget).parents('tr.line')[0];
299 injectInlineForm(tr);
300 return false;
301 });
302
284 $('.compare_view_files').on(
285 'click', 'tr.line .add-comment-line a',function(event) {
286 var tr = $(event.currentTarget).parents('tr.line')[0];
287 injectInlineForm(tr);
288 return false;
289 });
303 290
304 291 $('.collapse_file').on('click', function(e) {
305 292 e.stopPropagation();
306 293 if ($(e.target).is('a')) { return; }
307 294 var node = $(e.delegateTarget).first();
308 295 var icon = $($(node.children().first()).children().first());
309 296 var id = node.attr('fid');
310 297 var target = $('#'+id);
311 298 var tr = $('#tr_'+id);
312 299 var diff = $('#diff_'+id);
313 300 if(node.hasClass('expand_file')){
314 301 node.removeClass('expand_file');
315 302 icon.removeClass('expand_file_icon');
316 303 node.addClass('collapse_file');
317 304 icon.addClass('collapse_file_icon');
318 305 diff.show();
319 306 tr.show();
320 307 target.show();
321 308 } else {
322 309 node.removeClass('collapse_file');
323 310 icon.removeClass('collapse_file_icon');
324 311 node.addClass('expand_file');
325 312 icon.addClass('expand_file_icon');
326 313 diff.hide();
327 314 tr.hide();
328 315 target.hide();
329 316 }
330 317 });
331 318
332 319 $('#expand_all_files').click(function() {
333 320 $('.expand_file').each(function() {
334 321 var node = $(this);
335 322 var icon = $($(node.children().first()).children().first());
336 323 var id = $(this).attr('fid');
337 324 var target = $('#'+id);
338 325 var tr = $('#tr_'+id);
339 326 var diff = $('#diff_'+id);
340 327 node.removeClass('expand_file');
341 328 icon.removeClass('expand_file_icon');
342 329 node.addClass('collapse_file');
343 330 icon.addClass('collapse_file_icon');
344 331 diff.show();
345 332 tr.show();
346 333 target.show();
347 334 });
348 335 });
349 336
350 337 $('#collapse_all_files').click(function() {
351 338 $('.collapse_file').each(function() {
352 339 var node = $(this);
353 340 var icon = $($(node.children().first()).children().first());
354 341 var id = $(this).attr('fid');
355 342 var target = $('#'+id);
356 343 var tr = $('#tr_'+id);
357 344 var diff = $('#diff_'+id);
358 345 node.removeClass('collapse_file');
359 346 icon.removeClass('collapse_file_icon');
360 347 node.addClass('expand_file');
361 348 icon.addClass('expand_file_icon');
362 349 diff.hide();
363 350 tr.hide();
364 351 target.hide();
365 352 });
366 353 });
367 354
368 355 // Mouse over behavior for comments and line selection
369 356
370 357 // Select the line that comes from the url anchor
371 358 // At the time of development, Chrome didn't seem to support jquery's :target
372 359 // element, so I had to scroll manually
373 360 if (location.hash) {
374 361 var splitIx = location.hash.indexOf('/?/');
375 362 if (splitIx !== -1){
376 363 var loc = location.hash.slice(0, splitIx);
377 364 var remainder = location.hash.slice(splitIx + 2);
378 365 }
379 366 else{
380 367 var loc = location.hash;
381 368 var remainder = null;
382 369 }
383 370 if (loc.length > 1){
384 371 var lineno = $(loc+'.lineno');
385 372 if (lineno.length > 0){
386 373 var tr = lineno.parents('tr.line');
387 374 tr.addClass('selected');
388 375
389 376 tr[0].scrollIntoView();
390 377
391 378 $.Topic('/ui/plugins/code/anchor_focus').prepare({
392 379 tr:tr,
393 380 remainder:remainder});
394 381 }
395 382 }
396 383 }
397 384
398 385 collapsableContent();
399 386 });
General Comments 0
You need to be logged in to leave comments. Login now