##// END OF EJS Templates
diffs: adding inline comment toggle fixes #2884
lisaq -
r696:d77e3bd5 default
parent child Browse files
Show More
@@ -1,885 +1,886 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 # define max context, a file with more than this numbers of lines is unusable
43 # define max context, a file with more than this numbers of lines is unusable
44 # in browser anyway
44 # in browser anyway
45 MAX_CONTEXT = 1024 * 1014
45 MAX_CONTEXT = 1024 * 1014
46
46
47
47
48 class OPS(object):
48 class OPS(object):
49 ADD = 'A'
49 ADD = 'A'
50 MOD = 'M'
50 MOD = 'M'
51 DEL = 'D'
51 DEL = 'D'
52
52
53
53
54 def wrap_to_table(str_):
54 def wrap_to_table(str_):
55 return '''<table class="code-difftable">
55 return '''<table class="code-difftable">
56 <tr class="line no-comment">
56 <tr class="line no-comment">
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
58 <td></td>
58 <td class="lineno new"></td>
59 <td class="lineno new"></td>
59 <td class="code no-comment"><pre>%s</pre></td>
60 <td class="code no-comment"><pre>%s</pre></td>
60 </tr>
61 </tr>
61 </table>''' % (_('Click to comment'), str_)
62 </table>''' % (_('Click to comment'), str_)
62
63
63
64
64 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
65 show_full_diff=False, ignore_whitespace=True, line_context=3,
66 show_full_diff=False, ignore_whitespace=True, line_context=3,
66 enable_comments=False):
67 enable_comments=False):
67 """
68 """
68 returns a wrapped diff into a table, checks for cut_off_limit for file and
69 returns a wrapped diff into a table, checks for cut_off_limit for file and
69 whole diff and presents proper message
70 whole diff and presents proper message
70 """
71 """
71
72
72 if filenode_old is None:
73 if filenode_old is None:
73 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
74
75
75 if filenode_old.is_binary or filenode_new.is_binary:
76 if filenode_old.is_binary or filenode_new.is_binary:
76 diff = wrap_to_table(_('Binary file'))
77 diff = wrap_to_table(_('Binary file'))
77 stats = None
78 stats = None
78 size = 0
79 size = 0
79 data = None
80 data = None
80
81
81 elif diff_limit != -1 and (diff_limit is None or
82 elif diff_limit != -1 and (diff_limit is None or
82 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
83
84
84 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
85 ignore_whitespace=ignore_whitespace,
86 ignore_whitespace=ignore_whitespace,
86 context=line_context)
87 context=line_context)
87 diff_processor = DiffProcessor(
88 diff_processor = DiffProcessor(
88 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
89 file_limit=file_limit, show_full_diff=show_full_diff)
90 file_limit=file_limit, show_full_diff=show_full_diff)
90 _parsed = diff_processor.prepare()
91 _parsed = diff_processor.prepare()
91
92
92 diff = diff_processor.as_html(enable_comments=enable_comments)
93 diff = diff_processor.as_html(enable_comments=enable_comments)
93 stats = _parsed[0]['stats'] if _parsed else None
94 stats = _parsed[0]['stats'] if _parsed else None
94 size = len(diff or '')
95 size = len(diff or '')
95 data = _parsed[0] if _parsed else None
96 data = _parsed[0] if _parsed else None
96 else:
97 else:
97 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
98 'diff menu to display this diff'))
99 'diff menu to display this diff'))
99 stats = None
100 stats = None
100 size = 0
101 size = 0
101 data = None
102 data = None
102 if not diff:
103 if not diff:
103 submodules = filter(lambda o: isinstance(o, SubModuleNode),
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
104 [filenode_new, filenode_old])
105 [filenode_new, filenode_old])
105 if submodules:
106 if submodules:
106 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
107 else:
108 else:
108 diff = wrap_to_table(_('No changes detected'))
109 diff = wrap_to_table(_('No changes detected'))
109
110
110 cs1 = filenode_old.commit.raw_id
111 cs1 = filenode_old.commit.raw_id
111 cs2 = filenode_new.commit.raw_id
112 cs2 = filenode_new.commit.raw_id
112
113
113 return size, cs1, cs2, diff, stats, data
114 return size, cs1, cs2, diff, stats, data
114
115
115
116
116 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
117 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
117 """
118 """
118 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
119 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
119
120
120 :param ignore_whitespace: ignore whitespaces in diff
121 :param ignore_whitespace: ignore whitespaces in diff
121 """
122 """
122 # make sure we pass in default context
123 # make sure we pass in default context
123 context = context or 3
124 context = context or 3
124 # protect against IntOverflow when passing HUGE context
125 # protect against IntOverflow when passing HUGE context
125 if context > MAX_CONTEXT:
126 if context > MAX_CONTEXT:
126 context = MAX_CONTEXT
127 context = MAX_CONTEXT
127
128
128 submodules = filter(lambda o: isinstance(o, SubModuleNode),
129 submodules = filter(lambda o: isinstance(o, SubModuleNode),
129 [filenode_new, filenode_old])
130 [filenode_new, filenode_old])
130 if submodules:
131 if submodules:
131 return ''
132 return ''
132
133
133 for filenode in (filenode_old, filenode_new):
134 for filenode in (filenode_old, filenode_new):
134 if not isinstance(filenode, FileNode):
135 if not isinstance(filenode, FileNode):
135 raise VCSError(
136 raise VCSError(
136 "Given object should be FileNode object, not %s"
137 "Given object should be FileNode object, not %s"
137 % filenode.__class__)
138 % filenode.__class__)
138
139
139 repo = filenode_new.commit.repository
140 repo = filenode_new.commit.repository
140 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
141 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
141 new_commit = filenode_new.commit
142 new_commit = filenode_new.commit
142
143
143 vcs_gitdiff = repo.get_diff(
144 vcs_gitdiff = repo.get_diff(
144 old_commit, new_commit, filenode_new.path,
145 old_commit, new_commit, filenode_new.path,
145 ignore_whitespace, context, path1=filenode_old.path)
146 ignore_whitespace, context, path1=filenode_old.path)
146 return vcs_gitdiff
147 return vcs_gitdiff
147
148
148 NEW_FILENODE = 1
149 NEW_FILENODE = 1
149 DEL_FILENODE = 2
150 DEL_FILENODE = 2
150 MOD_FILENODE = 3
151 MOD_FILENODE = 3
151 RENAMED_FILENODE = 4
152 RENAMED_FILENODE = 4
152 COPIED_FILENODE = 5
153 COPIED_FILENODE = 5
153 CHMOD_FILENODE = 6
154 CHMOD_FILENODE = 6
154 BIN_FILENODE = 7
155 BIN_FILENODE = 7
155
156
156
157
157 class LimitedDiffContainer(object):
158 class LimitedDiffContainer(object):
158
159
159 def __init__(self, diff_limit, cur_diff_size, diff):
160 def __init__(self, diff_limit, cur_diff_size, diff):
160 self.diff = diff
161 self.diff = diff
161 self.diff_limit = diff_limit
162 self.diff_limit = diff_limit
162 self.cur_diff_size = cur_diff_size
163 self.cur_diff_size = cur_diff_size
163
164
164 def __getitem__(self, key):
165 def __getitem__(self, key):
165 return self.diff.__getitem__(key)
166 return self.diff.__getitem__(key)
166
167
167 def __iter__(self):
168 def __iter__(self):
168 for l in self.diff:
169 for l in self.diff:
169 yield l
170 yield l
170
171
171
172
172 class Action(object):
173 class Action(object):
173 """
174 """
174 Contains constants for the action value of the lines in a parsed diff.
175 Contains constants for the action value of the lines in a parsed diff.
175 """
176 """
176
177
177 ADD = 'add'
178 ADD = 'add'
178 DELETE = 'del'
179 DELETE = 'del'
179 UNMODIFIED = 'unmod'
180 UNMODIFIED = 'unmod'
180
181
181 CONTEXT = 'context'
182 CONTEXT = 'context'
182
183
183
184
184 class DiffProcessor(object):
185 class DiffProcessor(object):
185 """
186 """
186 Give it a unified or git diff and it returns a list of the files that were
187 Give it a unified or git diff and it returns a list of the files that were
187 mentioned in the diff together with a dict of meta information that
188 mentioned in the diff together with a dict of meta information that
188 can be used to render it in a HTML template.
189 can be used to render it in a HTML template.
189
190
190 .. note:: Unicode handling
191 .. note:: Unicode handling
191
192
192 The original diffs are a byte sequence and can contain filenames
193 The original diffs are a byte sequence and can contain filenames
193 in mixed encodings. This class generally returns `unicode` objects
194 in mixed encodings. This class generally returns `unicode` objects
194 since the result is intended for presentation to the user.
195 since the result is intended for presentation to the user.
195
196
196 """
197 """
197 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
198 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
198 _newline_marker = re.compile(r'^\\ No newline at end of file')
199 _newline_marker = re.compile(r'^\\ No newline at end of file')
199
200
200 # used for inline highlighter word split
201 # used for inline highlighter word split
201 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
202 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
202
203
203 def __init__(self, diff, format='gitdiff', diff_limit=None,
204 def __init__(self, diff, format='gitdiff', diff_limit=None,
204 file_limit=None, show_full_diff=True):
205 file_limit=None, show_full_diff=True):
205 """
206 """
206 :param diff: A `Diff` object representing a diff from a vcs backend
207 :param diff: A `Diff` object representing a diff from a vcs backend
207 :param format: format of diff passed, `udiff` or `gitdiff`
208 :param format: format of diff passed, `udiff` or `gitdiff`
208 :param diff_limit: define the size of diff that is considered "big"
209 :param diff_limit: define the size of diff that is considered "big"
209 based on that parameter cut off will be triggered, set to None
210 based on that parameter cut off will be triggered, set to None
210 to show full diff
211 to show full diff
211 """
212 """
212 self._diff = diff
213 self._diff = diff
213 self._format = format
214 self._format = format
214 self.adds = 0
215 self.adds = 0
215 self.removes = 0
216 self.removes = 0
216 # calculate diff size
217 # calculate diff size
217 self.diff_limit = diff_limit
218 self.diff_limit = diff_limit
218 self.file_limit = file_limit
219 self.file_limit = file_limit
219 self.show_full_diff = show_full_diff
220 self.show_full_diff = show_full_diff
220 self.cur_diff_size = 0
221 self.cur_diff_size = 0
221 self.parsed = False
222 self.parsed = False
222 self.parsed_diff = []
223 self.parsed_diff = []
223
224
224 if format == 'gitdiff':
225 if format == 'gitdiff':
225 self.differ = self._highlight_line_difflib
226 self.differ = self._highlight_line_difflib
226 self._parser = self._parse_gitdiff
227 self._parser = self._parse_gitdiff
227 else:
228 else:
228 self.differ = self._highlight_line_udiff
229 self.differ = self._highlight_line_udiff
229 self._parser = self._parse_udiff
230 self._parser = self._parse_udiff
230
231
231 def _copy_iterator(self):
232 def _copy_iterator(self):
232 """
233 """
233 make a fresh copy of generator, we should not iterate thru
234 make a fresh copy of generator, we should not iterate thru
234 an original as it's needed for repeating operations on
235 an original as it's needed for repeating operations on
235 this instance of DiffProcessor
236 this instance of DiffProcessor
236 """
237 """
237 self.__udiff, iterator_copy = tee(self.__udiff)
238 self.__udiff, iterator_copy = tee(self.__udiff)
238 return iterator_copy
239 return iterator_copy
239
240
240 def _escaper(self, string):
241 def _escaper(self, string):
241 """
242 """
242 Escaper for diff escapes special chars and checks the diff limit
243 Escaper for diff escapes special chars and checks the diff limit
243
244
244 :param string:
245 :param string:
245 """
246 """
246
247
247 self.cur_diff_size += len(string)
248 self.cur_diff_size += len(string)
248
249
249 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
250 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
250 raise DiffLimitExceeded('Diff Limit Exceeded')
251 raise DiffLimitExceeded('Diff Limit Exceeded')
251
252
252 return safe_unicode(string)\
253 return safe_unicode(string)\
253 .replace('&', '&amp;')\
254 .replace('&', '&amp;')\
254 .replace('<', '&lt;')\
255 .replace('<', '&lt;')\
255 .replace('>', '&gt;')
256 .replace('>', '&gt;')
256
257
257 def _line_counter(self, l):
258 def _line_counter(self, l):
258 """
259 """
259 Checks each line and bumps total adds/removes for this diff
260 Checks each line and bumps total adds/removes for this diff
260
261
261 :param l:
262 :param l:
262 """
263 """
263 if l.startswith('+') and not l.startswith('+++'):
264 if l.startswith('+') and not l.startswith('+++'):
264 self.adds += 1
265 self.adds += 1
265 elif l.startswith('-') and not l.startswith('---'):
266 elif l.startswith('-') and not l.startswith('---'):
266 self.removes += 1
267 self.removes += 1
267 return safe_unicode(l)
268 return safe_unicode(l)
268
269
269 def _highlight_line_difflib(self, line, next_):
270 def _highlight_line_difflib(self, line, next_):
270 """
271 """
271 Highlight inline changes in both lines.
272 Highlight inline changes in both lines.
272 """
273 """
273
274
274 if line['action'] == Action.DELETE:
275 if line['action'] == Action.DELETE:
275 old, new = line, next_
276 old, new = line, next_
276 else:
277 else:
277 old, new = next_, line
278 old, new = next_, line
278
279
279 oldwords = self._token_re.split(old['line'])
280 oldwords = self._token_re.split(old['line'])
280 newwords = self._token_re.split(new['line'])
281 newwords = self._token_re.split(new['line'])
281 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
282 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
282
283
283 oldfragments, newfragments = [], []
284 oldfragments, newfragments = [], []
284 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
285 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
285 oldfrag = ''.join(oldwords[i1:i2])
286 oldfrag = ''.join(oldwords[i1:i2])
286 newfrag = ''.join(newwords[j1:j2])
287 newfrag = ''.join(newwords[j1:j2])
287 if tag != 'equal':
288 if tag != 'equal':
288 if oldfrag:
289 if oldfrag:
289 oldfrag = '<del>%s</del>' % oldfrag
290 oldfrag = '<del>%s</del>' % oldfrag
290 if newfrag:
291 if newfrag:
291 newfrag = '<ins>%s</ins>' % newfrag
292 newfrag = '<ins>%s</ins>' % newfrag
292 oldfragments.append(oldfrag)
293 oldfragments.append(oldfrag)
293 newfragments.append(newfrag)
294 newfragments.append(newfrag)
294
295
295 old['line'] = "".join(oldfragments)
296 old['line'] = "".join(oldfragments)
296 new['line'] = "".join(newfragments)
297 new['line'] = "".join(newfragments)
297
298
298 def _highlight_line_udiff(self, line, next_):
299 def _highlight_line_udiff(self, line, next_):
299 """
300 """
300 Highlight inline changes in both lines.
301 Highlight inline changes in both lines.
301 """
302 """
302 start = 0
303 start = 0
303 limit = min(len(line['line']), len(next_['line']))
304 limit = min(len(line['line']), len(next_['line']))
304 while start < limit and line['line'][start] == next_['line'][start]:
305 while start < limit and line['line'][start] == next_['line'][start]:
305 start += 1
306 start += 1
306 end = -1
307 end = -1
307 limit -= start
308 limit -= start
308 while -end <= limit and line['line'][end] == next_['line'][end]:
309 while -end <= limit and line['line'][end] == next_['line'][end]:
309 end -= 1
310 end -= 1
310 end += 1
311 end += 1
311 if start or end:
312 if start or end:
312 def do(l):
313 def do(l):
313 last = end + len(l['line'])
314 last = end + len(l['line'])
314 if l['action'] == Action.ADD:
315 if l['action'] == Action.ADD:
315 tag = 'ins'
316 tag = 'ins'
316 else:
317 else:
317 tag = 'del'
318 tag = 'del'
318 l['line'] = '%s<%s>%s</%s>%s' % (
319 l['line'] = '%s<%s>%s</%s>%s' % (
319 l['line'][:start],
320 l['line'][:start],
320 tag,
321 tag,
321 l['line'][start:last],
322 l['line'][start:last],
322 tag,
323 tag,
323 l['line'][last:]
324 l['line'][last:]
324 )
325 )
325 do(line)
326 do(line)
326 do(next_)
327 do(next_)
327
328
328 def _clean_line(self, line, command):
329 def _clean_line(self, line, command):
329 if command in ['+', '-', ' ']:
330 if command in ['+', '-', ' ']:
330 # only modify the line if it's actually a diff thing
331 # only modify the line if it's actually a diff thing
331 line = line[1:]
332 line = line[1:]
332 return line
333 return line
333
334
334 def _parse_gitdiff(self, inline_diff=True):
335 def _parse_gitdiff(self, inline_diff=True):
335 _files = []
336 _files = []
336 diff_container = lambda arg: arg
337 diff_container = lambda arg: arg
337
338
338 for chunk in self._diff.chunks():
339 for chunk in self._diff.chunks():
339 head = chunk.header
340 head = chunk.header
340
341
341 diff = imap(self._escaper, chunk.diff.splitlines(1))
342 diff = imap(self._escaper, chunk.diff.splitlines(1))
342 raw_diff = chunk.raw
343 raw_diff = chunk.raw
343 limited_diff = False
344 limited_diff = False
344 exceeds_limit = False
345 exceeds_limit = False
345
346
346 op = None
347 op = None
347 stats = {
348 stats = {
348 'added': 0,
349 'added': 0,
349 'deleted': 0,
350 'deleted': 0,
350 'binary': False,
351 'binary': False,
351 'ops': {},
352 'ops': {},
352 }
353 }
353
354
354 if head['deleted_file_mode']:
355 if head['deleted_file_mode']:
355 op = OPS.DEL
356 op = OPS.DEL
356 stats['binary'] = True
357 stats['binary'] = True
357 stats['ops'][DEL_FILENODE] = 'deleted file'
358 stats['ops'][DEL_FILENODE] = 'deleted file'
358
359
359 elif head['new_file_mode']:
360 elif head['new_file_mode']:
360 op = OPS.ADD
361 op = OPS.ADD
361 stats['binary'] = True
362 stats['binary'] = True
362 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
363 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
363 else: # modify operation, can be copy, rename or chmod
364 else: # modify operation, can be copy, rename or chmod
364
365
365 # CHMOD
366 # CHMOD
366 if head['new_mode'] and head['old_mode']:
367 if head['new_mode'] and head['old_mode']:
367 op = OPS.MOD
368 op = OPS.MOD
368 stats['binary'] = True
369 stats['binary'] = True
369 stats['ops'][CHMOD_FILENODE] = (
370 stats['ops'][CHMOD_FILENODE] = (
370 'modified file chmod %s => %s' % (
371 'modified file chmod %s => %s' % (
371 head['old_mode'], head['new_mode']))
372 head['old_mode'], head['new_mode']))
372 # RENAME
373 # RENAME
373 if head['rename_from'] != head['rename_to']:
374 if head['rename_from'] != head['rename_to']:
374 op = OPS.MOD
375 op = OPS.MOD
375 stats['binary'] = True
376 stats['binary'] = True
376 stats['ops'][RENAMED_FILENODE] = (
377 stats['ops'][RENAMED_FILENODE] = (
377 'file renamed from %s to %s' % (
378 'file renamed from %s to %s' % (
378 head['rename_from'], head['rename_to']))
379 head['rename_from'], head['rename_to']))
379 # COPY
380 # COPY
380 if head.get('copy_from') and head.get('copy_to'):
381 if head.get('copy_from') and head.get('copy_to'):
381 op = OPS.MOD
382 op = OPS.MOD
382 stats['binary'] = True
383 stats['binary'] = True
383 stats['ops'][COPIED_FILENODE] = (
384 stats['ops'][COPIED_FILENODE] = (
384 'file copied from %s to %s' % (
385 'file copied from %s to %s' % (
385 head['copy_from'], head['copy_to']))
386 head['copy_from'], head['copy_to']))
386
387
387 # If our new parsed headers didn't match anything fallback to
388 # If our new parsed headers didn't match anything fallback to
388 # old style detection
389 # old style detection
389 if op is None:
390 if op is None:
390 if not head['a_file'] and head['b_file']:
391 if not head['a_file'] and head['b_file']:
391 op = OPS.ADD
392 op = OPS.ADD
392 stats['binary'] = True
393 stats['binary'] = True
393 stats['ops'][NEW_FILENODE] = 'new file'
394 stats['ops'][NEW_FILENODE] = 'new file'
394
395
395 elif head['a_file'] and not head['b_file']:
396 elif head['a_file'] and not head['b_file']:
396 op = OPS.DEL
397 op = OPS.DEL
397 stats['binary'] = True
398 stats['binary'] = True
398 stats['ops'][DEL_FILENODE] = 'deleted file'
399 stats['ops'][DEL_FILENODE] = 'deleted file'
399
400
400 # it's not ADD not DELETE
401 # it's not ADD not DELETE
401 if op is None:
402 if op is None:
402 op = OPS.MOD
403 op = OPS.MOD
403 stats['binary'] = True
404 stats['binary'] = True
404 stats['ops'][MOD_FILENODE] = 'modified file'
405 stats['ops'][MOD_FILENODE] = 'modified file'
405
406
406 # a real non-binary diff
407 # a real non-binary diff
407 if head['a_file'] or head['b_file']:
408 if head['a_file'] or head['b_file']:
408 try:
409 try:
409 raw_diff, chunks, _stats = self._parse_lines(diff)
410 raw_diff, chunks, _stats = self._parse_lines(diff)
410 stats['binary'] = False
411 stats['binary'] = False
411 stats['added'] = _stats[0]
412 stats['added'] = _stats[0]
412 stats['deleted'] = _stats[1]
413 stats['deleted'] = _stats[1]
413 # explicit mark that it's a modified file
414 # explicit mark that it's a modified file
414 if op == OPS.MOD:
415 if op == OPS.MOD:
415 stats['ops'][MOD_FILENODE] = 'modified file'
416 stats['ops'][MOD_FILENODE] = 'modified file'
416 exceeds_limit = len(raw_diff) > self.file_limit
417 exceeds_limit = len(raw_diff) > self.file_limit
417
418
418 # changed from _escaper function so we validate size of
419 # changed from _escaper function so we validate size of
419 # each file instead of the whole diff
420 # each file instead of the whole diff
420 # diff will hide big files but still show small ones
421 # diff will hide big files but still show small ones
421 # from my tests, big files are fairly safe to be parsed
422 # from my tests, big files are fairly safe to be parsed
422 # but the browser is the bottleneck
423 # but the browser is the bottleneck
423 if not self.show_full_diff and exceeds_limit:
424 if not self.show_full_diff and exceeds_limit:
424 raise DiffLimitExceeded('File Limit Exceeded')
425 raise DiffLimitExceeded('File Limit Exceeded')
425
426
426 except DiffLimitExceeded:
427 except DiffLimitExceeded:
427 diff_container = lambda _diff: \
428 diff_container = lambda _diff: \
428 LimitedDiffContainer(
429 LimitedDiffContainer(
429 self.diff_limit, self.cur_diff_size, _diff)
430 self.diff_limit, self.cur_diff_size, _diff)
430
431
431 exceeds_limit = len(raw_diff) > self.file_limit
432 exceeds_limit = len(raw_diff) > self.file_limit
432 limited_diff = True
433 limited_diff = True
433 chunks = []
434 chunks = []
434
435
435 else: # GIT format binary patch, or possibly empty diff
436 else: # GIT format binary patch, or possibly empty diff
436 if head['bin_patch']:
437 if head['bin_patch']:
437 # we have operation already extracted, but we mark simply
438 # we have operation already extracted, but we mark simply
438 # it's a diff we wont show for binary files
439 # it's a diff we wont show for binary files
439 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
440 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
440 chunks = []
441 chunks = []
441
442
442 if chunks and not self.show_full_diff and op == OPS.DEL:
443 if chunks and not self.show_full_diff and op == OPS.DEL:
443 # if not full diff mode show deleted file contents
444 # if not full diff mode show deleted file contents
444 # TODO: anderson: if the view is not too big, there is no way
445 # TODO: anderson: if the view is not too big, there is no way
445 # to see the content of the file
446 # to see the content of the file
446 chunks = []
447 chunks = []
447
448
448 chunks.insert(0, [{
449 chunks.insert(0, [{
449 'old_lineno': '',
450 'old_lineno': '',
450 'new_lineno': '',
451 'new_lineno': '',
451 'action': Action.CONTEXT,
452 'action': Action.CONTEXT,
452 'line': msg,
453 'line': msg,
453 } for _op, msg in stats['ops'].iteritems()
454 } for _op, msg in stats['ops'].iteritems()
454 if _op not in [MOD_FILENODE]])
455 if _op not in [MOD_FILENODE]])
455
456
456 _files.append({
457 _files.append({
457 'filename': safe_unicode(head['b_path']),
458 'filename': safe_unicode(head['b_path']),
458 'old_revision': head['a_blob_id'],
459 'old_revision': head['a_blob_id'],
459 'new_revision': head['b_blob_id'],
460 'new_revision': head['b_blob_id'],
460 'chunks': chunks,
461 'chunks': chunks,
461 'raw_diff': safe_unicode(raw_diff),
462 'raw_diff': safe_unicode(raw_diff),
462 'operation': op,
463 'operation': op,
463 'stats': stats,
464 'stats': stats,
464 'exceeds_limit': exceeds_limit,
465 'exceeds_limit': exceeds_limit,
465 'is_limited_diff': limited_diff,
466 'is_limited_diff': limited_diff,
466 })
467 })
467
468
468 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
469 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
469 OPS.DEL: 2}.get(info['operation'])
470 OPS.DEL: 2}.get(info['operation'])
470
471
471 if not inline_diff:
472 if not inline_diff:
472 return diff_container(sorted(_files, key=sorter))
473 return diff_container(sorted(_files, key=sorter))
473
474
474 # highlight inline changes
475 # highlight inline changes
475 for diff_data in _files:
476 for diff_data in _files:
476 for chunk in diff_data['chunks']:
477 for chunk in diff_data['chunks']:
477 lineiter = iter(chunk)
478 lineiter = iter(chunk)
478 try:
479 try:
479 while 1:
480 while 1:
480 line = lineiter.next()
481 line = lineiter.next()
481 if line['action'] not in (
482 if line['action'] not in (
482 Action.UNMODIFIED, Action.CONTEXT):
483 Action.UNMODIFIED, Action.CONTEXT):
483 nextline = lineiter.next()
484 nextline = lineiter.next()
484 if nextline['action'] in ['unmod', 'context'] or \
485 if nextline['action'] in ['unmod', 'context'] or \
485 nextline['action'] == line['action']:
486 nextline['action'] == line['action']:
486 continue
487 continue
487 self.differ(line, nextline)
488 self.differ(line, nextline)
488 except StopIteration:
489 except StopIteration:
489 pass
490 pass
490
491
491 return diff_container(sorted(_files, key=sorter))
492 return diff_container(sorted(_files, key=sorter))
492
493
493 def _parse_udiff(self, inline_diff=True):
494 def _parse_udiff(self, inline_diff=True):
494 raise NotImplementedError()
495 raise NotImplementedError()
495
496
496 def _parse_lines(self, diff):
497 def _parse_lines(self, diff):
497 """
498 """
498 Parse the diff an return data for the template.
499 Parse the diff an return data for the template.
499 """
500 """
500
501
501 lineiter = iter(diff)
502 lineiter = iter(diff)
502 stats = [0, 0]
503 stats = [0, 0]
503 chunks = []
504 chunks = []
504 raw_diff = []
505 raw_diff = []
505
506
506 try:
507 try:
507 line = lineiter.next()
508 line = lineiter.next()
508
509
509 while line:
510 while line:
510 raw_diff.append(line)
511 raw_diff.append(line)
511 lines = []
512 lines = []
512 chunks.append(lines)
513 chunks.append(lines)
513
514
514 match = self._chunk_re.match(line)
515 match = self._chunk_re.match(line)
515
516
516 if not match:
517 if not match:
517 break
518 break
518
519
519 gr = match.groups()
520 gr = match.groups()
520 (old_line, old_end,
521 (old_line, old_end,
521 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
522 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
522 old_line -= 1
523 old_line -= 1
523 new_line -= 1
524 new_line -= 1
524
525
525 context = len(gr) == 5
526 context = len(gr) == 5
526 old_end += old_line
527 old_end += old_line
527 new_end += new_line
528 new_end += new_line
528
529
529 if context:
530 if context:
530 # skip context only if it's first line
531 # skip context only if it's first line
531 if int(gr[0]) > 1:
532 if int(gr[0]) > 1:
532 lines.append({
533 lines.append({
533 'old_lineno': '...',
534 'old_lineno': '...',
534 'new_lineno': '...',
535 'new_lineno': '...',
535 'action': Action.CONTEXT,
536 'action': Action.CONTEXT,
536 'line': line,
537 'line': line,
537 })
538 })
538
539
539 line = lineiter.next()
540 line = lineiter.next()
540
541
541 while old_line < old_end or new_line < new_end:
542 while old_line < old_end or new_line < new_end:
542 command = ' '
543 command = ' '
543 if line:
544 if line:
544 command = line[0]
545 command = line[0]
545
546
546 affects_old = affects_new = False
547 affects_old = affects_new = False
547
548
548 # ignore those if we don't expect them
549 # ignore those if we don't expect them
549 if command in '#@':
550 if command in '#@':
550 continue
551 continue
551 elif command == '+':
552 elif command == '+':
552 affects_new = True
553 affects_new = True
553 action = Action.ADD
554 action = Action.ADD
554 stats[0] += 1
555 stats[0] += 1
555 elif command == '-':
556 elif command == '-':
556 affects_old = True
557 affects_old = True
557 action = Action.DELETE
558 action = Action.DELETE
558 stats[1] += 1
559 stats[1] += 1
559 else:
560 else:
560 affects_old = affects_new = True
561 affects_old = affects_new = True
561 action = Action.UNMODIFIED
562 action = Action.UNMODIFIED
562
563
563 if not self._newline_marker.match(line):
564 if not self._newline_marker.match(line):
564 old_line += affects_old
565 old_line += affects_old
565 new_line += affects_new
566 new_line += affects_new
566 lines.append({
567 lines.append({
567 'old_lineno': affects_old and old_line or '',
568 'old_lineno': affects_old and old_line or '',
568 'new_lineno': affects_new and new_line or '',
569 'new_lineno': affects_new and new_line or '',
569 'action': action,
570 'action': action,
570 'line': self._clean_line(line, command)
571 'line': self._clean_line(line, command)
571 })
572 })
572 raw_diff.append(line)
573 raw_diff.append(line)
573
574
574 line = lineiter.next()
575 line = lineiter.next()
575
576
576 if self._newline_marker.match(line):
577 if self._newline_marker.match(line):
577 # we need to append to lines, since this is not
578 # we need to append to lines, since this is not
578 # counted in the line specs of diff
579 # counted in the line specs of diff
579 lines.append({
580 lines.append({
580 'old_lineno': '...',
581 'old_lineno': '...',
581 'new_lineno': '...',
582 'new_lineno': '...',
582 'action': Action.CONTEXT,
583 'action': Action.CONTEXT,
583 'line': self._clean_line(line, command)
584 'line': self._clean_line(line, command)
584 })
585 })
585
586
586 except StopIteration:
587 except StopIteration:
587 pass
588 pass
588 return ''.join(raw_diff), chunks, stats
589 return ''.join(raw_diff), chunks, stats
589
590
590 def _safe_id(self, idstring):
591 def _safe_id(self, idstring):
591 """Make a string safe for including in an id attribute.
592 """Make a string safe for including in an id attribute.
592
593
593 The HTML spec says that id attributes 'must begin with
594 The HTML spec says that id attributes 'must begin with
594 a letter ([A-Za-z]) and may be followed by any number
595 a letter ([A-Za-z]) and may be followed by any number
595 of letters, digits ([0-9]), hyphens ("-"), underscores
596 of letters, digits ([0-9]), hyphens ("-"), underscores
596 ("_"), colons (":"), and periods (".")'. These regexps
597 ("_"), colons (":"), and periods (".")'. These regexps
597 are slightly over-zealous, in that they remove colons
598 are slightly over-zealous, in that they remove colons
598 and periods unnecessarily.
599 and periods unnecessarily.
599
600
600 Whitespace is transformed into underscores, and then
601 Whitespace is transformed into underscores, and then
601 anything which is not a hyphen or a character that
602 anything which is not a hyphen or a character that
602 matches \w (alphanumerics and underscore) is removed.
603 matches \w (alphanumerics and underscore) is removed.
603
604
604 """
605 """
605 # Transform all whitespace to underscore
606 # Transform all whitespace to underscore
606 idstring = re.sub(r'\s', "_", '%s' % idstring)
607 idstring = re.sub(r'\s', "_", '%s' % idstring)
607 # Remove everything that is not a hyphen or a member of \w
608 # Remove everything that is not a hyphen or a member of \w
608 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
609 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
609 return idstring
610 return idstring
610
611
611 def prepare(self, inline_diff=True):
612 def prepare(self, inline_diff=True):
612 """
613 """
613 Prepare the passed udiff for HTML rendering.
614 Prepare the passed udiff for HTML rendering.
614
615
615 :return: A list of dicts with diff information.
616 :return: A list of dicts with diff information.
616 """
617 """
617 parsed = self._parser(inline_diff=inline_diff)
618 parsed = self._parser(inline_diff=inline_diff)
618 self.parsed = True
619 self.parsed = True
619 self.parsed_diff = parsed
620 self.parsed_diff = parsed
620 return parsed
621 return parsed
621
622
622 def as_raw(self, diff_lines=None):
623 def as_raw(self, diff_lines=None):
623 """
624 """
624 Returns raw diff as a byte string
625 Returns raw diff as a byte string
625 """
626 """
626 return self._diff.raw
627 return self._diff.raw
627
628
628 def as_html(self, table_class='code-difftable', line_class='line',
629 def as_html(self, table_class='code-difftable', line_class='line',
629 old_lineno_class='lineno old', new_lineno_class='lineno new',
630 old_lineno_class='lineno old', new_lineno_class='lineno new',
630 code_class='code', enable_comments=False, parsed_lines=None):
631 code_class='code', enable_comments=False, parsed_lines=None):
631 """
632 """
632 Return given diff as html table with customized css classes
633 Return given diff as html table with customized css classes
633 """
634 """
634 def _link_to_if(condition, label, url):
635 def _link_to_if(condition, label, url):
635 """
636 """
636 Generates a link if condition is meet or just the label if not.
637 Generates a link if condition is meet or just the label if not.
637 """
638 """
638
639
639 if condition:
640 if condition:
640 return '''<a href="%(url)s" class="tooltip"
641 return '''<a href="%(url)s" class="tooltip"
641 title="%(title)s">%(label)s</a>''' % {
642 title="%(title)s">%(label)s</a>''' % {
642 'title': _('Click to select line'),
643 'title': _('Click to select line'),
643 'url': url,
644 'url': url,
644 'label': label
645 'label': label
645 }
646 }
646 else:
647 else:
647 return label
648 return label
648 if not self.parsed:
649 if not self.parsed:
649 self.prepare()
650 self.prepare()
650
651
651 diff_lines = self.parsed_diff
652 diff_lines = self.parsed_diff
652 if parsed_lines:
653 if parsed_lines:
653 diff_lines = parsed_lines
654 diff_lines = parsed_lines
654
655
655 _html_empty = True
656 _html_empty = True
656 _html = []
657 _html = []
657 _html.append('''<table class="%(table_class)s">\n''' % {
658 _html.append('''<table class="%(table_class)s">\n''' % {
658 'table_class': table_class
659 'table_class': table_class
659 })
660 })
660
661
661 for diff in diff_lines:
662 for diff in diff_lines:
662 for line in diff['chunks']:
663 for line in diff['chunks']:
663 _html_empty = False
664 _html_empty = False
664 for change in line:
665 for change in line:
665 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
666 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
666 'lc': line_class,
667 'lc': line_class,
667 'action': change['action']
668 'action': change['action']
668 })
669 })
669 anchor_old_id = ''
670 anchor_old_id = ''
670 anchor_new_id = ''
671 anchor_new_id = ''
671 anchor_old = "%(filename)s_o%(oldline_no)s" % {
672 anchor_old = "%(filename)s_o%(oldline_no)s" % {
672 'filename': self._safe_id(diff['filename']),
673 'filename': self._safe_id(diff['filename']),
673 'oldline_no': change['old_lineno']
674 'oldline_no': change['old_lineno']
674 }
675 }
675 anchor_new = "%(filename)s_n%(oldline_no)s" % {
676 anchor_new = "%(filename)s_n%(oldline_no)s" % {
676 'filename': self._safe_id(diff['filename']),
677 'filename': self._safe_id(diff['filename']),
677 'oldline_no': change['new_lineno']
678 'oldline_no': change['new_lineno']
678 }
679 }
679 cond_old = (change['old_lineno'] != '...' and
680 cond_old = (change['old_lineno'] != '...' and
680 change['old_lineno'])
681 change['old_lineno'])
681 cond_new = (change['new_lineno'] != '...' and
682 cond_new = (change['new_lineno'] != '...' and
682 change['new_lineno'])
683 change['new_lineno'])
683 if cond_old:
684 if cond_old:
684 anchor_old_id = 'id="%s"' % anchor_old
685 anchor_old_id = 'id="%s"' % anchor_old
685 if cond_new:
686 if cond_new:
686 anchor_new_id = 'id="%s"' % anchor_new
687 anchor_new_id = 'id="%s"' % anchor_new
687
688
688 if change['action'] != Action.CONTEXT:
689 if change['action'] != Action.CONTEXT:
689 anchor_link = True
690 anchor_link = True
690 else:
691 else:
691 anchor_link = False
692 anchor_link = False
692
693
693 ###########################################################
694 ###########################################################
694 # COMMENT ICON
695 # COMMENT ICONS
695 ###########################################################
696 ###########################################################
696 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
697 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
697
698
698 if enable_comments and change['action'] != Action.CONTEXT:
699 if enable_comments and change['action'] != Action.CONTEXT:
699 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
700 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
700
701
701 _html.append('''</span></td>\n''')
702 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>\n''')
702
703
703 ###########################################################
704 ###########################################################
704 # OLD LINE NUMBER
705 # OLD LINE NUMBER
705 ###########################################################
706 ###########################################################
706 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
707 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
707 'a_id': anchor_old_id,
708 'a_id': anchor_old_id,
708 'olc': old_lineno_class
709 'olc': old_lineno_class
709 })
710 })
710
711
711 _html.append('''%(link)s''' % {
712 _html.append('''%(link)s''' % {
712 'link': _link_to_if(anchor_link, change['old_lineno'],
713 'link': _link_to_if(anchor_link, change['old_lineno'],
713 '#%s' % anchor_old)
714 '#%s' % anchor_old)
714 })
715 })
715 _html.append('''</td>\n''')
716 _html.append('''</td>\n''')
716 ###########################################################
717 ###########################################################
717 # NEW LINE NUMBER
718 # NEW LINE NUMBER
718 ###########################################################
719 ###########################################################
719
720
720 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
721 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
721 'a_id': anchor_new_id,
722 'a_id': anchor_new_id,
722 'nlc': new_lineno_class
723 'nlc': new_lineno_class
723 })
724 })
724
725
725 _html.append('''%(link)s''' % {
726 _html.append('''%(link)s''' % {
726 'link': _link_to_if(anchor_link, change['new_lineno'],
727 'link': _link_to_if(anchor_link, change['new_lineno'],
727 '#%s' % anchor_new)
728 '#%s' % anchor_new)
728 })
729 })
729 _html.append('''</td>\n''')
730 _html.append('''</td>\n''')
730 ###########################################################
731 ###########################################################
731 # CODE
732 # CODE
732 ###########################################################
733 ###########################################################
733 code_classes = [code_class]
734 code_classes = [code_class]
734 if (not enable_comments or
735 if (not enable_comments or
735 change['action'] == Action.CONTEXT):
736 change['action'] == Action.CONTEXT):
736 code_classes.append('no-comment')
737 code_classes.append('no-comment')
737 _html.append('\t<td class="%s">' % ' '.join(code_classes))
738 _html.append('\t<td class="%s">' % ' '.join(code_classes))
738 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
739 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
739 'code': change['line']
740 'code': change['line']
740 })
741 })
741
742
742 _html.append('''\t</td>''')
743 _html.append('''\t</td>''')
743 _html.append('''\n</tr>\n''')
744 _html.append('''\n</tr>\n''')
744 _html.append('''</table>''')
745 _html.append('''</table>''')
745 if _html_empty:
746 if _html_empty:
746 return None
747 return None
747 return ''.join(_html)
748 return ''.join(_html)
748
749
749 def stat(self):
750 def stat(self):
750 """
751 """
751 Returns tuple of added, and removed lines for this instance
752 Returns tuple of added, and removed lines for this instance
752 """
753 """
753 return self.adds, self.removes
754 return self.adds, self.removes
754
755
755 def get_context_of_line(
756 def get_context_of_line(
756 self, path, diff_line=None, context_before=3, context_after=3):
757 self, path, diff_line=None, context_before=3, context_after=3):
757 """
758 """
758 Returns the context lines for the specified diff line.
759 Returns the context lines for the specified diff line.
759
760
760 :type diff_line: :class:`DiffLineNumber`
761 :type diff_line: :class:`DiffLineNumber`
761 """
762 """
762 assert self.parsed, "DiffProcessor is not initialized."
763 assert self.parsed, "DiffProcessor is not initialized."
763
764
764 if None not in diff_line:
765 if None not in diff_line:
765 raise ValueError(
766 raise ValueError(
766 "Cannot specify both line numbers: {}".format(diff_line))
767 "Cannot specify both line numbers: {}".format(diff_line))
767
768
768 file_diff = self._get_file_diff(path)
769 file_diff = self._get_file_diff(path)
769 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
770 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
770
771
771 first_line_to_include = max(idx - context_before, 0)
772 first_line_to_include = max(idx - context_before, 0)
772 first_line_after_context = idx + context_after + 1
773 first_line_after_context = idx + context_after + 1
773 context_lines = chunk[first_line_to_include:first_line_after_context]
774 context_lines = chunk[first_line_to_include:first_line_after_context]
774
775
775 line_contents = [
776 line_contents = [
776 _context_line(line) for line in context_lines
777 _context_line(line) for line in context_lines
777 if _is_diff_content(line)]
778 if _is_diff_content(line)]
778 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
779 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
779 # Once they are fixed, we can drop this line here.
780 # Once they are fixed, we can drop this line here.
780 if line_contents:
781 if line_contents:
781 line_contents[-1] = (
782 line_contents[-1] = (
782 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
783 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
783 return line_contents
784 return line_contents
784
785
785 def find_context(self, path, context, offset=0):
786 def find_context(self, path, context, offset=0):
786 """
787 """
787 Finds the given `context` inside of the diff.
788 Finds the given `context` inside of the diff.
788
789
789 Use the parameter `offset` to specify which offset the target line has
790 Use the parameter `offset` to specify which offset the target line has
790 inside of the given `context`. This way the correct diff line will be
791 inside of the given `context`. This way the correct diff line will be
791 returned.
792 returned.
792
793
793 :param offset: Shall be used to specify the offset of the main line
794 :param offset: Shall be used to specify the offset of the main line
794 within the given `context`.
795 within the given `context`.
795 """
796 """
796 if offset < 0 or offset >= len(context):
797 if offset < 0 or offset >= len(context):
797 raise ValueError(
798 raise ValueError(
798 "Only positive values up to the length of the context "
799 "Only positive values up to the length of the context "
799 "minus one are allowed.")
800 "minus one are allowed.")
800
801
801 matches = []
802 matches = []
802 file_diff = self._get_file_diff(path)
803 file_diff = self._get_file_diff(path)
803
804
804 for chunk in file_diff['chunks']:
805 for chunk in file_diff['chunks']:
805 context_iter = iter(context)
806 context_iter = iter(context)
806 for line_idx, line in enumerate(chunk):
807 for line_idx, line in enumerate(chunk):
807 try:
808 try:
808 if _context_line(line) == context_iter.next():
809 if _context_line(line) == context_iter.next():
809 continue
810 continue
810 except StopIteration:
811 except StopIteration:
811 matches.append((line_idx, chunk))
812 matches.append((line_idx, chunk))
812 context_iter = iter(context)
813 context_iter = iter(context)
813
814
814 # Increment position and triger StopIteration
815 # Increment position and triger StopIteration
815 # if we had a match at the end
816 # if we had a match at the end
816 line_idx += 1
817 line_idx += 1
817 try:
818 try:
818 context_iter.next()
819 context_iter.next()
819 except StopIteration:
820 except StopIteration:
820 matches.append((line_idx, chunk))
821 matches.append((line_idx, chunk))
821
822
822 effective_offset = len(context) - offset
823 effective_offset = len(context) - offset
823 found_at_diff_lines = [
824 found_at_diff_lines = [
824 _line_to_diff_line_number(chunk[idx - effective_offset])
825 _line_to_diff_line_number(chunk[idx - effective_offset])
825 for idx, chunk in matches]
826 for idx, chunk in matches]
826
827
827 return found_at_diff_lines
828 return found_at_diff_lines
828
829
829 def _get_file_diff(self, path):
830 def _get_file_diff(self, path):
830 for file_diff in self.parsed_diff:
831 for file_diff in self.parsed_diff:
831 if file_diff['filename'] == path:
832 if file_diff['filename'] == path:
832 break
833 break
833 else:
834 else:
834 raise FileNotInDiffException("File {} not in diff".format(path))
835 raise FileNotInDiffException("File {} not in diff".format(path))
835 return file_diff
836 return file_diff
836
837
837 def _find_chunk_line_index(self, file_diff, diff_line):
838 def _find_chunk_line_index(self, file_diff, diff_line):
838 for chunk in file_diff['chunks']:
839 for chunk in file_diff['chunks']:
839 for idx, line in enumerate(chunk):
840 for idx, line in enumerate(chunk):
840 if line['old_lineno'] == diff_line.old:
841 if line['old_lineno'] == diff_line.old:
841 return chunk, idx
842 return chunk, idx
842 if line['new_lineno'] == diff_line.new:
843 if line['new_lineno'] == diff_line.new:
843 return chunk, idx
844 return chunk, idx
844 raise LineNotInDiffException(
845 raise LineNotInDiffException(
845 "The line {} is not part of the diff.".format(diff_line))
846 "The line {} is not part of the diff.".format(diff_line))
846
847
847
848
848 def _is_diff_content(line):
849 def _is_diff_content(line):
849 return line['action'] in (
850 return line['action'] in (
850 Action.UNMODIFIED, Action.ADD, Action.DELETE)
851 Action.UNMODIFIED, Action.ADD, Action.DELETE)
851
852
852
853
853 def _context_line(line):
854 def _context_line(line):
854 return (line['action'], line['line'])
855 return (line['action'], line['line'])
855
856
856
857
857 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
858 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
858
859
859
860
860 def _line_to_diff_line_number(line):
861 def _line_to_diff_line_number(line):
861 new_line_no = line['new_lineno'] or None
862 new_line_no = line['new_lineno'] or None
862 old_line_no = line['old_lineno'] or None
863 old_line_no = line['old_lineno'] or None
863 return DiffLineNumber(old=old_line_no, new=new_line_no)
864 return DiffLineNumber(old=old_line_no, new=new_line_no)
864
865
865
866
866 class FileNotInDiffException(Exception):
867 class FileNotInDiffException(Exception):
867 """
868 """
868 Raised when the context for a missing file is requested.
869 Raised when the context for a missing file is requested.
869
870
870 If you request the context for a line in a file which is not part of the
871 If you request the context for a line in a file which is not part of the
871 given diff, then this exception is raised.
872 given diff, then this exception is raised.
872 """
873 """
873
874
874
875
875 class LineNotInDiffException(Exception):
876 class LineNotInDiffException(Exception):
876 """
877 """
877 Raised when the context for a missing line is requested.
878 Raised when the context for a missing line is requested.
878
879
879 If you request the context for a line in a file and this line is not
880 If you request the context for a line in a file and this line is not
880 part of the given diff, then this exception is raised.
881 part of the given diff, then this exception is raised.
881 """
882 """
882
883
883
884
884 class DiffLimitExceeded(Exception):
885 class DiffLimitExceeded(Exception):
885 pass
886 pass
@@ -1,78 +1,90 b''
1 div.diffblock .code-header .changeset_header > div {
1 div.diffblock .code-header .changeset_header > div {
2 margin: 0 @padding;
2 margin: 0 @padding;
3 }
3 }
4
4
5
5
6 // Line select and comment
6 // Line select and comment
7 div.diffblock.margined.comm tr {
7 div.diffblock.margined.comm tr {
8 td {
8 td {
9 position: relative;
9 position: relative;
10 }
10 }
11
11
12 .add-comment-line {
12 .add-comment-line {
13 // Force td width for Firefox
13 // Force td width for Firefox
14 width: 20px;
14 width: 20px;
15
15
16 // TODO: anderson: fixing mouse-over bug.
16 // TODO: anderson: fixing mouse-over bug.
17 // why was it vertical-align baseline in first place??
17 // why was it vertical-align baseline in first place??
18 vertical-align: top !important;
18 vertical-align: top !important;
19 // Force width and display for IE 9
19 // Force width and display for IE 9
20 .add-comment-content {
20 .add-comment-content {
21 display: inline-block;
21 display: inline-block;
22 width: 20px;
22 width: 20px;
23
23
24 a {
24 a {
25 display: none;
25 display: none;
26 position: absolute;
26 position: absolute;
27 top: 2px;
27 top: 2px;
28 left: 2px;
28 left: 2px;
29 color: @grey3;
29 color: @grey3;
30 }
30 }
31 }
31 }
32 }
32 }
33
33
34 .comment-toggle {
35 display: inline-block;
36 visibility: hidden;
37 width: 20px;
38 color: @rcblue;
39
40 &.active {
41 visibility: visible;
42 cursor: pointer;
43 }
44 }
45
34 &.line {
46 &.line {
35 &:hover, &.hover{
47 &:hover, &.hover{
36 .add-comment-line a{
48 .add-comment-line a{
37 display: inline;
49 display: inline;
38 }
50 }
39 }
51 }
40
52
41 &.hover, &.selected {
53 &.hover, &.selected {
42 &, del, ins {
54 &, del, ins {
43 background-color: lighten(@alert3, 10%) !important;
55 background-color: lighten(@alert3, 10%) !important;
44 }
56 }
45 }
57 }
46
58
47 &.commenting {
59 &.commenting {
48 &, del, ins {
60 &, del, ins {
49 background-image: none !important;
61 background-image: none !important;
50 background-color: lighten(@alert4, 10%) !important;
62 background-color: lighten(@alert4, 10%) !important;
51 }
63 }
52 }
64 }
53 }
65 }
54 }
66 }
55
67
56 .compare-header {
68 .compare-header {
57 overflow-x: auto;
69 overflow-x: auto;
58 overflow-y: hidden;
70 overflow-y: hidden;
59 clear: both;
71 clear: both;
60 padding: @padding;
72 padding: @padding;
61 background: @grey6;
73 background: @grey6;
62 border: @border-thickness solid @border-default-color;
74 border: @border-thickness solid @border-default-color;
63 .border-radius(@border-radius);
75 .border-radius(@border-radius);
64
76
65 .compare-value,
77 .compare-value,
66 .compare-label {
78 .compare-label {
67 float: left;
79 float: left;
68 display: inline-block;
80 display: inline-block;
69 min-width: 5em;
81 min-width: 5em;
70 margin: 0;
82 margin: 0;
71 padding: 0.9em 0.9em 0.9em 0;
83 padding: 0.9em 0.9em 0.9em 0;
72 }
84 }
73
85
74 .compare-label {
86 .compare-label {
75 clear: both;
87 clear: both;
76 font-family: @text-semibold;
88 font-family: @text-semibold;
77 }
89 }
78 } No newline at end of file
90 }
@@ -1,658 +1,672 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 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 // returns a node from given html;
28 // returns a node from given html;
29 var fromHTML = function(html){
29 var fromHTML = function(html){
30 var _html = document.createElement('element');
30 var _html = document.createElement('element');
31 _html.innerHTML = html;
31 _html.innerHTML = html;
32 return _html;
32 return _html;
33 };
33 };
34
34
35 var tableTr = function(cls, body){
35 var tableTr = function(cls, body){
36 var _el = document.createElement('div');
36 var _el = document.createElement('div');
37 var _body = $(body).attr('id');
37 var _body = $(body).attr('id');
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 var id = 'comment-tr-{0}'.format(comment_id);
39 var id = 'comment-tr-{0}'.format(comment_id);
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 '<td class="add-comment-line"><span class="add-comment-content"></span></td>'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 '<td></td>'+
42 '<td></td>'+
43 '<td></td>'+
43 '<td></td>'+
44 '<td></td>'+
44 '<td>{2}</td>'+
45 '<td>{2}</td>'+
45 '</tr></tbody></table>').format(id, cls, body);
46 '</tr></tbody></table>').format(id, cls, body);
46 $(_el).html(_html);
47 $(_el).html(_html);
47 return _el.children[0].children[0].children[0];
48 return _el.children[0].children[0].children[0];
48 };
49 };
49
50
50 var removeInlineForm = function(form) {
51 var removeInlineForm = function(form) {
51 form.parentNode.removeChild(form);
52 form.parentNode.removeChild(form);
52 };
53 };
53
54
54 var createInlineForm = function(parent_tr, f_path, line) {
55 var createInlineForm = function(parent_tr, f_path, line) {
55 var tmpl = $('#comment-inline-form-template').html();
56 var tmpl = $('#comment-inline-form-template').html();
56 tmpl = tmpl.format(f_path, line);
57 tmpl = tmpl.format(f_path, line);
57 var form = tableTr('comment-form-inline', tmpl);
58 var form = tableTr('comment-form-inline', tmpl);
58 var form_hide_button = $(form).find('.hide-inline-form');
59 var form_hide_button = $(form).find('.hide-inline-form');
59
60
60 $(form_hide_button).click(function(e) {
61 $(form_hide_button).click(function(e) {
61 $('.inline-comments').removeClass('hide-comment-button');
62 $('.inline-comments').removeClass('hide-comment-button');
62 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
63 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
64 $(newtr.nextElementSibling).show();
65 $(newtr.nextElementSibling).show();
65 }
66 }
66 $(newtr).parents('.comment-form-inline').remove();
67 $(newtr).parents('.comment-form-inline').remove();
67 $(parent_tr).removeClass('form-open');
68 $(parent_tr).removeClass('form-open');
68 $(parent_tr).removeClass('hl-comment');
69 $(parent_tr).removeClass('hl-comment');
69 });
70 });
70
71
71 return form;
72 return form;
72 };
73 };
73
74
74 var getLineNo = function(tr) {
75 var getLineNo = function(tr) {
75 var line;
76 var line;
76 // Try to get the id and return "" (empty string) if it doesn't exist
77 // Try to get the id and return "" (empty string) if it doesn't exist
77 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
78 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
79 if (n.length >= 2) {
80 if (n.length >= 2) {
80 line = n[n.length-1];
81 line = n[n.length-1];
81 } else if (o.length >= 2) {
82 } else if (o.length >= 2) {
82 line = o[o.length-1];
83 line = o[o.length-1];
83 }
84 }
84 return line;
85 return line;
85 };
86 };
86
87
87 /**
88 /**
88 * make a single inline comment and place it inside
89 * make a single inline comment and place it inside
89 */
90 */
90 var renderInlineComment = function(json_data, show_add_button) {
91 var renderInlineComment = function(json_data, show_add_button) {
91 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
92 try {
93 try {
93 var html = json_data.rendered_text;
94 var html = json_data.rendered_text;
94 var lineno = json_data.line_no;
95 var lineno = json_data.line_no;
95 var target_id = json_data.target_id;
96 var target_id = json_data.target_id;
96 placeInline(target_id, lineno, html, show_add_button);
97 placeInline(target_id, lineno, html, show_add_button);
97 } catch (e) {
98 } catch (e) {
98 console.error(e);
99 console.error(e);
99 }
100 }
100 };
101 };
101
102
102 function bindDeleteCommentButtons() {
103 function bindDeleteCommentButtons() {
103 $('.delete-comment').one('click', function() {
104 $('.delete-comment').one('click', function() {
104 var comment_id = $(this).data("comment-id");
105 var comment_id = $(this).data("comment-id");
105
106
106 if (comment_id){
107 if (comment_id){
107 deleteComment(comment_id);
108 deleteComment(comment_id);
108 }
109 }
109 });
110 });
110 }
111 }
111
112
112 /**
113 /**
113 * Inject inline comment for on given TR this tr should be always an .line
114 * Inject inline comment for on given TR this tr should be always an .line
114 * tr containing the line. Code will detect comment, and always put the comment
115 * tr containing the line. Code will detect comment, and always put the comment
115 * block at the very bottom
116 * block at the very bottom
116 */
117 */
117 var injectInlineForm = function(tr){
118 var injectInlineForm = function(tr){
118 if (!$(tr).hasClass('line')) {
119 if (!$(tr).hasClass('line')) {
119 return;
120 return;
120 }
121 }
121
122
122 var _td = $(tr).find('.code').get(0);
123 var _td = $(tr).find('.code').get(0);
123 if ($(tr).hasClass('form-open') ||
124 if ($(tr).hasClass('form-open') ||
124 $(tr).hasClass('context') ||
125 $(tr).hasClass('context') ||
125 $(_td).hasClass('no-comment')) {
126 $(_td).hasClass('no-comment')) {
126 return;
127 return;
127 }
128 }
128 $(tr).addClass('form-open');
129 $(tr).addClass('form-open');
129 $(tr).addClass('hl-comment');
130 $(tr).addClass('hl-comment');
130 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
131 var f_path = $(node).attr('path');
132 var f_path = $(node).attr('path');
132 var lineno = getLineNo(tr);
133 var lineno = getLineNo(tr);
133 var form = createInlineForm(tr, f_path, lineno);
134 var form = createInlineForm(tr, f_path, lineno);
134
135
135 var parent = tr;
136 var parent = tr;
136 while (1) {
137 while (1) {
137 var n = parent.nextElementSibling;
138 var n = parent.nextElementSibling;
138 // next element are comments !
139 // next element are comments !
139 if ($(n).hasClass('inline-comments')) {
140 if ($(n).hasClass('inline-comments')) {
140 parent = n;
141 parent = n;
141 }
142 }
142 else {
143 else {
143 break;
144 break;
144 }
145 }
145 }
146 }
146 var _parent = $(parent).get(0);
147 var _parent = $(parent).get(0);
147 $(_parent).after(form);
148 $(_parent).after(form);
148 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
149 var f = $(form).get(0);
150 var f = $(form).get(0);
150
151
151 var _form = $(f).find('.inline-form').get(0);
152 var _form = $(f).find('.inline-form').get(0);
152
153
153 var pullRequestId = templateContext.pull_request_data.pull_request_id;
154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
154 var commitId = templateContext.commit_data.commit_id;
155 var commitId = templateContext.commit_data.commit_id;
155
156
156 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
157 var cm = commentForm.getCmInstance();
158 var cm = commentForm.getCmInstance();
158
159
159 // set a CUSTOM submit handler for inline comments.
160 // set a CUSTOM submit handler for inline comments.
160 commentForm.setHandleFormSubmit(function(o) {
161 commentForm.setHandleFormSubmit(function(o) {
161 var text = commentForm.cm.getValue();
162 var text = commentForm.cm.getValue();
162
163
163 if (text === "") {
164 if (text === "") {
164 return;
165 return;
165 }
166 }
166
167
167 if (lineno === undefined) {
168 if (lineno === undefined) {
168 alert('missing line !');
169 alert('missing line !');
169 return;
170 return;
170 }
171 }
171 if (f_path === undefined) {
172 if (f_path === undefined) {
172 alert('missing file path !');
173 alert('missing file path !');
173 return;
174 return;
174 }
175 }
175
176
176 var excludeCancelBtn = false;
177 var excludeCancelBtn = false;
177 var submitEvent = true;
178 var submitEvent = true;
178 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
179 commentForm.cm.setOption("readOnly", true);
180 commentForm.cm.setOption("readOnly", true);
180 var postData = {
181 var postData = {
181 'text': text,
182 'text': text,
182 'f_path': f_path,
183 'f_path': f_path,
183 'line': lineno,
184 'line': lineno,
184 'csrf_token': CSRF_TOKEN
185 'csrf_token': CSRF_TOKEN
185 };
186 };
186 var submitSuccessCallback = function(o) {
187 var submitSuccessCallback = function(o) {
187 $(tr).removeClass('form-open');
188 $(tr).removeClass('form-open');
188 removeInlineForm(f);
189 removeInlineForm(f);
189 renderInlineComment(o);
190 renderInlineComment(o);
190 $('.inline-comments').removeClass('hide-comment-button');
191 $('.inline-comments').removeClass('hide-comment-button');
191
192
192 // re trigger the linkification of next/prev navigation
193 // re trigger the linkification of next/prev navigation
193 linkifyComments($('.inline-comment-injected'));
194 linkifyComments($('.inline-comment-injected'));
194 timeagoActivate();
195 timeagoActivate();
195 bindDeleteCommentButtons();
196 bindDeleteCommentButtons();
196 commentForm.setActionButtonsDisabled(false);
197 commentForm.setActionButtonsDisabled(false);
197
198
198 };
199 };
199 var submitFailCallback = function(){
200 var submitFailCallback = function(){
200 commentForm.resetCommentFormState(text)
201 commentForm.resetCommentFormState(text)
201 };
202 };
202 commentForm.submitAjaxPOST(
203 commentForm.submitAjaxPOST(
203 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
204 });
205 });
205
206
206 setTimeout(function() {
207 setTimeout(function() {
207 // callbacks
208 // callbacks
208 if (cm !== undefined) {
209 if (cm !== undefined) {
209 cm.focus();
210 cm.focus();
210 }
211 }
211 }, 10);
212 }, 10);
212
213
213 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
214 form:_form,
215 form:_form,
215 parent:_parent,
216 parent:_parent,
216 lineno: lineno,
217 lineno: lineno,
217 f_path: f_path}
218 f_path: f_path}
218 );
219 );
219 };
220 };
220
221
221 var deleteComment = function(comment_id) {
222 var deleteComment = function(comment_id) {
222 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
223 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
223 var postData = {
224 var postData = {
224 '_method': 'delete',
225 '_method': 'delete',
225 'csrf_token': CSRF_TOKEN
226 'csrf_token': CSRF_TOKEN
226 };
227 };
227
228
228 var success = function(o) {
229 var success = function(o) {
229 window.location.reload();
230 window.location.reload();
230 };
231 };
231 ajaxPOST(url, postData, success);
232 ajaxPOST(url, postData, success);
232 };
233 };
233
234
234 var createInlineAddButton = function(tr){
235 var createInlineAddButton = function(tr){
235 var label = _gettext('Add another comment');
236 var label = _gettext('Add another comment');
236 var html_el = document.createElement('div');
237 var html_el = document.createElement('div');
237 $(html_el).addClass('add-comment');
238 $(html_el).addClass('add-comment');
238 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
239 var add = new $(html_el);
240 var add = new $(html_el);
240 add.on('click', function(e) {
241 add.on('click', function(e) {
241 injectInlineForm(tr);
242 injectInlineForm(tr);
242 });
243 });
243 return add;
244 return add;
244 };
245 };
245
246
246 var placeAddButton = function(target_tr){
247 var placeAddButton = function(target_tr){
247 if(!target_tr){
248 if(!target_tr){
248 return;
249 return;
249 }
250 }
250 var last_node = target_tr;
251 var last_node = target_tr;
251 // scan
252 // scan
252 while (1){
253 while (1){
253 var n = last_node.nextElementSibling;
254 var n = last_node.nextElementSibling;
254 // next element are comments !
255 // next element are comments !
255 if($(n).hasClass('inline-comments')){
256 if($(n).hasClass('inline-comments')){
256 last_node = n;
257 last_node = n;
257 // also remove the comment button from previous
258 // also remove the comment button from previous
258 var comment_add_buttons = $(last_node).find('.add-comment');
259 var comment_add_buttons = $(last_node).find('.add-comment');
259 for(var i=0; i<comment_add_buttons.length; i++){
260 for(var i=0; i<comment_add_buttons.length; i++){
260 var b = comment_add_buttons[i];
261 var b = comment_add_buttons[i];
261 b.parentNode.removeChild(b);
262 b.parentNode.removeChild(b);
262 }
263 }
263 }
264 }
264 else{
265 else{
265 break;
266 break;
266 }
267 }
267 }
268 }
268 var add = createInlineAddButton(target_tr);
269 var add = createInlineAddButton(target_tr);
269 // get the comment div
270 // get the comment div
270 var comment_block = $(last_node).find('.comment')[0];
271 var comment_block = $(last_node).find('.comment')[0];
271 // attach add button
272 // attach add button
272 $(add).insertAfter(comment_block);
273 $(add).insertAfter(comment_block);
273 };
274 };
274
275
275 /**
276 /**
276 * Places the inline comment into the changeset block in proper line position
277 * Places the inline comment into the changeset block in proper line position
277 */
278 */
278 var placeInline = function(target_container, lineno, html, show_add_button) {
279 var placeInline = function(target_container, lineno, html, show_add_button) {
279 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
280
281
281 var lineid = "{0}_{1}".format(target_container, lineno);
282 var lineid = "{0}_{1}".format(target_container, lineno);
282 var target_line = $('#' + lineid).get(0);
283 var target_line = $('#' + lineid).get(0);
283 var comment = new $(tableTr('inline-comments', html));
284 var comment = new $(tableTr('inline-comments', html));
284 // check if there are comments already !
285 // check if there are comments already !
285 if (target_line) {
286 if (target_line) {
286 var parent_node = target_line.parentNode;
287 var parent_node = target_line.parentNode;
287 var root_parent = parent_node;
288 var root_parent = parent_node;
288
289
289 while (1) {
290 while (1) {
290 var n = parent_node.nextElementSibling;
291 var n = parent_node.nextElementSibling;
291 // next element are comments !
292 // next element are comments !
292 if ($(n).hasClass('inline-comments')) {
293 if ($(n).hasClass('inline-comments')) {
293 parent_node = n;
294 parent_node = n;
294 }
295 }
295 else {
296 else {
296 break;
297 break;
297 }
298 }
298 }
299 }
299 // put in the comment at the bottom
300 // put in the comment at the bottom
300 $(comment).insertAfter(parent_node);
301 $(comment).insertAfter(parent_node);
301 $(comment).find('.comment-inline').addClass('inline-comment-injected');
302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
302 // scan nodes, and attach add button to last one
303 // scan nodes, and attach add button to last one
303 if (show_add_button) {
304 if (show_add_button) {
304 placeAddButton(root_parent);
305 placeAddButton(root_parent);
305 }
306 }
307 addCommentToggle(target_line);
306 }
308 }
307
309
308 return target_line;
310 return target_line;
309 };
311 };
310
312
313 var addCommentToggle = function(target_line) {
314 // exposes comment toggle button
315 $(target_line).siblings('.comment-toggle').addClass('active');
316 return;
317 };
318
319 var bindToggleButtons = function() {
320 $('.comment-toggle').on('click', function() {
321 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
322 });
323 };
324
311 var linkifyComments = function(comments) {
325 var linkifyComments = function(comments) {
312
326
313 for (var i = 0; i < comments.length; i++) {
327 for (var i = 0; i < comments.length; i++) {
314 var comment_id = $(comments[i]).data('comment-id');
328 var comment_id = $(comments[i]).data('comment-id');
315 var prev_comment_id = $(comments[i - 1]).data('comment-id');
329 var prev_comment_id = $(comments[i - 1]).data('comment-id');
316 var next_comment_id = $(comments[i + 1]).data('comment-id');
330 var next_comment_id = $(comments[i + 1]).data('comment-id');
317
331
318 // place next/prev links
332 // place next/prev links
319 if (prev_comment_id) {
333 if (prev_comment_id) {
320 $('#prev_c_' + comment_id).show();
334 $('#prev_c_' + comment_id).show();
321 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
335 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
322 'href', '#comment-' + prev_comment_id).removeClass('disabled');
336 'href', '#comment-' + prev_comment_id).removeClass('disabled');
323 }
337 }
324 if (next_comment_id) {
338 if (next_comment_id) {
325 $('#next_c_' + comment_id).show();
339 $('#next_c_' + comment_id).show();
326 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
340 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
327 'href', '#comment-' + next_comment_id).removeClass('disabled');
341 'href', '#comment-' + next_comment_id).removeClass('disabled');
328 }
342 }
329 // place a first link to the total counter
343 // place a first link to the total counter
330 if (i === 0) {
344 if (i === 0) {
331 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
345 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
332 }
346 }
333 }
347 }
334
348
335 };
349 };
336
350
337 /**
351 /**
338 * Iterates over all the inlines, and places them inside proper blocks of data
352 * Iterates over all the inlines, and places them inside proper blocks of data
339 */
353 */
340 var renderInlineComments = function(file_comments, show_add_button) {
354 var renderInlineComments = function(file_comments, show_add_button) {
341 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
342
356
343 for (var i = 0; i < file_comments.length; i++) {
357 for (var i = 0; i < file_comments.length; i++) {
344 var box = file_comments[i];
358 var box = file_comments[i];
345
359
346 var target_id = $(box).attr('target_id');
360 var target_id = $(box).attr('target_id');
347
361
348 // actually comments with line numbers
362 // actually comments with line numbers
349 var comments = box.children;
363 var comments = box.children;
350
364
351 for (var j = 0; j < comments.length; j++) {
365 for (var j = 0; j < comments.length; j++) {
352 var data = {
366 var data = {
353 'rendered_text': comments[j].outerHTML,
367 'rendered_text': comments[j].outerHTML,
354 'line_no': $(comments[j]).attr('line'),
368 'line_no': $(comments[j]).attr('line'),
355 'target_id': target_id
369 'target_id': target_id
356 };
370 };
357 renderInlineComment(data, show_add_button);
371 renderInlineComment(data, show_add_button);
358 }
372 }
359 }
373 }
360
374
361 // since order of injection is random, we're now re-iterating
375 // since order of injection is random, we're now re-iterating
362 // from correct order and filling in links
376 // from correct order and filling in links
363 linkifyComments($('.inline-comment-injected'));
377 linkifyComments($('.inline-comment-injected'));
364 bindDeleteCommentButtons();
378 bindDeleteCommentButtons();
365 firefoxAnchorFix();
379 firefoxAnchorFix();
366 };
380 };
367
381
368
382
369 /* Comment form for main and inline comments */
383 /* Comment form for main and inline comments */
370 var CommentForm = (function() {
384 var CommentForm = (function() {
371 "use strict";
385 "use strict";
372
386
373 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
387 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
374
388
375 this.withLineNo = function(selector) {
389 this.withLineNo = function(selector) {
376 var lineNo = this.lineNo;
390 var lineNo = this.lineNo;
377 if (lineNo === undefined) {
391 if (lineNo === undefined) {
378 return selector
392 return selector
379 } else {
393 } else {
380 return selector + '_' + lineNo;
394 return selector + '_' + lineNo;
381 }
395 }
382 };
396 };
383
397
384 this.commitId = commitId;
398 this.commitId = commitId;
385 this.pullRequestId = pullRequestId;
399 this.pullRequestId = pullRequestId;
386 this.lineNo = lineNo;
400 this.lineNo = lineNo;
387 this.initAutocompleteActions = initAutocompleteActions;
401 this.initAutocompleteActions = initAutocompleteActions;
388
402
389 this.previewButton = this.withLineNo('#preview-btn');
403 this.previewButton = this.withLineNo('#preview-btn');
390 this.previewContainer = this.withLineNo('#preview-container');
404 this.previewContainer = this.withLineNo('#preview-container');
391
405
392 this.previewBoxSelector = this.withLineNo('#preview-box');
406 this.previewBoxSelector = this.withLineNo('#preview-box');
393
407
394 this.editButton = this.withLineNo('#edit-btn');
408 this.editButton = this.withLineNo('#edit-btn');
395 this.editContainer = this.withLineNo('#edit-container');
409 this.editContainer = this.withLineNo('#edit-container');
396
410
397 this.cancelButton = this.withLineNo('#cancel-btn');
411 this.cancelButton = this.withLineNo('#cancel-btn');
398
412
399 this.statusChange = '#change_status';
413 this.statusChange = '#change_status';
400 this.cmBox = this.withLineNo('#text');
414 this.cmBox = this.withLineNo('#text');
401 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
415 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
402
416
403 this.submitForm = formElement;
417 this.submitForm = formElement;
404 this.submitButton = $(this.submitForm).find('input[type="submit"]');
418 this.submitButton = $(this.submitForm).find('input[type="submit"]');
405 this.submitButtonText = this.submitButton.val();
419 this.submitButtonText = this.submitButton.val();
406
420
407 this.previewUrl = pyroutes.url('changeset_comment_preview',
421 this.previewUrl = pyroutes.url('changeset_comment_preview',
408 {'repo_name': templateContext.repo_name});
422 {'repo_name': templateContext.repo_name});
409
423
410 // based on commitId, or pullReuqestId decide where do we submit
424 // based on commitId, or pullReuqestId decide where do we submit
411 // out data
425 // out data
412 if (this.commitId){
426 if (this.commitId){
413 this.submitUrl = pyroutes.url('changeset_comment',
427 this.submitUrl = pyroutes.url('changeset_comment',
414 {'repo_name': templateContext.repo_name,
428 {'repo_name': templateContext.repo_name,
415 'revision': this.commitId});
429 'revision': this.commitId});
416
430
417 } else if (this.pullRequestId) {
431 } else if (this.pullRequestId) {
418 this.submitUrl = pyroutes.url('pullrequest_comment',
432 this.submitUrl = pyroutes.url('pullrequest_comment',
419 {'repo_name': templateContext.repo_name,
433 {'repo_name': templateContext.repo_name,
420 'pull_request_id': this.pullRequestId});
434 'pull_request_id': this.pullRequestId});
421
435
422 } else {
436 } else {
423 throw new Error(
437 throw new Error(
424 'CommentForm requires pullRequestId, or commitId to be specified.')
438 'CommentForm requires pullRequestId, or commitId to be specified.')
425 }
439 }
426
440
427 this.getCmInstance = function(){
441 this.getCmInstance = function(){
428 return this.cm
442 return this.cm
429 };
443 };
430
444
431 var self = this;
445 var self = this;
432
446
433 this.getCommentStatus = function() {
447 this.getCommentStatus = function() {
434 return $(this.submitForm).find(this.statusChange).val();
448 return $(this.submitForm).find(this.statusChange).val();
435 };
449 };
436
450
437 this.isAllowedToSubmit = function() {
451 this.isAllowedToSubmit = function() {
438 return !$(this.submitButton).prop('disabled');
452 return !$(this.submitButton).prop('disabled');
439 };
453 };
440
454
441 this.initStatusChangeSelector = function(){
455 this.initStatusChangeSelector = function(){
442 var formatChangeStatus = function(state, escapeMarkup) {
456 var formatChangeStatus = function(state, escapeMarkup) {
443 var originalOption = state.element;
457 var originalOption = state.element;
444 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
458 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
445 '<span>' + escapeMarkup(state.text) + '</span>';
459 '<span>' + escapeMarkup(state.text) + '</span>';
446 };
460 };
447 var formatResult = function(result, container, query, escapeMarkup) {
461 var formatResult = function(result, container, query, escapeMarkup) {
448 return formatChangeStatus(result, escapeMarkup);
462 return formatChangeStatus(result, escapeMarkup);
449 };
463 };
450
464
451 var formatSelection = function(data, container, escapeMarkup) {
465 var formatSelection = function(data, container, escapeMarkup) {
452 return formatChangeStatus(data, escapeMarkup);
466 return formatChangeStatus(data, escapeMarkup);
453 };
467 };
454
468
455 $(this.submitForm).find(this.statusChange).select2({
469 $(this.submitForm).find(this.statusChange).select2({
456 placeholder: _gettext('Status Review'),
470 placeholder: _gettext('Status Review'),
457 formatResult: formatResult,
471 formatResult: formatResult,
458 formatSelection: formatSelection,
472 formatSelection: formatSelection,
459 containerCssClass: "drop-menu status_box_menu",
473 containerCssClass: "drop-menu status_box_menu",
460 dropdownCssClass: "drop-menu-dropdown",
474 dropdownCssClass: "drop-menu-dropdown",
461 dropdownAutoWidth: true,
475 dropdownAutoWidth: true,
462 minimumResultsForSearch: -1
476 minimumResultsForSearch: -1
463 });
477 });
464 $(this.submitForm).find(this.statusChange).on('change', function() {
478 $(this.submitForm).find(this.statusChange).on('change', function() {
465 var status = self.getCommentStatus();
479 var status = self.getCommentStatus();
466 if (status && !self.lineNo) {
480 if (status && !self.lineNo) {
467 $(self.submitButton).prop('disabled', false);
481 $(self.submitButton).prop('disabled', false);
468 }
482 }
469 //todo, fix this name
483 //todo, fix this name
470 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
484 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
471 self.cm.setOption('placeholder', placeholderText);
485 self.cm.setOption('placeholder', placeholderText);
472 })
486 })
473 };
487 };
474
488
475 // reset the comment form into it's original state
489 // reset the comment form into it's original state
476 this.resetCommentFormState = function(content) {
490 this.resetCommentFormState = function(content) {
477 content = content || '';
491 content = content || '';
478
492
479 $(this.editContainer).show();
493 $(this.editContainer).show();
480 $(this.editButton).hide();
494 $(this.editButton).hide();
481
495
482 $(this.previewContainer).hide();
496 $(this.previewContainer).hide();
483 $(this.previewButton).show();
497 $(this.previewButton).show();
484
498
485 this.setActionButtonsDisabled(true);
499 this.setActionButtonsDisabled(true);
486 self.cm.setValue(content);
500 self.cm.setValue(content);
487 self.cm.setOption("readOnly", false);
501 self.cm.setOption("readOnly", false);
488 };
502 };
489
503
490 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
504 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
491 failHandler = failHandler || function() {};
505 failHandler = failHandler || function() {};
492 var postData = toQueryString(postData);
506 var postData = toQueryString(postData);
493 var request = $.ajax({
507 var request = $.ajax({
494 url: url,
508 url: url,
495 type: 'POST',
509 type: 'POST',
496 data: postData,
510 data: postData,
497 headers: {'X-PARTIAL-XHR': true}
511 headers: {'X-PARTIAL-XHR': true}
498 })
512 })
499 .done(function(data) {
513 .done(function(data) {
500 successHandler(data);
514 successHandler(data);
501 })
515 })
502 .fail(function(data, textStatus, errorThrown){
516 .fail(function(data, textStatus, errorThrown){
503 alert(
517 alert(
504 "Error while submitting comment.\n" +
518 "Error while submitting comment.\n" +
505 "Error code {0} ({1}).".format(data.status, data.statusText));
519 "Error code {0} ({1}).".format(data.status, data.statusText));
506 failHandler()
520 failHandler()
507 });
521 });
508 return request;
522 return request;
509 };
523 };
510
524
511 // overwrite a submitHandler, we need to do it for inline comments
525 // overwrite a submitHandler, we need to do it for inline comments
512 this.setHandleFormSubmit = function(callback) {
526 this.setHandleFormSubmit = function(callback) {
513 this.handleFormSubmit = callback;
527 this.handleFormSubmit = callback;
514 };
528 };
515
529
516 // default handler for for submit for main comments
530 // default handler for for submit for main comments
517 this.handleFormSubmit = function() {
531 this.handleFormSubmit = function() {
518 var text = self.cm.getValue();
532 var text = self.cm.getValue();
519 var status = self.getCommentStatus();
533 var status = self.getCommentStatus();
520
534
521 if (text === "" && !status) {
535 if (text === "" && !status) {
522 return;
536 return;
523 }
537 }
524
538
525 var excludeCancelBtn = false;
539 var excludeCancelBtn = false;
526 var submitEvent = true;
540 var submitEvent = true;
527 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
541 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
528 self.cm.setOption("readOnly", true);
542 self.cm.setOption("readOnly", true);
529 var postData = {
543 var postData = {
530 'text': text,
544 'text': text,
531 'changeset_status': status,
545 'changeset_status': status,
532 'csrf_token': CSRF_TOKEN
546 'csrf_token': CSRF_TOKEN
533 };
547 };
534
548
535 var submitSuccessCallback = function(o) {
549 var submitSuccessCallback = function(o) {
536 if (status) {
550 if (status) {
537 location.reload(true);
551 location.reload(true);
538 } else {
552 } else {
539 $('#injected_page_comments').append(o.rendered_text);
553 $('#injected_page_comments').append(o.rendered_text);
540 self.resetCommentFormState();
554 self.resetCommentFormState();
541 bindDeleteCommentButtons();
555 bindDeleteCommentButtons();
542 timeagoActivate();
556 timeagoActivate();
543 }
557 }
544 };
558 };
545 var submitFailCallback = function(){
559 var submitFailCallback = function(){
546 self.resetCommentFormState(text)
560 self.resetCommentFormState(text)
547 };
561 };
548 self.submitAjaxPOST(
562 self.submitAjaxPOST(
549 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
563 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
550 };
564 };
551
565
552 this.previewSuccessCallback = function(o) {
566 this.previewSuccessCallback = function(o) {
553 $(self.previewBoxSelector).html(o);
567 $(self.previewBoxSelector).html(o);
554 $(self.previewBoxSelector).removeClass('unloaded');
568 $(self.previewBoxSelector).removeClass('unloaded');
555
569
556 // swap buttons
570 // swap buttons
557 $(self.previewButton).hide();
571 $(self.previewButton).hide();
558 $(self.editButton).show();
572 $(self.editButton).show();
559
573
560 // unlock buttons
574 // unlock buttons
561 self.setActionButtonsDisabled(false);
575 self.setActionButtonsDisabled(false);
562 };
576 };
563
577
564 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
578 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
565 excludeCancelBtn = excludeCancelBtn || false;
579 excludeCancelBtn = excludeCancelBtn || false;
566 submitEvent = submitEvent || false;
580 submitEvent = submitEvent || false;
567
581
568 $(this.editButton).prop('disabled', state);
582 $(this.editButton).prop('disabled', state);
569 $(this.previewButton).prop('disabled', state);
583 $(this.previewButton).prop('disabled', state);
570
584
571 if (!excludeCancelBtn) {
585 if (!excludeCancelBtn) {
572 $(this.cancelButton).prop('disabled', state);
586 $(this.cancelButton).prop('disabled', state);
573 }
587 }
574
588
575 var submitState = state;
589 var submitState = state;
576 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
590 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
577 // if the value of commit review status is set, we allow
591 // if the value of commit review status is set, we allow
578 // submit button, but only on Main form, lineNo means inline
592 // submit button, but only on Main form, lineNo means inline
579 submitState = false
593 submitState = false
580 }
594 }
581 $(this.submitButton).prop('disabled', submitState);
595 $(this.submitButton).prop('disabled', submitState);
582 if (submitEvent) {
596 if (submitEvent) {
583 $(this.submitButton).val(_gettext('Submitting...'));
597 $(this.submitButton).val(_gettext('Submitting...'));
584 } else {
598 } else {
585 $(this.submitButton).val(this.submitButtonText);
599 $(this.submitButton).val(this.submitButtonText);
586 }
600 }
587
601
588 };
602 };
589
603
590 // lock preview/edit/submit buttons on load, but exclude cancel button
604 // lock preview/edit/submit buttons on load, but exclude cancel button
591 var excludeCancelBtn = true;
605 var excludeCancelBtn = true;
592 this.setActionButtonsDisabled(true, excludeCancelBtn);
606 this.setActionButtonsDisabled(true, excludeCancelBtn);
593
607
594 // anonymous users don't have access to initialized CM instance
608 // anonymous users don't have access to initialized CM instance
595 if (this.cm !== undefined){
609 if (this.cm !== undefined){
596 this.cm.on('change', function(cMirror) {
610 this.cm.on('change', function(cMirror) {
597 if (cMirror.getValue() === "") {
611 if (cMirror.getValue() === "") {
598 self.setActionButtonsDisabled(true, excludeCancelBtn)
612 self.setActionButtonsDisabled(true, excludeCancelBtn)
599 } else {
613 } else {
600 self.setActionButtonsDisabled(false, excludeCancelBtn)
614 self.setActionButtonsDisabled(false, excludeCancelBtn)
601 }
615 }
602 });
616 });
603 }
617 }
604
618
605 $(this.editButton).on('click', function(e) {
619 $(this.editButton).on('click', function(e) {
606 e.preventDefault();
620 e.preventDefault();
607
621
608 $(self.previewButton).show();
622 $(self.previewButton).show();
609 $(self.previewContainer).hide();
623 $(self.previewContainer).hide();
610 $(self.editButton).hide();
624 $(self.editButton).hide();
611 $(self.editContainer).show();
625 $(self.editContainer).show();
612
626
613 });
627 });
614
628
615 $(this.previewButton).on('click', function(e) {
629 $(this.previewButton).on('click', function(e) {
616 e.preventDefault();
630 e.preventDefault();
617 var text = self.cm.getValue();
631 var text = self.cm.getValue();
618
632
619 if (text === "") {
633 if (text === "") {
620 return;
634 return;
621 }
635 }
622
636
623 var postData = {
637 var postData = {
624 'text': text,
638 'text': text,
625 'renderer': DEFAULT_RENDERER,
639 'renderer': DEFAULT_RENDERER,
626 'csrf_token': CSRF_TOKEN
640 'csrf_token': CSRF_TOKEN
627 };
641 };
628
642
629 // lock ALL buttons on preview
643 // lock ALL buttons on preview
630 self.setActionButtonsDisabled(true);
644 self.setActionButtonsDisabled(true);
631
645
632 $(self.previewBoxSelector).addClass('unloaded');
646 $(self.previewBoxSelector).addClass('unloaded');
633 $(self.previewBoxSelector).html(_gettext('Loading ...'));
647 $(self.previewBoxSelector).html(_gettext('Loading ...'));
634 $(self.editContainer).hide();
648 $(self.editContainer).hide();
635 $(self.previewContainer).show();
649 $(self.previewContainer).show();
636
650
637 // by default we reset state of comment preserving the text
651 // by default we reset state of comment preserving the text
638 var previewFailCallback = function(){
652 var previewFailCallback = function(){
639 self.resetCommentFormState(text)
653 self.resetCommentFormState(text)
640 };
654 };
641 self.submitAjaxPOST(
655 self.submitAjaxPOST(
642 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
656 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
643
657
644 });
658 });
645
659
646 $(this.submitForm).submit(function(e) {
660 $(this.submitForm).submit(function(e) {
647 e.preventDefault();
661 e.preventDefault();
648 var allowedToSubmit = self.isAllowedToSubmit();
662 var allowedToSubmit = self.isAllowedToSubmit();
649 if (!allowedToSubmit){
663 if (!allowedToSubmit){
650 return false;
664 return false;
651 }
665 }
652 self.handleFormSubmit();
666 self.handleFormSubmit();
653 });
667 });
654
668
655 }
669 }
656
670
657 return CommentForm;
671 return CommentForm;
658 })();
672 })();
@@ -1,312 +1,312 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.html"/>
6 <%namespace name="base" file="/base/base.html"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <div class="comment ${'comment-inline' if inline else ''}" id="comment-${comment.comment_id}" line="${comment.line_no}" data-comment-id="${comment.comment_id}">
9 <div class="comment ${'comment-inline' if inline else ''}" id="comment-${comment.comment_id}" line="${comment.line_no}" data-comment-id="${comment.comment_id}">
10 <div class="meta">
10 <div class="meta">
11 <div class="author">
11 <div class="author">
12 ${base.gravatar_with_user(comment.author.email, 16)}
12 ${base.gravatar_with_user(comment.author.email, 16)}
13 </div>
13 </div>
14 <div class="date">
14 <div class="date">
15 ${h.age_component(comment.modified_at, time_is_local=True)}
15 ${h.age_component(comment.modified_at, time_is_local=True)}
16 </div>
16 </div>
17 <div class="status-change">
17 <div class="status-change">
18 %if comment.pull_request:
18 %if comment.pull_request:
19 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
19 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
20 %if comment.status_change:
20 %if comment.status_change:
21 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
21 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
22 %else:
22 %else:
23 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
23 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
24 %endif
24 %endif
25 </a>
25 </a>
26 %else:
26 %else:
27 %if comment.status_change:
27 %if comment.status_change:
28 ${_('Status change on commit')}:
28 ${_('Status change on commit')}:
29 %else:
29 %else:
30 ${_('Comment on commit')}
30 ${_('Comment on commit')}
31 %endif
31 %endif
32 %endif
32 %endif
33 </div>
33 </div>
34 %if comment.status_change:
34 %if comment.status_change:
35 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
35 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
36 <div title="${_('Commit status')}" class="changeset-status-lbl">
36 <div title="${_('Commit status')}" class="changeset-status-lbl">
37 ${comment.status_change[0].status_lbl}
37 ${comment.status_change[0].status_lbl}
38 </div>
38 </div>
39 %endif
39 %endif
40 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
40 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
41
41
42
42
43 <div class="comment-links-block">
43 <div class="comment-links-block">
44
44
45 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
45 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
46 ## only super-admin, repo admin OR comment owner can delete
46 ## only super-admin, repo admin OR comment owner can delete
47 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
47 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
48 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
48 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
49 <div onClick="deleteComment(${comment.comment_id})" class="delete-comment"> ${_('Delete')}</div>
49 <div onClick="deleteComment(${comment.comment_id})" class="delete-comment"> ${_('Delete')}</div>
50 %if inline:
50 %if inline:
51 <div class="comment-links-divider"> | </div>
51 <div class="comment-links-divider"> | </div>
52 %endif
52 %endif
53 %endif
53 %endif
54 %endif
54 %endif
55
55
56 %if inline:
56 %if inline:
57
57
58 <div id="prev_c_${comment.comment_id}" class="comment-previous-link" title="${_('Previous comment')}">
58 <div id="prev_c_${comment.comment_id}" class="comment-previous-link" title="${_('Previous comment')}">
59 <a class="arrow_comment_link disabled"><i class="icon-left"></i></a>
59 <a class="arrow_comment_link disabled"><i class="icon-left"></i></a>
60 </div>
60 </div>
61
61
62 <div id="next_c_${comment.comment_id}" class="comment-next-link" title="${_('Next comment')}">
62 <div id="next_c_${comment.comment_id}" class="comment-next-link" title="${_('Next comment')}">
63 <a class="arrow_comment_link disabled"><i class="icon-right"></i></a>
63 <a class="arrow_comment_link disabled"><i class="icon-right"></i></a>
64 </div>
64 </div>
65 %endif
65 %endif
66
66
67 </div>
67 </div>
68 </div>
68 </div>
69 <div class="text">
69 <div class="text">
70 ${comment.render(mentions=True)|n}
70 ${comment.render(mentions=True)|n}
71 </div>
71 </div>
72 </div>
72 </div>
73 </%def>
73 </%def>
74
74
75 <%def name="comment_block_outdated(comment)">
75 <%def name="comment_block_outdated(comment)">
76 <div class="comments" id="comment-${comment.comment_id}">
76 <div class="comments" id="comment-${comment.comment_id}">
77 <div class="comment comment-wrapp">
77 <div class="comment comment-wrapp">
78 <div class="meta">
78 <div class="meta">
79 <div class="author">
79 <div class="author">
80 ${base.gravatar_with_user(comment.author.email, 16)}
80 ${base.gravatar_with_user(comment.author.email, 16)}
81 </div>
81 </div>
82 <div class="date">
82 <div class="date">
83 ${h.age_component(comment.modified_at, time_is_local=True)}
83 ${h.age_component(comment.modified_at, time_is_local=True)}
84 </div>
84 </div>
85 %if comment.status_change:
85 %if comment.status_change:
86 <span class="changeset-status-container">
86 <span class="changeset-status-container">
87 <span class="changeset-status-ico">
87 <span class="changeset-status-ico">
88 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
88 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
89 </span>
89 </span>
90 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
90 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
91 </span>
91 </span>
92 %endif
92 %endif
93 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
93 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
94 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
94 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
95 ## only super-admin, repo admin OR comment owner can delete
95 ## only super-admin, repo admin OR comment owner can delete
96 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
96 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
97 <div class="comment-links-block">
97 <div class="comment-links-block">
98 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
98 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
99 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
99 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
100 %endif
100 %endif
101 </div>
101 </div>
102 %endif
102 %endif
103 </div>
103 </div>
104 <div class="text">
104 <div class="text">
105 ${comment.render(mentions=True)|n}
105 ${comment.render(mentions=True)|n}
106 </div>
106 </div>
107 </div>
107 </div>
108 </div>
108 </div>
109 </%def>
109 </%def>
110
110
111 <%def name="comment_inline_form()">
111 <%def name="comment_inline_form()">
112 <div id="comment-inline-form-template" style="display: none;">
112 <div id="comment-inline-form-template" style="display: none;">
113 <div class="comment-inline-form ac">
113 <div class="comment-inline-form ac">
114 %if c.rhodecode_user.username != h.DEFAULT_USER:
114 %if c.rhodecode_user.username != h.DEFAULT_USER:
115 ${h.form('#', class_='inline-form', method='get')}
115 ${h.form('#', class_='inline-form', method='get')}
116 <div id="edit-container_{1}" class="clearfix">
116 <div id="edit-container_{1}" class="clearfix">
117 <div class="comment-title pull-left">
117 <div class="comment-title pull-left">
118 ${_('Create a comment on line {1}.')}
118 ${_('Create a comment on line {1}.')}
119 </div>
119 </div>
120 <div class="comment-help pull-right">
120 <div class="comment-help pull-right">
121 ${(_('Comments parsed using %s syntax with %s support.') % (
121 ${(_('Comments parsed using %s syntax with %s support.') % (
122 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
122 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
123 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
123 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
124 )
124 )
125 )|n
125 )|n
126 }
126 }
127 </div>
127 </div>
128 <div style="clear: both"></div>
128 <div style="clear: both"></div>
129 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
129 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
130 </div>
130 </div>
131 <div id="preview-container_{1}" class="clearfix" style="display: none;">
131 <div id="preview-container_{1}" class="clearfix" style="display: none;">
132 <div class="comment-help">
132 <div class="comment-help">
133 ${_('Comment preview')}
133 ${_('Comment preview')}
134 </div>
134 </div>
135 <div id="preview-box_{1}" class="preview-box"></div>
135 <div id="preview-box_{1}" class="preview-box"></div>
136 </div>
136 </div>
137 <div class="comment-footer">
137 <div class="comment-footer">
138 <div class="comment-button hide-inline-form-button cancel-button">
138 <div class="comment-button hide-inline-form-button cancel-button">
139 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
139 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
140 </div>
140 </div>
141 <div class="action-buttons">
141 <div class="action-buttons">
142 <input type="hidden" name="f_path" value="{0}">
142 <input type="hidden" name="f_path" value="{0}">
143 <input type="hidden" name="line" value="{1}">
143 <input type="hidden" name="line" value="{1}">
144 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
144 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
145 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
145 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
146 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
146 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
147 </div>
147 </div>
148 ${h.end_form()}
148 ${h.end_form()}
149 </div>
149 </div>
150 %else:
150 %else:
151 ${h.form('', class_='inline-form comment-form-login', method='get')}
151 ${h.form('', class_='inline-form comment-form-login', method='get')}
152 <div class="pull-left">
152 <div class="pull-left">
153 <div class="comment-help pull-right">
153 <div class="comment-help pull-right">
154 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
154 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div class="comment-button pull-right">
157 <div class="comment-button pull-right">
158 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
158 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
159 </div>
159 </div>
160 <div class="clearfix"></div>
160 <div class="clearfix"></div>
161 ${h.end_form()}
161 ${h.end_form()}
162 %endif
162 %endif
163 </div>
163 </div>
164 </div>
164 </div>
165 </%def>
165 </%def>
166
166
167
167
168 ## generates inlines taken from c.comments var
168 ## generates inlines taken from c.comments var
169 <%def name="inlines(is_pull_request=False)">
169 <%def name="inlines(is_pull_request=False)">
170 %if is_pull_request:
170 %if is_pull_request:
171 <h2 id="comments">${ungettext("%d Pull Request Comment", "%d Pull Request Comments", len(c.comments)) % len(c.comments)}</h2>
171 <h2 id="comments">${ungettext("%d Pull Request Comment", "%d Pull Request Comments", len(c.comments)) % len(c.comments)}</h2>
172 %else:
172 %else:
173 <h2 id="comments">${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}</h2>
173 <h2 id="comments">${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}</h2>
174 %endif
174 %endif
175 %for path, lines_comments in c.inline_comments:
175 %for path, lines_comments in c.inline_comments:
176 % for line, comments in lines_comments.iteritems():
176 % for line, comments in lines_comments.iteritems():
177 <div style="display: none;" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
177 <div style="display: none;" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
178 ## for each comment in particular line
178 ## for each comment in particular line
179 %for comment in comments:
179 %for comment in comments:
180 ${comment_block(comment, inline=True)}
180 ${comment_block(comment, inline=True)}
181 %endfor
181 %endfor
182 </div>
182 </div>
183 %endfor
183 %endfor
184 %endfor
184 %endfor
185
185
186 </%def>
186 </%def>
187
187
188 ## generate inline comments and the main ones
188 ## generate inline comments and the main ones
189 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
189 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
190 ## generate inlines for this changeset
190 ## generate inlines for this changeset
191 ${inlines(is_pull_request)}
191 ${inlines(is_pull_request)}
192
192
193 %for comment in c.comments:
193 %for comment in c.comments:
194 <div id="comment-tr-${comment.comment_id}">
194 <div id="comment-tr-${comment.comment_id}">
195 ## only render comments that are not from pull request, or from
195 ## only render comments that are not from pull request, or from
196 ## pull request and a status change
196 ## pull request and a status change
197 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
197 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
198 ${comment_block(comment)}
198 ${comment_block(comment)}
199 %endif
199 %endif
200 </div>
200 </div>
201 %endfor
201 %endfor
202 ## to anchor ajax comments
202 ## to anchor ajax comments
203 <div id="injected_page_comments"></div>
203 <div id="injected_page_comments"></div>
204 </%def>
204 </%def>
205
205
206 ## MAIN COMMENT FORM
206 ## MAIN COMMENT FORM
207 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
207 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
208 %if is_compare:
208 %if is_compare:
209 <% form_id = "comments_form_compare" %>
209 <% form_id = "comments_form_compare" %>
210 %else:
210 %else:
211 <% form_id = "comments_form" %>
211 <% form_id = "comments_form" %>
212 %endif
212 %endif
213
213
214
214
215 %if is_pull_request:
215 %if is_pull_request:
216 <div class="pull-request-merge">
216 <div class="pull-request-merge">
217 %if c.allowed_to_merge:
217 %if c.allowed_to_merge:
218 <div class="pull-request-wrap">
218 <div class="pull-request-wrap">
219 <div class="pull-right">
219 <div class="pull-right">
220 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
220 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
221 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
221 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
222 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
222 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
223 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
223 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
224 ${h.end_form()}
224 ${h.end_form()}
225 </div>
225 </div>
226 </div>
226 </div>
227 %else:
227 %else:
228 <div class="pull-request-wrap">
228 <div class="pull-request-wrap">
229 <div class="pull-right">
229 <div class="pull-right">
230 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
230 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
231 </div>
231 </div>
232 </div>
232 </div>
233 %endif
233 %endif
234 </div>
234 </div>
235 %endif
235 %endif
236 <div class="comments">
236 <div class="comments">
237 %if c.rhodecode_user.username != h.DEFAULT_USER:
237 %if c.rhodecode_user.username != h.DEFAULT_USER:
238 <div class="comment-form ac">
238 <div class="comment-form ac">
239 ${h.secure_form(post_url, id_=form_id)}
239 ${h.secure_form(post_url, id_=form_id)}
240 <div id="edit-container" class="clearfix">
240 <div id="edit-container" class="clearfix">
241 <div class="comment-title pull-left">
241 <div class="comment-title pull-left">
242 %if is_pull_request:
242 %if is_pull_request:
243 ${(_('Create a comment on this Pull Request.'))}
243 ${(_('Create a comment on this Pull Request.'))}
244 %elif is_compare:
244 %elif is_compare:
245 ${(_('Create comments on this Commit range.'))}
245 ${(_('Create comments on this Commit range.'))}
246 %else:
246 %else:
247 ${(_('Create a comment on this Commit.'))}
247 ${(_('Create a comment on this Commit.'))}
248 %endif
248 %endif
249 </div>
249 </div>
250 <div class="comment-help pull-right">
250 <div class="comment-help pull-right">
251 ${(_('Comments parsed using %s syntax with %s support.') % (
251 ${(_('Comments parsed using %s syntax with %s support.') % (
252 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
252 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
253 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
253 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
254 )
254 )
255 )|n
255 )|n
256 }
256 }
257 </div>
257 </div>
258 <div style="clear: both"></div>
258 <div style="clear: both"></div>
259 ${h.textarea('text', class_="comment-block-ta")}
259 ${h.textarea('text', class_="comment-block-ta")}
260 </div>
260 </div>
261
261
262 <div id="preview-container" class="clearfix" style="display: none;">
262 <div id="preview-container" class="clearfix" style="display: none;">
263 <div class="comment-title">
263 <div class="comment-title">
264 ${_('Comment preview')}
264 ${_('Comment preview')}
265 </div>
265 </div>
266 <div id="preview-box" class="preview-box"></div>
266 <div id="preview-box" class="preview-box"></div>
267 </div>
267 </div>
268
268
269 <div id="comment_form_extras">
269 <div id="comment_form_extras">
270 %if form_extras and isinstance(form_extras, (list, tuple)):
270 %if form_extras and isinstance(form_extras, (list, tuple)):
271 % for form_ex_el in form_extras:
271 % for form_ex_el in form_extras:
272 ${form_ex_el|n}
272 ${form_ex_el|n}
273 % endfor
273 % endfor
274 %endif
274 %endif
275 </div>
275 </div>
276 <div class="comment-footer">
276 <div class="comment-footer">
277 %if change_status:
277 %if change_status:
278 <div class="status_box">
278 <div class="status_box">
279 <select id="change_status" name="changeset_status">
279 <select id="change_status" name="changeset_status">
280 <option></option> # Placeholder
280 <option></option> # Placeholder
281 %for status,lbl in c.commit_statuses:
281 %for status,lbl in c.commit_statuses:
282 <option value="${status}" data-status="${status}">${lbl}</option>
282 <option value="${status}" data-status="${status}">${lbl}</option>
283 %if is_pull_request and change_status and status in ('approved', 'rejected'):
283 %if is_pull_request and change_status and status in ('approved', 'rejected'):
284 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
284 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
285 %endif
285 %endif
286 %endfor
286 %endfor
287 </select>
287 </select>
288 </div>
288 </div>
289 %endif
289 %endif
290 <div class="action-buttons">
290 <div class="action-buttons">
291 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
291 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
292 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
292 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
293 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
293 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
294 </div>
294 </div>
295 </div>
295 </div>
296 ${h.end_form()}
296 ${h.end_form()}
297 </div>
297 </div>
298 %endif
298 %endif
299 </div>
299 </div>
300 <script>
300 <script>
301 // init active elements of commentForm
301 // init active elements of commentForm
302 var commitId = templateContext.commit_data.commit_id;
302 var commitId = templateContext.commit_data.commit_id;
303 var pullRequestId = templateContext.pull_request_data.pull_request_id;
303 var pullRequestId = templateContext.pull_request_data.pull_request_id;
304 var lineNo;
304 var lineNo;
305
305
306 var mainCommentForm = new CommentForm(
306 var mainCommentForm = new CommentForm(
307 "#${form_id}", commitId, pullRequestId, lineNo, true);
307 "#${form_id}", commitId, pullRequestId, lineNo, true);
308
308
309 mainCommentForm.initStatusChangeSelector();
309 mainCommentForm.initStatusChangeSelector();
310
310 bindToggleButtons();
311 </script>
311 </script>
312 </%def>
312 </%def>
General Comments 0
You need to be logged in to leave comments. Login now