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