##// END OF EJS Templates
diffs: add comments to changeset diffs
dan -
r1143:7bd159d9 default
parent child Browse files
Show More
@@ -1,470 +1,465 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import ChangesetCommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
168
159 169 # get ranges of commit ids if preset
160 170 commit_range = commit_id_range.split('...')[:2]
161 enable_comments = True
171
162 172 try:
163 173 pre_load = ['affected_files', 'author', 'branch', 'date',
164 174 'message', 'parents']
165 175
166 176 if len(commit_range) == 2:
167 enable_comments = False
168 177 commits = c.rhodecode_repo.get_commits(
169 178 start_id=commit_range[0], end_id=commit_range[1],
170 179 pre_load=pre_load)
171 180 commits = list(commits)
172 181 else:
173 182 commits = [c.rhodecode_repo.get_commit(
174 183 commit_id=commit_id_range, pre_load=pre_load)]
175 184
176 185 c.commit_ranges = commits
177 186 if not c.commit_ranges:
178 187 raise RepositoryError(
179 188 'The commit range returned an empty result')
180 189 except CommitDoesNotExistError:
181 190 msg = _('No such commit exists for this repository')
182 191 h.flash(msg, category='error')
183 192 raise HTTPNotFound()
184 193 except Exception:
185 194 log.exception("General failure")
186 195 raise HTTPNotFound()
187 196
188 197 c.changes = OrderedDict()
189 198 c.lines_added = 0
190 199 c.lines_deleted = 0
191 200
192 201 c.commit_statuses = ChangesetStatus.STATUSES
193 c.comments = []
194 c.statuses = []
195 202 c.inline_comments = []
196 203 c.inline_cnt = 0
197 204 c.files = []
198 205
206 c.statuses = []
207 c.comments = []
208 if len(c.commit_ranges) == 1:
209 commit = c.commit_ranges[0]
210 c.comments = ChangesetCommentsModel().get_comments(
211 c.rhodecode_db_repo.repo_id,
212 revision=commit.raw_id)
213 c.statuses.append(ChangesetStatusModel().get_status(
214 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 # comments from PR
216 statuses = ChangesetStatusModel().get_statuses(
217 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 with_revisions=True)
219 prs = set(st.pull_request for st in statuses
220 if st is st.pull_request is not None)
221
222 # from associated statuses, check the pull requests, and
223 # show comments from them
224 for pr in prs:
225 c.comments.extend(pr.comments)
226
199 227 # Iterate over ranges (default commit view is always one commit)
200 228 for commit in c.commit_ranges:
201 if method == 'show':
202 c.statuses.extend([ChangesetStatusModel().get_status(
203 c.rhodecode_db_repo.repo_id, commit.raw_id)])
204
205 c.comments.extend(ChangesetCommentsModel().get_comments(
206 c.rhodecode_db_repo.repo_id,
207 revision=commit.raw_id))
208
209 # comments from PR
210 st = ChangesetStatusModel().get_statuses(
211 c.rhodecode_db_repo.repo_id, commit.raw_id,
212 with_revisions=True)
213
214 # from associated statuses, check the pull requests, and
215 # show comments from them
216
217 prs = set(x.pull_request for x in
218 filter(lambda x: x.pull_request is not None, st))
219 for pr in prs:
220 c.comments.extend(pr.comments)
221
222 inlines = ChangesetCommentsModel().get_inline_comments(
223 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
224 c.inline_comments.extend(inlines.iteritems())
225
226 229 c.changes[commit.raw_id] = []
227 230
228 231 commit2 = commit
229 232 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
230 233
231 # fetch global flags of ignore ws or context lines
232 context_lcl = get_line_ctx('', request.GET)
233 ign_whitespace_lcl = get_ignore_ws('', request.GET)
234
235 234 _diff = c.rhodecode_repo.get_diff(
236 235 commit1, commit2,
237 236 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238
239 # diff_limit will cut off the whole diff if the limit is applied
240 # otherwise it will just hide the big files from the front-end
241 diff_limit = self.cut_off_limit_diff
242 file_limit = self.cut_off_limit_file
243
244 237 diff_processor = diffs.DiffProcessor(
245 238 _diff, format='newdiff', diff_limit=diff_limit,
246 239 file_limit=file_limit, show_full_diff=fulldiff)
240
247 241 commit_changes = OrderedDict()
248 242 if method == 'show':
249 243 _parsed = diff_processor.prepare()
250 244 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
251 245
252 246 _parsed = diff_processor.prepare()
253 247
254 248 def _node_getter(commit):
255 249 def get_node(fname):
256 250 try:
257 251 return commit.get_node(fname)
258 252 except NodeDoesNotExistError:
259 253 return None
260 254 return get_node
261 255
256 inline_comments = ChangesetCommentsModel().get_inline_comments(
257 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
258 c.inline_cnt += len(inline_comments)
259
262 260 diffset = codeblocks.DiffSet(
263 261 repo_name=c.repo_name,
264 262 source_node_getter=_node_getter(commit1),
265 263 target_node_getter=_node_getter(commit2),
264 comments=inline_comments
266 265 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
267 266 c.changes[commit.raw_id] = diffset
268 267 else:
269 268 # downloads/raw we only need RAW diff nothing else
270 269 diff = diff_processor.as_raw()
271 270 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
272 271
273 272 # sort comments by how they were generated
274 273 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
275 274
276 # count inline comments
277 for __, lines in c.inline_comments:
278 for comments in lines.values():
279 c.inline_cnt += len(comments)
280 275
281 276 if len(c.commit_ranges) == 1:
282 277 c.commit = c.commit_ranges[0]
283 278 c.parent_tmpl = ''.join(
284 279 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 280 if method == 'download':
286 281 response.content_type = 'text/plain'
287 282 response.content_disposition = (
288 283 'attachment; filename=%s.diff' % commit_id_range[:12])
289 284 return diff
290 285 elif method == 'patch':
291 286 response.content_type = 'text/plain'
292 287 c.diff = safe_unicode(diff)
293 288 return render('changeset/patch_changeset.html')
294 289 elif method == 'raw':
295 290 response.content_type = 'text/plain'
296 291 return diff
297 292 elif method == 'show':
298 293 if len(c.commit_ranges) == 1:
299 294 return render('changeset/changeset.html')
300 295 else:
301 296 c.ancestor = None
302 297 c.target_repo = c.rhodecode_db_repo
303 298 return render('changeset/changeset_range.html')
304 299
305 300 @LoginRequired()
306 301 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 302 'repository.admin')
308 303 def index(self, revision, method='show'):
309 304 return self._index(revision, method=method)
310 305
311 306 @LoginRequired()
312 307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 308 'repository.admin')
314 309 def changeset_raw(self, revision):
315 310 return self._index(revision, method='raw')
316 311
317 312 @LoginRequired()
318 313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 314 'repository.admin')
320 315 def changeset_patch(self, revision):
321 316 return self._index(revision, method='patch')
322 317
323 318 @LoginRequired()
324 319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 320 'repository.admin')
326 321 def changeset_download(self, revision):
327 322 return self._index(revision, method='download')
328 323
329 324 @LoginRequired()
330 325 @NotAnonymous()
331 326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 327 'repository.admin')
333 328 @auth.CSRFRequired()
334 329 @jsonify
335 330 def comment(self, repo_name, revision):
336 331 commit_id = revision
337 332 status = request.POST.get('changeset_status', None)
338 333 text = request.POST.get('text')
339 334 if status:
340 335 text = text or (_('Status change %(transition_icon)s %(status)s')
341 336 % {'transition_icon': '>',
342 337 'status': ChangesetStatus.get_status_lbl(status)})
343 338
344 339 multi_commit_ids = filter(
345 340 lambda s: s not in ['', None],
346 341 request.POST.get('commit_ids', '').split(','),)
347 342
348 343 commit_ids = multi_commit_ids or [commit_id]
349 344 comment = None
350 345 for current_id in filter(None, commit_ids):
351 346 c.co = comment = ChangesetCommentsModel().create(
352 347 text=text,
353 348 repo=c.rhodecode_db_repo.repo_id,
354 349 user=c.rhodecode_user.user_id,
355 350 revision=current_id,
356 351 f_path=request.POST.get('f_path'),
357 352 line_no=request.POST.get('line'),
358 353 status_change=(ChangesetStatus.get_status_lbl(status)
359 354 if status else None),
360 355 status_change_type=status
361 356 )
362 357 # get status if set !
363 358 if status:
364 359 # if latest status was from pull request and it's closed
365 360 # disallow changing status !
366 361 # dont_allow_on_closed_pull_request = True !
367 362
368 363 try:
369 364 ChangesetStatusModel().set_status(
370 365 c.rhodecode_db_repo.repo_id,
371 366 status,
372 367 c.rhodecode_user.user_id,
373 368 comment,
374 369 revision=current_id,
375 370 dont_allow_on_closed_pull_request=True
376 371 )
377 372 except StatusChangeOnClosedPullRequestError:
378 373 msg = _('Changing the status of a commit associated with '
379 374 'a closed pull request is not allowed')
380 375 log.exception(msg)
381 376 h.flash(msg, category='warning')
382 377 return redirect(h.url(
383 378 'changeset_home', repo_name=repo_name,
384 379 revision=current_id))
385 380
386 381 # finalize, commit and redirect
387 382 Session().commit()
388 383
389 384 data = {
390 385 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 386 }
392 387 if comment:
393 388 data.update(comment.get_dict())
394 389 data.update({'rendered_text':
395 390 render('changeset/changeset_comment_block.html')})
396 391
397 392 return data
398 393
399 394 @LoginRequired()
400 395 @NotAnonymous()
401 396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 397 'repository.admin')
403 398 @auth.CSRFRequired()
404 399 def preview_comment(self):
405 400 # Technically a CSRF token is not needed as no state changes with this
406 401 # call. However, as this is a POST is better to have it, so automated
407 402 # tools don't flag it as potential CSRF.
408 403 # Post is required because the payload could be bigger than the maximum
409 404 # allowed by GET.
410 405 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 406 raise HTTPBadRequest()
412 407 text = request.POST.get('text')
413 408 renderer = request.POST.get('renderer') or 'rst'
414 409 if text:
415 410 return h.render(text, renderer=renderer, mentions=True)
416 411 return ''
417 412
418 413 @LoginRequired()
419 414 @NotAnonymous()
420 415 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 416 'repository.admin')
422 417 @auth.CSRFRequired()
423 418 @jsonify
424 419 def delete_comment(self, repo_name, comment_id):
425 420 comment = ChangesetComment.get(comment_id)
426 421 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 422 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 423 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 424 ChangesetCommentsModel().delete(comment=comment)
430 425 Session().commit()
431 426 return True
432 427 else:
433 428 raise HTTPForbidden()
434 429
435 430 @LoginRequired()
436 431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 432 'repository.admin')
438 433 @jsonify
439 434 def changeset_info(self, repo_name, revision):
440 435 if request.is_xhr:
441 436 try:
442 437 return c.rhodecode_repo.get_commit(commit_id=revision)
443 438 except CommitDoesNotExistError as e:
444 439 return EmptyCommit(message=str(e))
445 440 else:
446 441 raise HTTPBadRequest()
447 442
448 443 @LoginRequired()
449 444 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 445 'repository.admin')
451 446 @jsonify
452 447 def changeset_children(self, repo_name, revision):
453 448 if request.is_xhr:
454 449 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 450 result = {"results": commit.children}
456 451 return result
457 452 else:
458 453 raise HTTPBadRequest()
459 454
460 455 @LoginRequired()
461 456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 457 'repository.admin')
463 458 @jsonify
464 459 def changeset_parents(self, repo_name, revision):
465 460 if request.is_xhr:
466 461 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 462 result = {"results": commit.parents}
468 463 return result
469 464 else:
470 465 raise HTTPBadRequest()
@@ -1,642 +1,665 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import difflib
23 23 from itertools import groupby
24 24
25 25 from pygments import lex
26 26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 27 from rhodecode.lib.helpers import (
28 28 get_lexer_for_filenode, get_lexer_safe, html_escape)
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.lib.vcs.nodes import FileNode
31 31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 32 from rhodecode.lib.diffs import LimitedDiffContainer
33 33 from pygments.lexers import get_lexer_by_name
34 34
35 35 plain_text_lexer = get_lexer_by_name(
36 36 'text', stripall=False, stripnl=False, ensurenl=False)
37 37
38 38
39 39 log = logging.getLogger()
40 40
41 41
42 42 def filenode_as_lines_tokens(filenode, lexer=None):
43 43 lexer = lexer or get_lexer_for_filenode(filenode)
44 44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
45 45 tokens = tokenize_string(filenode.content, lexer)
46 46 lines = split_token_stream(tokens, split_string='\n')
47 47 rv = list(lines)
48 48 return rv
49 49
50 50
51 51 def tokenize_string(content, lexer):
52 52 """
53 53 Use pygments to tokenize some content based on a lexer
54 54 ensuring all original new lines and whitespace is preserved
55 55 """
56 56
57 57 lexer.stripall = False
58 58 lexer.stripnl = False
59 59 lexer.ensurenl = False
60 60 for token_type, token_text in lex(content, lexer):
61 61 yield pygment_token_class(token_type), token_text
62 62
63 63
64 64 def split_token_stream(tokens, split_string=u'\n'):
65 65 """
66 66 Take a list of (TokenType, text) tuples and split them by a string
67 67
68 68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
69 69 [(TEXT, 'some'), (TEXT, 'text'),
70 70 (TEXT, 'more'), (TEXT, 'text')]
71 71 """
72 72
73 73 buffer = []
74 74 for token_class, token_text in tokens:
75 75 parts = token_text.split(split_string)
76 76 for part in parts[:-1]:
77 77 buffer.append((token_class, part))
78 78 yield buffer
79 79 buffer = []
80 80
81 81 buffer.append((token_class, parts[-1]))
82 82
83 83 if buffer:
84 84 yield buffer
85 85
86 86
87 87 def filenode_as_annotated_lines_tokens(filenode):
88 88 """
89 89 Take a file node and return a list of annotations => lines, if no annotation
90 90 is found, it will be None.
91 91
92 92 eg:
93 93
94 94 [
95 95 (annotation1, [
96 96 (1, line1_tokens_list),
97 97 (2, line2_tokens_list),
98 98 ]),
99 99 (annotation2, [
100 100 (3, line1_tokens_list),
101 101 ]),
102 102 (None, [
103 103 (4, line1_tokens_list),
104 104 ]),
105 105 (annotation1, [
106 106 (5, line1_tokens_list),
107 107 (6, line2_tokens_list),
108 108 ])
109 109 ]
110 110 """
111 111
112 112 commit_cache = {} # cache commit_getter lookups
113 113
114 114 def _get_annotation(commit_id, commit_getter):
115 115 if commit_id not in commit_cache:
116 116 commit_cache[commit_id] = commit_getter()
117 117 return commit_cache[commit_id]
118 118
119 119 annotation_lookup = {
120 120 line_no: _get_annotation(commit_id, commit_getter)
121 121 for line_no, commit_id, commit_getter, line_content
122 122 in filenode.annotate
123 123 }
124 124
125 125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
126 126 for line_no, tokens
127 127 in enumerate(filenode_as_lines_tokens(filenode), 1))
128 128
129 129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
130 130
131 131 for annotation, group in grouped_annotations_lines:
132 132 yield (
133 133 annotation, [(line_no, tokens)
134 134 for (_, line_no, tokens) in group]
135 135 )
136 136
137 137
138 138 def render_tokenstream(tokenstream):
139 139 result = []
140 140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
141 141
142 142 if token_class:
143 143 result.append(u'<span class="%s">' % token_class)
144 144 else:
145 145 result.append(u'<span>')
146 146
147 147 for op_tag, token_text in token_ops_texts:
148 148
149 149 if op_tag:
150 150 result.append(u'<%s>' % op_tag)
151 151
152 152 escaped_text = html_escape(token_text)
153 153
154 154 # TODO: dan: investigate showing hidden characters like space/nl/tab
155 155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
156 156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
157 157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
158 158
159 159 result.append(escaped_text)
160 160
161 161 if op_tag:
162 162 result.append(u'</%s>' % op_tag)
163 163
164 164 result.append(u'</span>')
165 165
166 166 html = ''.join(result)
167 167 return html
168 168
169 169
170 170 def rollup_tokenstream(tokenstream):
171 171 """
172 172 Group a token stream of the format:
173 173
174 174 ('class', 'op', 'text')
175 175 or
176 176 ('class', 'text')
177 177
178 178 into
179 179
180 180 [('class1',
181 181 [('op1', 'text'),
182 182 ('op2', 'text')]),
183 183 ('class2',
184 184 [('op3', 'text')])]
185 185
186 186 This is used to get the minimal tags necessary when
187 187 rendering to html eg for a token stream ie.
188 188
189 189 <span class="A"><ins>he</ins>llo</span>
190 190 vs
191 191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
192 192
193 193 If a 2 tuple is passed in, the output op will be an empty string.
194 194
195 195 eg:
196 196
197 197 >>> rollup_tokenstream([('classA', '', 'h'),
198 198 ('classA', 'del', 'ell'),
199 199 ('classA', '', 'o'),
200 200 ('classB', '', ' '),
201 201 ('classA', '', 'the'),
202 202 ('classA', '', 're'),
203 203 ])
204 204
205 205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
206 206 ('classB', [('', ' ')],
207 207 ('classA', [('', 'there')]]
208 208
209 209 """
210 210 if tokenstream and len(tokenstream[0]) == 2:
211 211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
212 212
213 213 result = []
214 214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
215 215 ops = []
216 216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
217 217 text_buffer = []
218 218 for t_class, t_op, t_text in token_text_list:
219 219 text_buffer.append(t_text)
220 220 ops.append((token_op, ''.join(text_buffer)))
221 221 result.append((token_class, ops))
222 222 return result
223 223
224 224
225 225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
226 226 """
227 227 Converts a list of (token_class, token_text) tuples to a list of
228 228 (token_class, token_op, token_text) tuples where token_op is one of
229 229 ('ins', 'del', '')
230 230
231 231 :param old_tokens: list of (token_class, token_text) tuples of old line
232 232 :param new_tokens: list of (token_class, token_text) tuples of new line
233 233 :param use_diff_match_patch: boolean, will use google's diff match patch
234 234 library which has options to 'smooth' out the character by character
235 235 differences making nicer ins/del blocks
236 236 """
237 237
238 238 old_tokens_result = []
239 239 new_tokens_result = []
240 240
241 241 similarity = difflib.SequenceMatcher(None,
242 242 ''.join(token_text for token_class, token_text in old_tokens),
243 243 ''.join(token_text for token_class, token_text in new_tokens)
244 244 ).ratio()
245 245
246 246 if similarity < 0.6: # return, the blocks are too different
247 247 for token_class, token_text in old_tokens:
248 248 old_tokens_result.append((token_class, '', token_text))
249 249 for token_class, token_text in new_tokens:
250 250 new_tokens_result.append((token_class, '', token_text))
251 251 return old_tokens_result, new_tokens_result, similarity
252 252
253 253 token_sequence_matcher = difflib.SequenceMatcher(None,
254 254 [x[1] for x in old_tokens],
255 255 [x[1] for x in new_tokens])
256 256
257 257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
258 258 # check the differences by token block types first to give a more
259 259 # nicer "block" level replacement vs character diffs
260 260
261 261 if tag == 'equal':
262 262 for token_class, token_text in old_tokens[o1:o2]:
263 263 old_tokens_result.append((token_class, '', token_text))
264 264 for token_class, token_text in new_tokens[n1:n2]:
265 265 new_tokens_result.append((token_class, '', token_text))
266 266 elif tag == 'delete':
267 267 for token_class, token_text in old_tokens[o1:o2]:
268 268 old_tokens_result.append((token_class, 'del', token_text))
269 269 elif tag == 'insert':
270 270 for token_class, token_text in new_tokens[n1:n2]:
271 271 new_tokens_result.append((token_class, 'ins', token_text))
272 272 elif tag == 'replace':
273 273 # if same type token blocks must be replaced, do a diff on the
274 274 # characters in the token blocks to show individual changes
275 275
276 276 old_char_tokens = []
277 277 new_char_tokens = []
278 278 for token_class, token_text in old_tokens[o1:o2]:
279 279 for char in token_text:
280 280 old_char_tokens.append((token_class, char))
281 281
282 282 for token_class, token_text in new_tokens[n1:n2]:
283 283 for char in token_text:
284 284 new_char_tokens.append((token_class, char))
285 285
286 286 old_string = ''.join([token_text for
287 287 token_class, token_text in old_char_tokens])
288 288 new_string = ''.join([token_text for
289 289 token_class, token_text in new_char_tokens])
290 290
291 291 char_sequence = difflib.SequenceMatcher(
292 292 None, old_string, new_string)
293 293 copcodes = char_sequence.get_opcodes()
294 294 obuffer, nbuffer = [], []
295 295
296 296 if use_diff_match_patch:
297 297 dmp = diff_match_patch()
298 298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
299 299 reps = dmp.diff_main(old_string, new_string)
300 300 dmp.diff_cleanupEfficiency(reps)
301 301
302 302 a, b = 0, 0
303 303 for op, rep in reps:
304 304 l = len(rep)
305 305 if op == 0:
306 306 for i, c in enumerate(rep):
307 307 obuffer.append((old_char_tokens[a+i][0], '', c))
308 308 nbuffer.append((new_char_tokens[b+i][0], '', c))
309 309 a += l
310 310 b += l
311 311 elif op == -1:
312 312 for i, c in enumerate(rep):
313 313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
314 314 a += l
315 315 elif op == 1:
316 316 for i, c in enumerate(rep):
317 317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
318 318 b += l
319 319 else:
320 320 for ctag, co1, co2, cn1, cn2 in copcodes:
321 321 if ctag == 'equal':
322 322 for token_class, token_text in old_char_tokens[co1:co2]:
323 323 obuffer.append((token_class, '', token_text))
324 324 for token_class, token_text in new_char_tokens[cn1:cn2]:
325 325 nbuffer.append((token_class, '', token_text))
326 326 elif ctag == 'delete':
327 327 for token_class, token_text in old_char_tokens[co1:co2]:
328 328 obuffer.append((token_class, 'del', token_text))
329 329 elif ctag == 'insert':
330 330 for token_class, token_text in new_char_tokens[cn1:cn2]:
331 331 nbuffer.append((token_class, 'ins', token_text))
332 332 elif ctag == 'replace':
333 333 for token_class, token_text in old_char_tokens[co1:co2]:
334 334 obuffer.append((token_class, 'del', token_text))
335 335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 336 nbuffer.append((token_class, 'ins', token_text))
337 337
338 338 old_tokens_result.extend(obuffer)
339 339 new_tokens_result.extend(nbuffer)
340 340
341 341 return old_tokens_result, new_tokens_result, similarity
342 342
343 343
344 344 class DiffSet(object):
345 345 """
346 346 An object for parsing the diff result from diffs.DiffProcessor and
347 347 adding highlighting, side by side/unified renderings and line diffs
348 348 """
349 349
350 350 HL_REAL = 'REAL' # highlights using original file, slow
351 351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
352 352 # in the case of multiline code
353 353 HL_NONE = 'NONE' # no highlighting, fastest
354 354
355 355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
356 356 source_node_getter=lambda filename: None,
357 357 target_node_getter=lambda filename: None,
358 358 source_nodes=None, target_nodes=None,
359 359 max_file_size_limit=150 * 1024, # files over this size will
360 360 # use fast highlighting
361 comments=None,
361 362 ):
362 363
363 364 self.highlight_mode = highlight_mode
364 365 self.highlighted_filenodes = {}
365 366 self.source_node_getter = source_node_getter
366 367 self.target_node_getter = target_node_getter
367 368 self.source_nodes = source_nodes or {}
368 369 self.target_nodes = target_nodes or {}
369 370 self.repo_name = repo_name
370
371 self.comments = comments or {}
371 372 self.max_file_size_limit = max_file_size_limit
372 373
373 374 def render_patchset(self, patchset, source_ref=None, target_ref=None):
374 375 diffset = AttributeDict(dict(
375 376 lines_added=0,
376 377 lines_deleted=0,
377 378 changed_files=0,
378 379 files=[],
379 380 limited_diff=isinstance(patchset, LimitedDiffContainer),
380 381 repo_name=self.repo_name,
381 382 source_ref=source_ref,
382 383 target_ref=target_ref,
383 384 ))
384 385 for patch in patchset:
385 386 filediff = self.render_patch(patch)
386 387 filediff.diffset = diffset
387 388 diffset.files.append(filediff)
388 389 diffset.changed_files += 1
389 390 if not patch['stats']['binary']:
390 391 diffset.lines_added += patch['stats']['added']
391 392 diffset.lines_deleted += patch['stats']['deleted']
392 393
393 394 return diffset
394 395
395 396 _lexer_cache = {}
396 397 def _get_lexer_for_filename(self, filename):
397 398 # cached because we might need to call it twice for source/target
398 399 if filename not in self._lexer_cache:
399 400 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
400 401 return self._lexer_cache[filename]
401 402
402 403 def render_patch(self, patch):
403 404 log.debug('rendering diff for %r' % patch['filename'])
404 405
405 406 source_filename = patch['original_filename']
406 407 target_filename = patch['filename']
407 408
408 409 source_lexer = plain_text_lexer
409 410 target_lexer = plain_text_lexer
410 411
411 412 if not patch['stats']['binary']:
412 413 if self.highlight_mode == self.HL_REAL:
413 414 if (source_filename and patch['operation'] in ('D', 'M')
414 415 and source_filename not in self.source_nodes):
415 416 self.source_nodes[source_filename] = (
416 417 self.source_node_getter(source_filename))
417 418
418 419 if (target_filename and patch['operation'] in ('A', 'M')
419 420 and target_filename not in self.target_nodes):
420 421 self.target_nodes[target_filename] = (
421 422 self.target_node_getter(target_filename))
422 423
423 424 elif self.highlight_mode == self.HL_FAST:
424 425 source_lexer = self._get_lexer_for_filename(source_filename)
425 426 target_lexer = self._get_lexer_for_filename(target_filename)
426 427
427 428 source_file = self.source_nodes.get(source_filename, source_filename)
428 429 target_file = self.target_nodes.get(target_filename, target_filename)
429 430
430 431 source_filenode, target_filenode = None, None
431 432
432 433 # TODO: dan: FileNode.lexer works on the content of the file - which
433 434 # can be slow - issue #4289 explains a lexer clean up - which once
434 435 # done can allow caching a lexer for a filenode to avoid the file lookup
435 436 if isinstance(source_file, FileNode):
436 437 source_filenode = source_file
437 438 source_lexer = source_file.lexer
438 439 if isinstance(target_file, FileNode):
439 440 target_filenode = target_file
440 441 target_lexer = target_file.lexer
441 442
442 443 source_file_path, target_file_path = None, None
443 444
444 445 if source_filename != '/dev/null':
445 446 source_file_path = source_filename
446 447 if target_filename != '/dev/null':
447 448 target_file_path = target_filename
448 449
449 450 source_file_type = source_lexer.name
450 451 target_file_type = target_lexer.name
451 452
452 453 op_hunks = patch['chunks'][0]
453 454 hunks = patch['chunks'][1:]
454 455
455 456 filediff = AttributeDict({
456 457 'source_file_path': source_file_path,
457 458 'target_file_path': target_file_path,
458 459 'source_filenode': source_filenode,
459 460 'target_filenode': target_filenode,
460 461 'hunks': [],
461 462 'source_file_type': target_file_type,
462 463 'target_file_type': source_file_type,
463 464 'patch': patch,
464 465 'source_mode': patch['stats']['old_mode'],
465 466 'target_mode': patch['stats']['new_mode'],
466 467 'limited_diff': isinstance(patch, LimitedDiffContainer),
467 468 'diffset': self,
468 469 })
469 470
470 471 for hunk in hunks:
471 472 hunkbit = self.parse_hunk(hunk, source_file, target_file)
472 473 hunkbit.filediff = filediff
473 474 filediff.hunks.append(hunkbit)
474 475 return filediff
475 476
476 477 def parse_hunk(self, hunk, source_file, target_file):
477 478 result = AttributeDict(dict(
478 479 source_start=hunk['source_start'],
479 480 source_length=hunk['source_length'],
480 481 target_start=hunk['target_start'],
481 482 target_length=hunk['target_length'],
482 483 section_header=hunk['section_header'],
483 484 lines=[],
484 485 ))
485 486 before, after = [], []
486 487
487 488 for line in hunk['lines']:
488 489 if line['action'] == 'unmod':
489 490 result.lines.extend(
490 491 self.parse_lines(before, after, source_file, target_file))
491 492 after.append(line)
492 493 before.append(line)
493 494 elif line['action'] == 'add':
494 495 after.append(line)
495 496 elif line['action'] == 'del':
496 497 before.append(line)
497 498 elif line['action'] == 'old-no-nl':
498 499 before.append(line)
499 500 elif line['action'] == 'new-no-nl':
500 501 after.append(line)
501 502
502 503 result.lines.extend(
503 504 self.parse_lines(before, after, source_file, target_file))
504 505 result.unified = self.as_unified(result.lines)
505 506 result.sideside = result.lines
506 507 return result
507 508
508 509 def parse_lines(self, before_lines, after_lines, source_file, target_file):
509 510 # TODO: dan: investigate doing the diff comparison and fast highlighting
510 511 # on the entire before and after buffered block lines rather than by
511 512 # line, this means we can get better 'fast' highlighting if the context
512 513 # allows it - eg.
513 514 # line 4: """
514 515 # line 5: this gets highlighted as a string
515 516 # line 6: """
516 517
517 518 lines = []
518 519 while before_lines or after_lines:
519 520 before, after = None, None
520 521 before_tokens, after_tokens = None, None
521 522
522 523 if before_lines:
523 524 before = before_lines.pop(0)
524 525 if after_lines:
525 526 after = after_lines.pop(0)
526 527
527 528 original = AttributeDict()
528 529 modified = AttributeDict()
529 530
530 531 if before:
531 532 if before['action'] == 'old-no-nl':
532 533 before_tokens = [('nonl', before['line'])]
533 534 else:
534 535 before_tokens = self.get_line_tokens(
535 536 line_text=before['line'], line_number=before['old_lineno'],
536 537 file=source_file)
537 538 original.lineno = before['old_lineno']
538 539 original.content = before['line']
539 540 original.action = self.action_to_op(before['action'])
541 original.comments = self.get_comments_for('old',
542 source_file, before['old_lineno'])
540 543
541 544 if after:
542 545 if after['action'] == 'new-no-nl':
543 546 after_tokens = [('nonl', after['line'])]
544 547 else:
545 548 after_tokens = self.get_line_tokens(
546 549 line_text=after['line'], line_number=after['new_lineno'],
547 550 file=target_file)
548 551 modified.lineno = after['new_lineno']
549 552 modified.content = after['line']
550 553 modified.action = self.action_to_op(after['action'])
554 modified.comments = self.get_comments_for('new',
555 target_file, after['new_lineno'])
551 556
552 557 # diff the lines
553 558 if before_tokens and after_tokens:
554 559 o_tokens, m_tokens, similarity = tokens_diff(
555 560 before_tokens, after_tokens)
556 561 original.content = render_tokenstream(o_tokens)
557 562 modified.content = render_tokenstream(m_tokens)
558 563 elif before_tokens:
559 564 original.content = render_tokenstream(
560 565 [(x[0], '', x[1]) for x in before_tokens])
561 566 elif after_tokens:
562 567 modified.content = render_tokenstream(
563 568 [(x[0], '', x[1]) for x in after_tokens])
564 569
565 570 lines.append(AttributeDict({
566 571 'original': original,
567 572 'modified': modified,
568 573 }))
569 574
570 575 return lines
571 576
577 def get_comments_for(self, version, file, line_number):
578 if hasattr(file, 'unicode_path'):
579 file = file.unicode_path
580
581 if not isinstance(file, basestring):
582 return None
583
584 line_key = {
585 'old': 'o',
586 'new': 'n',
587 }[version] + str(line_number)
588
589 return self.comments.get(file, {}).get(line_key)
590
572 591 def get_line_tokens(self, line_text, line_number, file=None):
573 592 filenode = None
574 593 filename = None
575 594
576 595 if isinstance(file, basestring):
577 596 filename = file
578 597 elif isinstance(file, FileNode):
579 598 filenode = file
580 599 filename = file.unicode_path
581 600
582 601 if self.highlight_mode == self.HL_REAL and filenode:
583 602 if line_number and file.size < self.max_file_size_limit:
584 603 return self.get_tokenized_filenode_line(file, line_number)
585 604
586 605 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
587 606 lexer = self._get_lexer_for_filename(filename)
588 607 return list(tokenize_string(line_text, lexer))
589 608
590 609 return list(tokenize_string(line_text, plain_text_lexer))
591 610
592 611 def get_tokenized_filenode_line(self, filenode, line_number):
593 612
594 613 if filenode not in self.highlighted_filenodes:
595 614 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
596 615 self.highlighted_filenodes[filenode] = tokenized_lines
597 616 return self.highlighted_filenodes[filenode][line_number - 1]
598 617
599 618 def action_to_op(self, action):
600 619 return {
601 620 'add': '+',
602 621 'del': '-',
603 622 'unmod': ' ',
604 623 'old-no-nl': ' ',
605 624 'new-no-nl': ' ',
606 625 }.get(action, action)
607 626
608 627 def as_unified(self, lines):
609 628 """ Return a generator that yields the lines of a diff in unified order """
610 629 def generator():
611 630 buf = []
612 631 for line in lines:
613 632
614 633 if buf and not line.original or line.original.action == ' ':
615 634 for b in buf:
616 635 yield b
617 636 buf = []
618 637
619 638 if line.original:
620 639 if line.original.action == ' ':
621 640 yield (line.original.lineno, line.modified.lineno,
622 line.original.action, line.original.content)
641 line.original.action, line.original.content,
642 line.original.comments)
623 643 continue
624 644
625 645 if line.original.action == '-':
626 646 yield (line.original.lineno, None,
627 line.original.action, line.original.content)
647 line.original.action, line.original.content,
648 line.original.comments)
628 649
629 650 if line.modified.action == '+':
630 651 buf.append((
631 652 None, line.modified.lineno,
632 line.modified.action, line.modified.content))
653 line.modified.action, line.modified.content,
654 line.modified.comments))
633 655 continue
634 656
635 657 if line.modified:
636 658 yield (None, line.modified.lineno,
637 line.modified.action, line.modified.content)
659 line.modified.action, line.modified.content,
660 line.modified.comments)
638 661
639 662 for b in buf:
640 663 yield b
641 664
642 665 return generator()
@@ -1,1039 +1,1142 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px !important;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 overflow-y: hidden;
395 395 padding: 0;
396 396 }
397 397 div.diffblock {
398 398 border: none;
399 399 }
400 400
401 401 &.inline-form {
402 402 width: 99%
403 403 }
404 404 }
405 405
406 406
407 407 table.code-difftable {
408 408 width: 100%;
409 409 }
410 410
411 411 /** PYGMENTS COLORING **/
412 412 div.codeblock {
413 413
414 414 // TODO: johbo: Added interim to get rid of the margin around
415 415 // Select2 widgets. This needs further cleanup.
416 416 margin-top: @padding;
417 417
418 418 overflow: auto;
419 419 padding: 0px;
420 420 border: @border-thickness solid @grey5;
421 421 background: @grey6;
422 422 .border-radius(@border-radius);
423 423
424 424 #remove_gist {
425 425 float: right;
426 426 }
427 427
428 428 .author {
429 429 clear: both;
430 430 vertical-align: middle;
431 431 font-family: @text-bold;
432 432 }
433 433
434 434 .btn-mini {
435 435 float: left;
436 436 margin: 0 5px 0 0;
437 437 }
438 438
439 439 .code-header {
440 440 padding: @padding;
441 441 border-bottom: @border-thickness solid @grey5;
442 442
443 443 .rc-user {
444 444 min-width: 0;
445 445 margin-right: .5em;
446 446 }
447 447
448 448 .stats {
449 449 clear: both;
450 450 margin: 0 0 @padding 0;
451 451 padding: 0;
452 452 .left {
453 453 float: left;
454 454 clear: left;
455 455 max-width: 75%;
456 456 margin: 0 0 @padding 0;
457 457
458 458 &.item {
459 459 margin-right: @padding;
460 460 &.last { border-right: none; }
461 461 }
462 462 }
463 463 .buttons { float: right; }
464 464 .author {
465 465 height: 25px; margin-left: 15px; font-weight: bold;
466 466 }
467 467 }
468 468
469 469 .commit {
470 470 margin: 5px 0 0 26px;
471 471 font-weight: normal;
472 472 white-space: pre-wrap;
473 473 }
474 474 }
475 475
476 476 .message {
477 477 position: relative;
478 478 margin: @padding;
479 479
480 480 .codeblock-label {
481 481 margin: 0 0 1em 0;
482 482 }
483 483 }
484 484
485 485 .code-body {
486 486 padding: @padding;
487 487 background-color: #ffffff;
488 488 min-width: 100%;
489 489 box-sizing: border-box;
490 490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 491 // to have the intended size and to scroll. Should be simplified.
492 492 width: 100%;
493 493 overflow-x: auto;
494 494 }
495 495 }
496 496
497 497 .code-highlighttable,
498 498 div.codeblock {
499 499
500 500 &.readme {
501 501 background-color: white;
502 502 }
503 503
504 504 .markdown-block table {
505 505 border-collapse: collapse;
506 506
507 507 th,
508 508 td {
509 509 padding: .5em;
510 510 border: @border-thickness solid @border-default-color;
511 511 }
512 512 }
513 513
514 514 table {
515 515 border: 0px;
516 516 margin: 0;
517 517 letter-spacing: normal;
518 518
519 519
520 520 td {
521 521 border: 0px;
522 522 vertical-align: top;
523 523 }
524 524 }
525 525 }
526 526
527 527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 528 div.search-code-body {
529 529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 530 pre {
531 531 .match { background-color: #faffa6;}
532 532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 533 }
534 534 .code-highlighttable {
535 535 border-collapse: collapse;
536 536
537 537 tr:hover {
538 538 background: #fafafa;
539 539 }
540 540 td.code {
541 541 padding-left: 10px;
542 542 }
543 543 td.line {
544 544 border-right: 1px solid #ccc !important;
545 545 padding-right: 10px;
546 546 text-align: right;
547 547 font-family: "Lucida Console",Monaco,monospace;
548 548 span {
549 549 white-space: pre-wrap;
550 550 color: #666666;
551 551 }
552 552 }
553 553 }
554 554 }
555 555
556 556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 557 .code-highlight {
558 558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 560 pre div:target {background-color: @comment-highlight-color !important;}
561 561 }
562 562
563 563 .linenos a { text-decoration: none; }
564 564
565 565 .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 567 .CodeMirror ::selection { background: @rchighlightblue; }
568 568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569 569
570 570 .code { display: block; border:0px !important; }
571 571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 572 .codehilite {
573 573 .hll { background-color: #ffffcc }
574 574 .c { color: #408080; font-style: italic } /* Comment */
575 575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 576 .k { color: #008000; font-weight: bold } /* Keyword */
577 577 .o { color: #666666 } /* Operator */
578 578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 582 .gd { color: #A00000 } /* Generic.Deleted */
583 583 .ge { font-style: italic } /* Generic.Emph */
584 584 .gr { color: #FF0000 } /* Generic.Error */
585 585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 586 .gi { color: #00A000 } /* Generic.Inserted */
587 587 .go { color: #808080 } /* Generic.Output */
588 588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 589 .gs { font-weight: bold } /* Generic.Strong */
590 590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 591 .gt { color: #0040D0 } /* Generic.Traceback */
592 592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 595 .kp { color: #008000 } /* Keyword.Pseudo */
596 596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 597 .kt { color: #B00040 } /* Keyword.Type */
598 598 .m { color: #666666 } /* Literal.Number */
599 599 .s { color: #BA2121 } /* Literal.String */
600 600 .na { color: #7D9029 } /* Name.Attribute */
601 601 .nb { color: #008000 } /* Name.Builtin */
602 602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 603 .no { color: #880000 } /* Name.Constant */
604 604 .nd { color: #AA22FF } /* Name.Decorator */
605 605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 607 .nf { color: #0000FF } /* Name.Function */
608 608 .nl { color: #A0A000 } /* Name.Label */
609 609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 611 .nv { color: #19177C } /* Name.Variable */
612 612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 613 .w { color: #bbbbbb } /* Text.Whitespace */
614 614 .mf { color: #666666 } /* Literal.Number.Float */
615 615 .mh { color: #666666 } /* Literal.Number.Hex */
616 616 .mi { color: #666666 } /* Literal.Number.Integer */
617 617 .mo { color: #666666 } /* Literal.Number.Oct */
618 618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 619 .sc { color: #BA2121 } /* Literal.String.Char */
620 620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 625 .sx { color: #008000 } /* Literal.String.Other */
626 626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 628 .ss { color: #19177C } /* Literal.String.Symbol */
629 629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 630 .vc { color: #19177C } /* Name.Variable.Class */
631 631 .vg { color: #19177C } /* Name.Variable.Global */
632 632 .vi { color: #19177C } /* Name.Variable.Instance */
633 633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 634 }
635 635
636 636 /* customized pre blocks for markdown/rst */
637 637 pre.literal-block, .codehilite pre{
638 638 padding: @padding;
639 639 border: 1px solid @grey6;
640 640 .border-radius(@border-radius);
641 641 background-color: @grey7;
642 642 }
643 643
644 644
645 645 /* START NEW CODE BLOCK CSS */
646 646
647 647 @cb-line-height: 18px;
648 648 @cb-line-code-padding: 10px;
649 649 @cb-text-padding: 5px;
650 650
651 651 @pill-padding: 2px 7px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 661 border-width: 9px 0 9px 15.6px;
662 662 border-color: transparent transparent transparent #ccc;
663 663 }
664 664 .filediff-menu {
665 665 display: none;
666 666 }
667 667 margin: -1px 0 0 0;
668 668 }
669 669
670 670 &+ .filediff { /* file diff is expanded */
671 671 .filediff-collapse-indicator {
672 672 border-width: 15.6px 9px 0 9px;
673 673 border-color: #ccc transparent transparent transparent;
674 674 }
675 675 .filediff-menu {
676 676 display: block;
677 677 }
678 678 margin: 20px 0;
679 679 &:nth-child(2) {
680 680 margin: 0;
681 681 }
682 682 }
683 683 }
684 684 .cs_files {
685 685 clear: both;
686 686 }
687 687
688 688 .diffset-menu {
689 689 margin-bottom: 20px;
690 690 }
691 691 .diffset {
692 692 margin: 20px auto;
693 693 .diffset-heading {
694 694 border: 1px solid @grey5;
695 695 margin-bottom: -1px;
696 696 // margin-top: 20px;
697 697 h2 {
698 698 margin: 0;
699 699 line-height: 38px;
700 700 padding-left: 10px;
701 701 }
702 702 .btn {
703 703 margin: 0;
704 704 }
705 705 background: @grey6;
706 706 display: block;
707 707 padding: 5px;
708 708 }
709 709 .diffset-heading-warning {
710 710 background: @alert3-inner;
711 711 border: 1px solid @alert3;
712 712 }
713 713 }
714 714 .pill {
715 715 display: block;
716 716 float: left;
717 717 padding: @pill-padding;
718 718 }
719 719 .pill-group {
720 720 .pill {
721 721 opacity: .8;
722 722 &:first-child {
723 723 border-radius: @border-radius 0 0 @border-radius;
724 724 }
725 725 &:last-child {
726 726 border-radius: 0 @border-radius @border-radius 0;
727 727 }
728 728 &:only-child {
729 729 border-radius: @border-radius;
730 730 }
731 731 }
732 732 }
733
733 734 .filediff {
734 735 border: 1px solid @grey5;
735 736
736 737 /* START OVERRIDES */
737 738 .code-highlight {
738 739 border: none; // TODO: remove this border from the global
739 740 // .code-highlight, it doesn't belong there
740 741 }
741 742 label {
742 743 margin: 0; // TODO: remove this margin definition from global label
743 744 // it doesn't belong there - if margin on labels
744 745 // are needed for a form they should be defined
745 746 // in the form's class
746 747 }
747 748 /* END OVERRIDES */
748 749
749 750 * {
750 751 box-sizing: border-box;
751 752 }
752 753 .filediff-anchor {
753 754 visibility: hidden;
754 755 }
755 756 &:hover {
756 757 .filediff-anchor {
757 758 visibility: visible;
758 759 }
759 760 }
760 761
761 762 .filediff-collapse-indicator {
762 763 width: 0;
763 764 height: 0;
764 765 border-style: solid;
765 766 float: left;
766 767 margin: 2px 2px 0 0;
767 768 cursor: pointer;
768 769 }
769 770
770 771 .filediff-heading {
771 772 background: @grey7;
772 773 cursor: pointer;
773 774 display: block;
774 775 padding: 5px 10px;
775 776 }
776 777 .filediff-heading:after {
777 778 content: "";
778 779 display: table;
779 780 clear: both;
780 781 }
781 782 .filediff-heading:hover {
782 783 background: #e1e9f4 !important;
783 784 }
784 785
785 786 .filediff-menu {
786 787 float: right;
787 788
788 a, span {
789 &> a, &> span {
789 790 padding: 5px;
790 791 display: block;
791 792 float: left
792 793 }
793 794 }
795
794 796 .pill {
795 797 &[op="name"] {
796 798 background: none;
797 799 color: @grey2;
798 800 opacity: 1;
799 801 color: white;
800 802 }
801 803 &[op="limited"] {
802 804 background: @grey2;
803 805 color: white;
804 806 }
805 807 &[op="binary"] {
806 808 background: @color7;
807 809 color: white;
808 810 }
809 811 &[op="modified"] {
810 812 background: @alert1;
811 813 color: white;
812 814 }
813 815 &[op="renamed"] {
814 816 background: @color4;
815 817 color: white;
816 818 }
817 819 &[op="mode"] {
818 820 background: @grey3;
819 821 color: white;
820 822 }
821 823 &[op="symlink"] {
822 824 background: @color8;
823 825 color: white;
824 826 }
825 827
826 828 &[op="added"] { /* added lines */
827 829 background: @alert1;
828 830 color: white;
829 831 }
830 832 &[op="deleted"] { /* deleted lines */
831 833 background: @alert2;
832 834 color: white;
833 835 }
834 836
835 837 &[op="created"] { /* created file */
836 838 background: @alert1;
837 839 color: white;
838 840 }
839 841 &[op="removed"] { /* deleted file */
840 842 background: @color5;
841 843 color: white;
842 844 }
843 845 }
844 846
845 847 .filediff-collapse-button, .filediff-expand-button {
846 848 cursor: pointer;
847 849 }
848 850 .filediff-collapse-button {
849 851 display: inline;
850 852 }
851 853 .filediff-expand-button {
852 854 display: none;
853 855 }
854 856 .filediff-collapsed .filediff-collapse-button {
855 857 display: none;
856 858 }
857 859 .filediff-collapsed .filediff-expand-button {
858 860 display: inline;
859 861 }
862
863 @comment-padding: 5px;
864
865 /**** COMMENTS ****/
866
867 .filediff-menu {
868 .show-comment-button {
869 display: none;
870 }
871 }
872 &.hide-comments {
873 .inline-comments {
874 display: none;
875 }
876 .filediff-menu {
877 .show-comment-button {
878 display: inline;
879 }
880 .show-comment-button {
881 display: none;
882 }
883 }
884 }
885 .inline-comments {
886 border-radius: @border-radius;
887 background: @grey6;
888 .comment {
889 margin: 0;
890 border-radius: @border-radius;
891 }
892 .comment-outdated {
893 opacity: 0.5;
894 }
895 .comment-inline {
896 background: white;
897 padding: (@comment-padding + 3px) @comment-padding;
898 border: @comment-padding solid @grey6;
899
900 .text {
901 border: none;
902 }
903 .meta {
904 border-bottom: 1px solid @grey6;
905 padding-bottom: 10px;
906 }
907 }
908 .comment-selected {
909 border-left: 6px solid @comment-highlight-color;
910 }
911 .comment-inline-form {
912 padding: @comment-padding;
913 display: none;
914 }
915 .cb-comment-add-button {
916 margin: @comment-padding;
917 }
918 /* hide add comment button when form is open */
919 .comment-inline-form-open + .cb-comment-add-button {
920 display: none;
921 }
922 .comment-inline-form-open {
923 display: block;
924 }
925 /* hide add comment button when form but no comments */
926 .comment-inline-form:first-child + .cb-comment-add-button {
927 display: none;
928 }
929 /* hide add comment button when no comments or form */
930 .cb-comment-add-button:first-child {
931 display: none;
932 }
933 /* hide add comment button when only comment is being deleted */
934 .comment-deleting:first-child + .cb-comment-add-button {
935 display: none;
936 }
937 }
938 /**** END COMMENTS ****/
939
860 940 }
941
942
861 943 table.cb {
862 944 width: 100%;
863 945 border-collapse: collapse;
864 946
865 947 .cb-text {
866 948 padding: @cb-text-padding;
867 949 }
868 950 .cb-hunk {
869 951 padding: @cb-text-padding;
870 952 }
871 953 .cb-expand {
872 954 display: none;
873 955 }
874 956 .cb-collapse {
875 957 display: inline;
876 958 }
877 959 &.cb-collapsed {
878 960 .cb-line {
879 961 display: none;
880 962 }
881 963 .cb-expand {
882 964 display: inline;
883 965 }
884 966 .cb-collapse {
885 967 display: none;
886 968 }
887 969 }
888 970
889 971 /* intentionally general selector since .cb-line-selected must override it
890 972 and they both use !important since the td itself may have a random color
891 973 generated by annotation blocks. TLDR: if you change it, make sure
892 974 annotated block selection and line selection in file view still work */
893 975 .cb-line-fresh .cb-content {
894 976 background: white !important;
895 977 }
896 978 .cb-warning {
897 979 background: #fff4dd;
898 980 }
899 981
900 982 &.cb-diff-sideside {
901 983 td {
902 984 &.cb-content {
903 985 width: 50%;
904 986 }
905 987 }
906 988 }
907 989
908 990 tr {
909 991 &.cb-annotate {
910 992 border-top: 1px solid #eee;
911 993
912 994 &+ .cb-line {
913 995 border-top: 1px solid #eee;
914 996 }
915 997
916 998 &:first-child {
917 999 border-top: none;
918 1000 &+ .cb-line {
919 1001 border-top: none;
920 1002 }
921 1003 }
922 1004 }
923 1005
924 1006 &.cb-hunk {
925 1007 font-family: @font-family-monospace;
926 1008 color: rgba(0, 0, 0, 0.3);
927 1009
928 1010 td {
929 1011 &:first-child {
930 1012 background: #edf2f9;
931 1013 }
932 1014 &:last-child {
933 1015 background: #f4f7fb;
934 1016 }
935 1017 }
936 1018 }
937 1019 }
938 1020
939 1021 td {
940 1022 vertical-align: top;
941 1023 padding: 0;
942 1024
943 1025 &.cb-content {
944 1026 font-size: 12.35px;
945 1027
946 1028 &.cb-line-selected .cb-code {
947 1029 background: @comment-highlight-color !important;
948 1030 }
949 1031
950 1032 span.cb-code {
951 1033 line-height: @cb-line-height;
952 1034 padding-left: @cb-line-code-padding;
953 1035 padding-right: @cb-line-code-padding;
954 1036 display: block;
955 1037 white-space: pre-wrap;
956 1038 font-family: @font-family-monospace;
957 1039 word-break: break-word;
958 1040 }
1041
1042 &> button.cb-comment-box-opener {
1043 padding: 2px 6px 2px 6px;
1044 margin-left: -20px;
1045 margin-top: -2px;
1046 border-radius: @border-radius;
1047 position: absolute;
1048 display: none;
1049 }
1050 .cb-comment {
1051 margin-top: 10px;
1052 white-space: normal;
1053 }
1054 }
1055 &:hover {
1056 button.cb-comment-box-opener {
1057 display: block;
1058 }
1059 &+ td button.cb-comment-box-opener {
1060 display: block
1061 }
959 1062 }
960 1063
961 1064 &.cb-lineno {
962 1065 padding: 0;
963 1066 width: 50px;
964 1067 color: rgba(0, 0, 0, 0.3);
965 1068 text-align: right;
966 1069 border-right: 1px solid #eee;
967 1070 font-family: @font-family-monospace;
968 1071
969 1072 a::before {
970 1073 content: attr(data-line-no);
971 1074 }
972 1075 &.cb-line-selected a {
973 1076 background: @comment-highlight-color !important;
974 1077 }
975 1078
976 1079 a {
977 1080 display: block;
978 1081 padding-right: @cb-line-code-padding;
979 1082 padding-left: @cb-line-code-padding;
980 1083 line-height: @cb-line-height;
981 1084 color: rgba(0, 0, 0, 0.3);
982 1085 }
983 1086 }
984 1087
985 1088 &.cb-empty {
986 1089 background: @grey7;
987 1090 }
988 1091
989 1092 ins {
990 1093 color: black;
991 1094 background: #a6f3a6;
992 1095 text-decoration: none;
993 1096 }
994 1097 del {
995 1098 color: black;
996 1099 background: #f8cbcb;
997 1100 text-decoration: none;
998 1101 }
999 1102 &.cb-addition {
1000 1103 background: #ecffec;
1001 1104
1002 1105 &.blob-lineno {
1003 1106 background: #ddffdd;
1004 1107 }
1005 1108 }
1006 1109 &.cb-deletion {
1007 1110 background: #ffecec;
1008 1111
1009 1112 &.blob-lineno {
1010 1113 background: #ffdddd;
1011 1114 }
1012 1115 }
1013 1116
1014 1117 &.cb-annotate-info {
1015 1118 width: 320px;
1016 1119 min-width: 320px;
1017 1120 max-width: 320px;
1018 1121 padding: 5px 2px;
1019 1122 font-size: 13px;
1020 1123
1021 1124 strong.cb-annotate-message {
1022 1125 padding: 5px 0;
1023 1126 white-space: pre-line;
1024 1127 display: inline-block;
1025 1128 }
1026 1129 .rc-user {
1027 1130 float: none;
1028 1131 padding: 0 6px 0 17px;
1029 1132 min-width: auto;
1030 1133 min-height: auto;
1031 1134 }
1032 1135 }
1033 1136
1034 1137 &.cb-annotate-revision {
1035 1138 cursor: pointer;
1036 1139 text-align: right;
1037 1140 }
1038 1141 }
1039 1142 }
@@ -1,85 +1,88 b''
1 1 //--- RESETS ---//
2 2 :focus { outline: none; }
3 3 a { cursor: pointer; }
4 4
5 5 //--- clearfix --//
6 6 .clearfix {
7 7 &:before,
8 8 &:after {
9 9 content:"";
10 10 width: 100%;
11 11 clear: both;
12 12 float: left;
13 13 }
14 14 }
15 15
16 16 .clearinner:after { /* clears all floating divs inside a block */
17 17 content: "";
18 18 display: table;
19 19 clear: both;
20 20 }
21 21
22 .js-template { /* mark a template for javascript use */
23 display: none;
24 }
22 25
23 26 .linebreak {
24 27 display: block;
25 28 }
26 29
27 30 .pull-right {
28 31 float: right !important;
29 32 }
30 33
31 34 .pull-left {
32 35 float: left !important;
33 36 }
34 37
35 38 .block-left {
36 39 float: left;
37 40 }
38 41
39 42 .block-right {
40 43 float: right;
41 44 clear: right;
42 45
43 46 li:before { content:none; }
44 47 }
45 48
46 49 //--- DEVICE-SPECIFIC CLASSES ---------------//
47 50 //regular tablet and up
48 51 @media (min-width:768px) {
49 52 .no-mobile {
50 53 display: block;
51 54 }
52 55 .mobile-only {
53 56 display: none;
54 57 }
55 58 }
56 59 //small tablet and phone
57 60 @media (max-width:767px) {
58 61 .mobile-only {
59 62 display: block;
60 63 }
61 64 .no-mobile {
62 65 display: none;
63 66 }
64 67 }
65 68
66 69 //--- STICKY FOOTER ---//
67 70 html, body {
68 71 height: 100%;
69 72 margin: 0;
70 73 }
71 74 .outerwrapper {
72 75 height: 100%;
73 76 min-height: 100%;
74 77 margin: 0;
75 78 padding-bottom: 3em; /* must be equal to footer height */
76 79 }
77 80 .outerwrapper:after{
78 81 content:" ";
79 82 }
80 83 #footer {
81 84 clear: both;
82 85 position: relative;
83 86 height: 3em; /* footer height */
84 87 margin: -3em 0 0; /* must be equal to footer height */
85 88 }
@@ -1,672 +1,672 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 // returns a node from given html;
29 29 var fromHTML = function(html){
30 30 var _html = document.createElement('element');
31 31 _html.innerHTML = html;
32 32 return _html;
33 33 };
34 34
35 35 var tableTr = function(cls, body){
36 36 var _el = document.createElement('div');
37 37 var _body = $(body).attr('id');
38 38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 39 var id = 'comment-tr-{0}'.format(comment_id);
40 40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 42 '<td></td>'+
43 43 '<td></td>'+
44 44 '<td></td>'+
45 45 '<td>{2}</td>'+
46 46 '</tr></tbody></table>').format(id, cls, body);
47 47 $(_el).html(_html);
48 48 return _el.children[0].children[0].children[0];
49 49 };
50 50
51 51 var removeInlineForm = function(form) {
52 52 form.parentNode.removeChild(form);
53 53 };
54 54
55 55 var createInlineForm = function(parent_tr, f_path, line) {
56 56 var tmpl = $('#comment-inline-form-template').html();
57 57 tmpl = tmpl.format(f_path, line);
58 58 var form = tableTr('comment-form-inline', tmpl);
59 59 var form_hide_button = $(form).find('.hide-inline-form');
60 60
61 61 $(form_hide_button).click(function(e) {
62 62 $('.inline-comments').removeClass('hide-comment-button');
63 63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
64 64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
65 65 $(newtr.nextElementSibling).show();
66 66 }
67 67 $(newtr).parents('.comment-form-inline').remove();
68 68 $(parent_tr).removeClass('form-open');
69 69 $(parent_tr).removeClass('hl-comment');
70 70 });
71 71
72 72 return form;
73 73 };
74 74
75 75 var getLineNo = function(tr) {
76 76 var line;
77 77 // Try to get the id and return "" (empty string) if it doesn't exist
78 78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
79 79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
80 80 if (n.length >= 2) {
81 81 line = n[n.length-1];
82 82 } else if (o.length >= 2) {
83 83 line = o[o.length-1];
84 84 }
85 85 return line;
86 86 };
87 87
88 88 /**
89 89 * make a single inline comment and place it inside
90 90 */
91 91 var renderInlineComment = function(json_data, show_add_button) {
92 92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
93 93 try {
94 94 var html = json_data.rendered_text;
95 95 var lineno = json_data.line_no;
96 96 var target_id = json_data.target_id;
97 97 placeInline(target_id, lineno, html, show_add_button);
98 98 } catch (e) {
99 99 console.error(e);
100 100 }
101 101 };
102 102
103 103 function bindDeleteCommentButtons() {
104 104 $('.delete-comment').one('click', function() {
105 105 var comment_id = $(this).data("comment-id");
106 106
107 107 if (comment_id){
108 108 deleteComment(comment_id);
109 109 }
110 110 });
111 111 }
112 112
113 113 /**
114 114 * Inject inline comment for on given TR this tr should be always an .line
115 115 * tr containing the line. Code will detect comment, and always put the comment
116 116 * block at the very bottom
117 117 */
118 118 var injectInlineForm = function(tr){
119 119 if (!$(tr).hasClass('line')) {
120 120 return;
121 121 }
122 122
123 123 var _td = $(tr).find('.code').get(0);
124 124 if ($(tr).hasClass('form-open') ||
125 125 $(tr).hasClass('context') ||
126 126 $(_td).hasClass('no-comment')) {
127 127 return;
128 128 }
129 129 $(tr).addClass('form-open');
130 130 $(tr).addClass('hl-comment');
131 131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
132 132 var f_path = $(node).attr('path');
133 133 var lineno = getLineNo(tr);
134 134 var form = createInlineForm(tr, f_path, lineno);
135 135
136 136 var parent = tr;
137 137 while (1) {
138 138 var n = parent.nextElementSibling;
139 139 // next element are comments !
140 140 if ($(n).hasClass('inline-comments')) {
141 141 parent = n;
142 142 }
143 143 else {
144 144 break;
145 145 }
146 146 }
147 147 var _parent = $(parent).get(0);
148 148 $(_parent).after(form);
149 149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
150 150 var f = $(form).get(0);
151 151
152 152 var _form = $(f).find('.inline-form').get(0);
153 153
154 154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
155 155 var commitId = templateContext.commit_data.commit_id;
156 156
157 157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
158 158 var cm = commentForm.getCmInstance();
159 159
160 160 // set a CUSTOM submit handler for inline comments.
161 161 commentForm.setHandleFormSubmit(function(o) {
162 162 var text = commentForm.cm.getValue();
163 163
164 164 if (text === "") {
165 165 return;
166 166 }
167 167
168 168 if (lineno === undefined) {
169 169 alert('missing line !');
170 170 return;
171 171 }
172 172 if (f_path === undefined) {
173 173 alert('missing file path !');
174 174 return;
175 175 }
176 176
177 177 var excludeCancelBtn = false;
178 178 var submitEvent = true;
179 179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
180 180 commentForm.cm.setOption("readOnly", true);
181 181 var postData = {
182 182 'text': text,
183 183 'f_path': f_path,
184 184 'line': lineno,
185 185 'csrf_token': CSRF_TOKEN
186 186 };
187 187 var submitSuccessCallback = function(o) {
188 188 $(tr).removeClass('form-open');
189 189 removeInlineForm(f);
190 190 renderInlineComment(o);
191 191 $('.inline-comments').removeClass('hide-comment-button');
192 192
193 193 // re trigger the linkification of next/prev navigation
194 194 linkifyComments($('.inline-comment-injected'));
195 195 timeagoActivate();
196 196 bindDeleteCommentButtons();
197 197 commentForm.setActionButtonsDisabled(false);
198 198
199 199 };
200 200 var submitFailCallback = function(){
201 201 commentForm.resetCommentFormState(text)
202 202 };
203 203 commentForm.submitAjaxPOST(
204 204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
205 205 });
206 206
207 207 setTimeout(function() {
208 208 // callbacks
209 209 if (cm !== undefined) {
210 210 cm.focus();
211 211 }
212 212 }, 10);
213 213
214 214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
215 215 form:_form,
216 216 parent:_parent,
217 217 lineno: lineno,
218 218 f_path: f_path}
219 219 );
220 220 };
221 221
222 222 var deleteComment = function(comment_id) {
223 223 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
224 224 var postData = {
225 225 '_method': 'delete',
226 226 'csrf_token': CSRF_TOKEN
227 227 };
228 228
229 229 var success = function(o) {
230 230 window.location.reload();
231 231 };
232 232 ajaxPOST(url, postData, success);
233 233 };
234 234
235 235 var createInlineAddButton = function(tr){
236 236 var label = _gettext('Add another comment');
237 237 var html_el = document.createElement('div');
238 238 $(html_el).addClass('add-comment');
239 239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
240 240 var add = new $(html_el);
241 241 add.on('click', function(e) {
242 242 injectInlineForm(tr);
243 243 });
244 244 return add;
245 245 };
246 246
247 247 var placeAddButton = function(target_tr){
248 248 if(!target_tr){
249 249 return;
250 250 }
251 251 var last_node = target_tr;
252 252 // scan
253 253 while (1){
254 254 var n = last_node.nextElementSibling;
255 255 // next element are comments !
256 256 if($(n).hasClass('inline-comments')){
257 257 last_node = n;
258 258 // also remove the comment button from previous
259 259 var comment_add_buttons = $(last_node).find('.add-comment');
260 260 for(var i=0; i<comment_add_buttons.length; i++){
261 261 var b = comment_add_buttons[i];
262 262 b.parentNode.removeChild(b);
263 263 }
264 264 }
265 265 else{
266 266 break;
267 267 }
268 268 }
269 269 var add = createInlineAddButton(target_tr);
270 270 // get the comment div
271 271 var comment_block = $(last_node).find('.comment')[0];
272 272 // attach add button
273 273 $(add).insertAfter(comment_block);
274 274 };
275 275
276 276 /**
277 277 * Places the inline comment into the changeset block in proper line position
278 278 */
279 279 var placeInline = function(target_container, lineno, html, show_add_button) {
280 280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
281 281
282 282 var lineid = "{0}_{1}".format(target_container, lineno);
283 283 var target_line = $('#' + lineid).get(0);
284 284 var comment = new $(tableTr('inline-comments', html));
285 285 // check if there are comments already !
286 286 if (target_line) {
287 287 var parent_node = target_line.parentNode;
288 288 var root_parent = parent_node;
289 289
290 290 while (1) {
291 291 var n = parent_node.nextElementSibling;
292 292 // next element are comments !
293 293 if ($(n).hasClass('inline-comments')) {
294 294 parent_node = n;
295 295 }
296 296 else {
297 297 break;
298 298 }
299 299 }
300 300 // put in the comment at the bottom
301 301 $(comment).insertAfter(parent_node);
302 302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
303 303 // scan nodes, and attach add button to last one
304 304 if (show_add_button) {
305 305 placeAddButton(root_parent);
306 306 }
307 307 addCommentToggle(target_line);
308 308 }
309 309
310 310 return target_line;
311 311 };
312 312
313 313 var addCommentToggle = function(target_line) {
314 314 // exposes comment toggle button
315 315 $(target_line).siblings('.comment-toggle').addClass('active');
316 316 return;
317 317 };
318 318
319 319 var bindToggleButtons = function() {
320 320 $('.comment-toggle').on('click', function() {
321 321 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
322 322 });
323 323 };
324 324
325 325 var linkifyComments = function(comments) {
326
326 /* TODO: dan: remove this - it should no longer needed */
327 327 for (var i = 0; i < comments.length; i++) {
328 328 var comment_id = $(comments[i]).data('comment-id');
329 329 var prev_comment_id = $(comments[i - 1]).data('comment-id');
330 330 var next_comment_id = $(comments[i + 1]).data('comment-id');
331 331
332 332 // place next/prev links
333 333 if (prev_comment_id) {
334 334 $('#prev_c_' + comment_id).show();
335 335 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
336 336 'href', '#comment-' + prev_comment_id).removeClass('disabled');
337 337 }
338 338 if (next_comment_id) {
339 339 $('#next_c_' + comment_id).show();
340 340 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
341 341 'href', '#comment-' + next_comment_id).removeClass('disabled');
342 342 }
343 343 // place a first link to the total counter
344 344 if (i === 0) {
345 345 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
346 346 }
347 347 }
348 348
349 349 };
350
350
351 351 /**
352 352 * Iterates over all the inlines, and places them inside proper blocks of data
353 353 */
354 354 var renderInlineComments = function(file_comments, show_add_button) {
355 355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
356 356
357 357 for (var i = 0; i < file_comments.length; i++) {
358 358 var box = file_comments[i];
359 359
360 360 var target_id = $(box).attr('target_id');
361 361
362 362 // actually comments with line numbers
363 363 var comments = box.children;
364 364
365 365 for (var j = 0; j < comments.length; j++) {
366 366 var data = {
367 367 'rendered_text': comments[j].outerHTML,
368 368 'line_no': $(comments[j]).attr('line'),
369 369 'target_id': target_id
370 370 };
371 371 renderInlineComment(data, show_add_button);
372 372 }
373 373 }
374 374
375 375 // since order of injection is random, we're now re-iterating
376 376 // from correct order and filling in links
377 377 linkifyComments($('.inline-comment-injected'));
378 378 bindDeleteCommentButtons();
379 379 firefoxAnchorFix();
380 380 };
381 381
382 382
383 383 /* Comment form for main and inline comments */
384 384 var CommentForm = (function() {
385 385 "use strict";
386 386
387 387 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
388 388
389 389 this.withLineNo = function(selector) {
390 390 var lineNo = this.lineNo;
391 391 if (lineNo === undefined) {
392 392 return selector
393 393 } else {
394 394 return selector + '_' + lineNo;
395 395 }
396 396 };
397 397
398 398 this.commitId = commitId;
399 399 this.pullRequestId = pullRequestId;
400 400 this.lineNo = lineNo;
401 401 this.initAutocompleteActions = initAutocompleteActions;
402 402
403 403 this.previewButton = this.withLineNo('#preview-btn');
404 404 this.previewContainer = this.withLineNo('#preview-container');
405 405
406 406 this.previewBoxSelector = this.withLineNo('#preview-box');
407 407
408 408 this.editButton = this.withLineNo('#edit-btn');
409 409 this.editContainer = this.withLineNo('#edit-container');
410 410
411 411 this.cancelButton = this.withLineNo('#cancel-btn');
412 412
413 413 this.statusChange = '#change_status';
414 414 this.cmBox = this.withLineNo('#text');
415 415 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
416 416
417 417 this.submitForm = formElement;
418 418 this.submitButton = $(this.submitForm).find('input[type="submit"]');
419 419 this.submitButtonText = this.submitButton.val();
420 420
421 421 this.previewUrl = pyroutes.url('changeset_comment_preview',
422 422 {'repo_name': templateContext.repo_name});
423 423
424 424 // based on commitId, or pullReuqestId decide where do we submit
425 425 // out data
426 426 if (this.commitId){
427 427 this.submitUrl = pyroutes.url('changeset_comment',
428 428 {'repo_name': templateContext.repo_name,
429 429 'revision': this.commitId});
430 430
431 431 } else if (this.pullRequestId) {
432 432 this.submitUrl = pyroutes.url('pullrequest_comment',
433 433 {'repo_name': templateContext.repo_name,
434 434 'pull_request_id': this.pullRequestId});
435 435
436 436 } else {
437 437 throw new Error(
438 438 'CommentForm requires pullRequestId, or commitId to be specified.')
439 439 }
440 440
441 441 this.getCmInstance = function(){
442 442 return this.cm
443 443 };
444 444
445 445 var self = this;
446 446
447 447 this.getCommentStatus = function() {
448 448 return $(this.submitForm).find(this.statusChange).val();
449 449 };
450 450
451 451 this.isAllowedToSubmit = function() {
452 452 return !$(this.submitButton).prop('disabled');
453 453 };
454 454
455 455 this.initStatusChangeSelector = function(){
456 456 var formatChangeStatus = function(state, escapeMarkup) {
457 457 var originalOption = state.element;
458 458 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
459 459 '<span>' + escapeMarkup(state.text) + '</span>';
460 460 };
461 461 var formatResult = function(result, container, query, escapeMarkup) {
462 462 return formatChangeStatus(result, escapeMarkup);
463 463 };
464 464
465 465 var formatSelection = function(data, container, escapeMarkup) {
466 466 return formatChangeStatus(data, escapeMarkup);
467 467 };
468 468
469 469 $(this.submitForm).find(this.statusChange).select2({
470 470 placeholder: _gettext('Status Review'),
471 471 formatResult: formatResult,
472 472 formatSelection: formatSelection,
473 473 containerCssClass: "drop-menu status_box_menu",
474 474 dropdownCssClass: "drop-menu-dropdown",
475 475 dropdownAutoWidth: true,
476 476 minimumResultsForSearch: -1
477 477 });
478 478 $(this.submitForm).find(this.statusChange).on('change', function() {
479 479 var status = self.getCommentStatus();
480 480 if (status && !self.lineNo) {
481 481 $(self.submitButton).prop('disabled', false);
482 482 }
483 483 //todo, fix this name
484 484 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
485 485 self.cm.setOption('placeholder', placeholderText);
486 486 })
487 487 };
488 488
489 489 // reset the comment form into it's original state
490 490 this.resetCommentFormState = function(content) {
491 491 content = content || '';
492 492
493 493 $(this.editContainer).show();
494 494 $(this.editButton).hide();
495 495
496 496 $(this.previewContainer).hide();
497 497 $(this.previewButton).show();
498 498
499 499 this.setActionButtonsDisabled(true);
500 500 self.cm.setValue(content);
501 501 self.cm.setOption("readOnly", false);
502 502 };
503 503
504 504 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
505 505 failHandler = failHandler || function() {};
506 506 var postData = toQueryString(postData);
507 507 var request = $.ajax({
508 508 url: url,
509 509 type: 'POST',
510 510 data: postData,
511 511 headers: {'X-PARTIAL-XHR': true}
512 512 })
513 513 .done(function(data) {
514 514 successHandler(data);
515 515 })
516 516 .fail(function(data, textStatus, errorThrown){
517 517 alert(
518 518 "Error while submitting comment.\n" +
519 519 "Error code {0} ({1}).".format(data.status, data.statusText));
520 520 failHandler()
521 521 });
522 522 return request;
523 523 };
524 524
525 525 // overwrite a submitHandler, we need to do it for inline comments
526 526 this.setHandleFormSubmit = function(callback) {
527 527 this.handleFormSubmit = callback;
528 528 };
529 529
530 530 // default handler for for submit for main comments
531 531 this.handleFormSubmit = function() {
532 532 var text = self.cm.getValue();
533 533 var status = self.getCommentStatus();
534 534
535 535 if (text === "" && !status) {
536 536 return;
537 537 }
538 538
539 539 var excludeCancelBtn = false;
540 540 var submitEvent = true;
541 541 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
542 542 self.cm.setOption("readOnly", true);
543 543 var postData = {
544 544 'text': text,
545 545 'changeset_status': status,
546 546 'csrf_token': CSRF_TOKEN
547 547 };
548 548
549 549 var submitSuccessCallback = function(o) {
550 550 if (status) {
551 551 location.reload(true);
552 552 } else {
553 553 $('#injected_page_comments').append(o.rendered_text);
554 554 self.resetCommentFormState();
555 555 bindDeleteCommentButtons();
556 556 timeagoActivate();
557 557 }
558 558 };
559 559 var submitFailCallback = function(){
560 560 self.resetCommentFormState(text)
561 561 };
562 562 self.submitAjaxPOST(
563 563 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
564 564 };
565 565
566 566 this.previewSuccessCallback = function(o) {
567 567 $(self.previewBoxSelector).html(o);
568 568 $(self.previewBoxSelector).removeClass('unloaded');
569 569
570 570 // swap buttons
571 571 $(self.previewButton).hide();
572 572 $(self.editButton).show();
573 573
574 574 // unlock buttons
575 575 self.setActionButtonsDisabled(false);
576 576 };
577 577
578 578 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
579 579 excludeCancelBtn = excludeCancelBtn || false;
580 580 submitEvent = submitEvent || false;
581 581
582 582 $(this.editButton).prop('disabled', state);
583 583 $(this.previewButton).prop('disabled', state);
584 584
585 585 if (!excludeCancelBtn) {
586 586 $(this.cancelButton).prop('disabled', state);
587 587 }
588 588
589 589 var submitState = state;
590 590 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
591 591 // if the value of commit review status is set, we allow
592 592 // submit button, but only on Main form, lineNo means inline
593 593 submitState = false
594 594 }
595 595 $(this.submitButton).prop('disabled', submitState);
596 596 if (submitEvent) {
597 597 $(this.submitButton).val(_gettext('Submitting...'));
598 598 } else {
599 599 $(this.submitButton).val(this.submitButtonText);
600 600 }
601 601
602 602 };
603 603
604 604 // lock preview/edit/submit buttons on load, but exclude cancel button
605 605 var excludeCancelBtn = true;
606 606 this.setActionButtonsDisabled(true, excludeCancelBtn);
607 607
608 608 // anonymous users don't have access to initialized CM instance
609 609 if (this.cm !== undefined){
610 610 this.cm.on('change', function(cMirror) {
611 611 if (cMirror.getValue() === "") {
612 612 self.setActionButtonsDisabled(true, excludeCancelBtn)
613 613 } else {
614 614 self.setActionButtonsDisabled(false, excludeCancelBtn)
615 615 }
616 616 });
617 617 }
618 618
619 619 $(this.editButton).on('click', function(e) {
620 620 e.preventDefault();
621 621
622 622 $(self.previewButton).show();
623 623 $(self.previewContainer).hide();
624 624 $(self.editButton).hide();
625 625 $(self.editContainer).show();
626 626
627 627 });
628 628
629 629 $(this.previewButton).on('click', function(e) {
630 630 e.preventDefault();
631 631 var text = self.cm.getValue();
632 632
633 633 if (text === "") {
634 634 return;
635 635 }
636 636
637 637 var postData = {
638 638 'text': text,
639 639 'renderer': DEFAULT_RENDERER,
640 640 'csrf_token': CSRF_TOKEN
641 641 };
642 642
643 643 // lock ALL buttons on preview
644 644 self.setActionButtonsDisabled(true);
645 645
646 646 $(self.previewBoxSelector).addClass('unloaded');
647 647 $(self.previewBoxSelector).html(_gettext('Loading ...'));
648 648 $(self.editContainer).hide();
649 649 $(self.previewContainer).show();
650 650
651 651 // by default we reset state of comment preserving the text
652 652 var previewFailCallback = function(){
653 653 self.resetCommentFormState(text)
654 654 };
655 655 self.submitAjaxPOST(
656 656 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
657 657
658 658 });
659 659
660 660 $(this.submitForm).submit(function(e) {
661 661 e.preventDefault();
662 662 var allowedToSubmit = self.isAllowedToSubmit();
663 663 if (!allowedToSubmit){
664 664 return false;
665 665 }
666 666 self.handleFormSubmit();
667 667 });
668 668
669 669 }
670 670
671 671 return CommentForm;
672 672 })();
@@ -1,175 +1,387 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10
11 11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
15 15 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.name
16 16 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.lastname
17 17
18 18 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
19 19 %>
20 20 <html xmlns="http://www.w3.org/1999/xhtml">
21 21 <head>
22 22 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}"></script>
23 23 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
24 24 <title>${self.title()}</title>
25 25 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
26 26 <%def name="robots()">
27 27 <meta name="robots" content="index, nofollow"/>
28 28 </%def>
29 29 ${self.robots()}
30 30 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
31 31
32 32 ## CSS definitions
33 33 <%def name="css()">
34 34 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
35 35 <!--[if lt IE 9]>
36 36 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
37 37 <![endif]-->
38 38 ## EXTRA FOR CSS
39 39 ${self.css_extra()}
40 40 </%def>
41 41 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
42 42 <%def name="css_extra()">
43 43 </%def>
44 44
45 45 ${self.css()}
46 46
47 47 ## JAVASCRIPT
48 48 <%def name="js()">
49 49 <script>
50 50 // setup Polymer options
51 51 window.Polymer = {lazyRegister: true, dom: 'shadow'};
52 52
53 53 // Load webcomponentsjs polyfill if browser does not support native Web Components
54 54 (function() {
55 55 'use strict';
56 56 var onload = function() {
57 57 // For native Imports, manually fire WebComponentsReady so user code
58 58 // can use the same code path for native and polyfill'd imports.
59 59 if (!window.HTMLImports) {
60 60 document.dispatchEvent(
61 61 new CustomEvent('WebComponentsReady', {bubbles: true})
62 62 );
63 63 }
64 64 };
65 65 var webComponentsSupported = (
66 66 'registerElement' in document
67 67 && 'import' in document.createElement('link')
68 68 && 'content' in document.createElement('template')
69 69 );
70 70 if (!webComponentsSupported) {
71 71 } else {
72 72 onload();
73 73 }
74 74 })();
75 75 </script>
76 76
77 77 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
78 78 <script type="text/javascript">
79 79 // register templateContext to pass template variables to JS
80 80 var templateContext = ${h.json.dumps(c.template_context)|n};
81 81
82 82 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
83 83 %if hasattr(c, 'rhodecode_db_repo'):
84 84 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
85 85 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
86 86 %else:
87 87 var REPO_LANDING_REV = '';
88 88 var REPO_TYPE = '';
89 89 %endif
90 90 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
91 91 var ASSET_URL = "${h.asset('')}";
92 92 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
93 93 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
94 94 % if getattr(c, 'rhodecode_user', None):
95 95 var USER = {name:'${c.rhodecode_user.username}'};
96 96 % else:
97 97 var USER = {name:null};
98 98 % endif
99 99
100 100 var APPENLIGHT = {
101 101 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
102 102 key: '${getattr(c, "appenlight_api_public_key", "")}',
103 103 % if getattr(c, 'appenlight_server_url', None):
104 104 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
105 105 % endif
106 106 requestInfo: {
107 107 % if getattr(c, 'rhodecode_user', None):
108 108 ip: '${c.rhodecode_user.ip_addr}',
109 109 username: '${c.rhodecode_user.username}'
110 110 % endif
111 111 },
112 112 tags: {
113 113 rhodecode_version: '${c.rhodecode_version}',
114 114 rhodecode_edition: '${c.rhodecode_edition}'
115 115 }
116 116 };
117
118
119 Rhodecode = (function() {
120 function _Rhodecode() {
121 this.comments = new (function() { /* comments controller */
122 var self = this;
123
124 this.cancelComment = function(node) {
125 var $node = $(node);
126 var $td = $node.closest('td');
127 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
128 return false;
129 }
130 this.getLineNumber = function(node) {
131 var $node = $(node);
132 return $node.closest('td').attr('data-line-number');
133 }
134 this.scrollToComment = function(node, offset) {
135 if (!node) {
136 node = $('.comment-selected');
137 if (!node.length) {
138 node = $('comment-current')
139 }
140 }
141 $comment = $(node).closest('.comment-current');
142 $comments = $('.comment-current');
143
144 $('.comment-selected').removeClass('comment-selected');
145
146 var nextIdx = $('.comment-current').index($comment) + offset;
147 if (nextIdx >= $comments.length) {
148 nextIdx = 0;
149 }
150 var $next = $('.comment-current').eq(nextIdx);
151 var $cb = $next.closest('.cb');
152 $cb.removeClass('cb-collapsed')
153
154 var $filediffCollapseState = $cb.closest('.filediff').prev();
155 $filediffCollapseState.prop('checked', false);
156 $next.addClass('comment-selected');
157 scrollToElement($next);
158 return false;
159 }
160 this.nextComment = function(node) {
161 return self.scrollToComment(node, 1);
162 }
163 this.prevComment = function(node) {
164 return self.scrollToComment(node, -1);
165 }
166 this.deleteComment = function(node) {
167 if (!confirm(_gettext('Delete this comment?'))) {
168 return false;
169 }
170 var $node = $(node);
171 var $td = $node.closest('td');
172 var $comment = $node.closest('.comment');
173 var comment_id = $comment.attr('data-comment-id');
174 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
175 var postData = {
176 '_method': 'delete',
177 'csrf_token': CSRF_TOKEN
178 };
179
180 $comment.addClass('comment-deleting');
181 $comment.hide('fast');
182
183 var success = function(response) {
184 $comment.remove();
185 return false;
186 };
187 var failure = function(data, textStatus, xhr) {
188 alert("error processing request: " + textStatus);
189 $comment.show('fast');
190 $comment.removeClass('comment-deleting');
191 return false;
192 };
193 ajaxPOST(url, postData, success, failure);
194 }
195 this.createComment = function(node) {
196 var $node = $(node);
197 var $td = $node.closest('td');
198 var $form = $td.find('.comment-inline-form');
199
200 if (!$form.length) {
201 var tmpl = $('#cb-comment-inline-form-template').html();
202 var f_path = $node.closest('.filediff').attr('data-f-path');
203 var lineno = self.getLineNumber(node);
204 tmpl = tmpl.format(f_path, lineno);
205 $form = $(tmpl);
206
207 var $comments = $td.find('.inline-comments');
208 if (!$comments.length) {
209 $comments = $(
210 $('#cb-comments-inline-container-template').html());
211 $td.append($comments);
212 }
213
214 $td.find('.cb-comment-add-button').before($form);
215
216 var pullRequestId = templateContext.pull_request_data.pull_request_id;
217 var commitId = templateContext.commit_data.commit_id;
218 var _form = $form[0];
219 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
220 var cm = commentForm.getCmInstance();
221
222 // set a CUSTOM submit handler for inline comments.
223 commentForm.setHandleFormSubmit(function(o) {
224 var text = commentForm.cm.getValue();
225
226 if (text === "") {
227 return;
228 }
229
230 if (lineno === undefined) {
231 alert('missing line !');
232 return;
233 }
234 if (f_path === undefined) {
235 alert('missing file path !');
236 return;
237 }
238
239 var excludeCancelBtn = false;
240 var submitEvent = true;
241 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
242 commentForm.cm.setOption("readOnly", true);
243 var postData = {
244 'text': text,
245 'f_path': f_path,
246 'line': lineno,
247 'csrf_token': CSRF_TOKEN
248 };
249 var submitSuccessCallback = function(json_data) {
250 $form.remove();
251 console.log(json_data)
252 try {
253 var html = json_data.rendered_text;
254 var lineno = json_data.line_no;
255 var target_id = json_data.target_id;
256
257 $comments.find('.cb-comment-add-button').before(html);
258 console.log(lineno, target_id, $comments);
259
260 } catch (e) {
261 console.error(e);
262 }
263
264
265 // re trigger the linkification of next/prev navigation
266 linkifyComments($('.inline-comment-injected'));
267 timeagoActivate();
268 bindDeleteCommentButtons();
269 commentForm.setActionButtonsDisabled(false);
270
271 };
272 var submitFailCallback = function(){
273 commentForm.resetCommentFormState(text)
274 };
275 commentForm.submitAjaxPOST(
276 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
277 });
278
279 setTimeout(function() {
280 // callbacks
281 if (cm !== undefined) {
282 cm.focus();
283 }
284 }, 10);
285
286 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
287 form: _form,
288 parent: $td[0],
289 lineno: lineno,
290 f_path: f_path}
291 );
292 }
293
294 $form.addClass('comment-inline-form-open');
295 }
296
297 this.renderInlineComments = function(file_comments) {
298 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
299
300 for (var i = 0; i < file_comments.length; i++) {
301 var box = file_comments[i];
302
303 var target_id = $(box).attr('target_id');
304
305 // actually comments with line numbers
306 var comments = box.children;
307
308 for (var j = 0; j < comments.length; j++) {
309 var data = {
310 'rendered_text': comments[j].outerHTML,
311 'line_no': $(comments[j]).attr('line'),
312 'target_id': target_id
313 };
314 }
315 }
316
317 // since order of injection is random, we're now re-iterating
318 // from correct order and filling in links
319 linkifyComments($('.inline-comment-injected'));
320 bindDeleteCommentButtons();
321 firefoxAnchorFix();
322 };
323
324 })();
325 }
326 return new _Rhodecode();
327 })();
328
117 329 </script>
118 330 <%include file="/base/plugins_base.html"/>
119 331 <!--[if lt IE 9]>
120 332 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
121 333 <![endif]-->
122 334 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
123 335 <script> var alertMessagePayloads = ${h.flash.json_alerts()|n}; </script>
124 336 ## avoide escaping the %N
125 337 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}"></script>
126 338 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
127 339
128 340
129 341 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
130 342 ${self.js_extra()}
131 343
132 344 <script type="text/javascript">
133 345 $(document).ready(function(){
134 346 show_more_event();
135 347 timeagoActivate();
136 348 })
137 349 </script>
138 350
139 351 </%def>
140 352
141 353 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
142 354 <%def name="js_extra()"></%def>
143 355 ${self.js()}
144 356
145 357 <%def name="head_extra()"></%def>
146 358 ${self.head_extra()}
147 359 ## extra stuff
148 360 %if c.pre_code:
149 361 ${c.pre_code|n}
150 362 %endif
151 363 </head>
152 364 <body id="body">
153 365 <noscript>
154 366 <div class="noscript-error">
155 367 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
156 368 </div>
157 369 </noscript>
158 370 ## IE hacks
159 371 <!--[if IE 7]>
160 372 <script>$(document.body).addClass('ie7')</script>
161 373 <![endif]-->
162 374 <!--[if IE 8]>
163 375 <script>$(document.body).addClass('ie8')</script>
164 376 <![endif]-->
165 377 <!--[if IE 9]>
166 378 <script>$(document.body).addClass('ie9')</script>
167 379 <![endif]-->
168 380
169 381 ${next.body()}
170 382 %if c.post_code:
171 383 ${c.post_code|n}
172 384 %endif
173 385 <rhodecode-app></rhodecode-app>
174 386 </body>
175 387 </html>
@@ -1,317 +1,315 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
5 5
6 6 <%def name="title()">
7 7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 8 %if c.rhodecode_name:
9 9 &middot; ${h.branding(c.rhodecode_name)}
10 10 %endif
11 11 </%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <script>
23 23 // TODO: marcink switch this to pyroutes
24 24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 26 </script>
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 30 </div>
31 31
32 32 <div id="changeset_compare_view_content" class="summary changeset">
33 33 <div class="summary-detail">
34 34 <div class="summary-detail-header">
35 35 <span class="breadcrumbs files_location">
36 36 <h4>${_('Commit')}
37 37 <code>
38 38 ${h.show_id(c.commit)}
39 39 </code>
40 40 </h4>
41 41 </span>
42 42 <span id="parent_link">
43 43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 44 </span>
45 45 |
46 46 <span id="child_link">
47 47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 48 </span>
49 49 </div>
50 50
51 51 <div class="fieldset">
52 52 <div class="left-label">
53 53 ${_('Description')}:
54 54 </div>
55 55 <div class="right-content">
56 56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 57 <div id="message_expand" style="display:none;">
58 58 ${_('Expand')}
59 59 </div>
60 60 </div>
61 61 </div>
62 62
63 63 %if c.statuses:
64 64 <div class="fieldset">
65 65 <div class="left-label">
66 66 ${_('Commit status')}:
67 67 </div>
68 68 <div class="right-content">
69 69 <div class="changeset-status-ico">
70 70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 71 </div>
72 72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 73 </div>
74 74 </div>
75 75 %endif
76 76
77 77 <div class="fieldset">
78 78 <div class="left-label">
79 79 ${_('References')}:
80 80 </div>
81 81 <div class="right-content">
82 82 <div class="tags">
83 83
84 84 %if c.commit.merge:
85 85 <span class="mergetag tag">
86 86 <i class="icon-merge"></i>${_('merge')}
87 87 </span>
88 88 %endif
89 89
90 90 %if h.is_hg(c.rhodecode_repo):
91 91 %for book in c.commit.bookmarks:
92 92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 94 </span>
95 95 %endfor
96 96 %endif
97 97
98 98 %for tag in c.commit.tags:
99 99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 101 </span>
102 102 %endfor
103 103
104 104 %if c.commit.branch:
105 105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 107 </span>
108 108 %endif
109 109 </div>
110 110 </div>
111 111 </div>
112 112
113 113 <div class="fieldset">
114 114 <div class="left-label">
115 115 ${_('Diffs')}:
116 116 </div>
117 117 <div class="right-content">
118 118 <div class="diff-actions">
119 119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 120 ${_('Raw Diff')}
121 121 </a>
122 122 |
123 123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 124 ${_('Patch Diff')}
125 125 </a>
126 126 |
127 127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 128 ${_('Download Diff')}
129 129 </a>
130 130 |
131 131 ${c.ignorews_url(request.GET)}
132 132 |
133 133 ${c.context_url(request.GET)}
134 134 </div>
135 135 </div>
136 136 </div>
137 137
138 138 <div class="fieldset">
139 139 <div class="left-label">
140 140 ${_('Comments')}:
141 141 </div>
142 142 <div class="right-content">
143 143 <div class="comments-number">
144 144 %if c.comments:
145 145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 146 %else:
147 147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 148 %endif
149 149 %if c.inline_cnt:
150 ## this is replaced with a proper link to first comment via JS linkifyComments() func
151 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
152 151 %else:
153 152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
154 153 %endif
155 154 </div>
156 155 </div>
157 156 </div>
158 157
159 158 </div> <!-- end summary-detail -->
160 159
161 160 <div id="commit-stats" class="sidebar-right">
162 161 <div class="summary-detail-header">
163 162 <h4 class="item">
164 163 ${_('Author')}
165 164 </h4>
166 165 </div>
167 166 <div class="sidebar-right-content">
168 167 ${self.gravatar_with_user(c.commit.author)}
169 168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
170 169 </div>
171 170 </div><!-- end sidebar -->
172 171 </div> <!-- end summary -->
173 172 <div class="cs_files">
174 ${cbdiffs.render_diffset_menu()}
175
176 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
177 ${cbdiffs.render_diffset(c.changes[c.commit.raw_id], commit=c.commit)}
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
174 ${cbdiffs.render_diffset_menu()}
175 ${cbdiffs.render_diffset(
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
178 177 </div>
179 </div>
180 178
181 179 ## template for inline comment form
182 180 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
183 181 ${comment.comment_inline_form()}
184 182
185 ## render comments and inlines
183 ## ## render comments and inlines
186 184 ${comment.generate_comments()}
187 185
188 186 ## main comment form and it status
189 187 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
190 188 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
189 </div>
191 190
192 191 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
193 192 <script type="text/javascript">
194 193
195 194 $(document).ready(function() {
196 195
197 196 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
198 197 if($('#trimmed_message_box').height() === boxmax){
199 198 $('#message_expand').show();
200 199 }
201 200
202 201 $('#message_expand').on('click', function(e){
203 202 $('#trimmed_message_box').css('max-height', 'none');
204 203 $(this).hide();
205 204 });
206 205
207 206 $('.show-inline-comments').on('click', function(e){
208 207 var boxid = $(this).attr('data-comment-id');
209 208 var button = $(this);
210 209
211 210 if(button.hasClass("comments-visible")) {
212 211 $('#{0} .inline-comments'.format(boxid)).each(function(index){
213 212 $(this).hide();
214 213 })
215 214 button.removeClass("comments-visible");
216 215 } else {
217 216 $('#{0} .inline-comments'.format(boxid)).each(function(index){
218 217 $(this).show();
219 218 })
220 219 button.addClass("comments-visible");
221 220 }
222 221 });
223 222
224 223
225 224 // next links
226 225 $('#child_link').on('click', function(e){
227 226 // fetch via ajax what is going to be the next link, if we have
228 227 // >1 links show them to user to choose
229 228 if(!$('#child_link').hasClass('disabled')){
230 229 $.ajax({
231 230 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
232 231 success: function(data) {
233 232 if(data.results.length === 0){
234 233 $('#child_link').html('${_('No Child Commits')}').addClass('disabled');
235 234 }
236 235 if(data.results.length === 1){
237 236 var commit = data.results[0];
238 237 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
239 238 }
240 239 else if(data.results.length === 2){
241 240 $('#child_link').addClass('disabled');
242 241 $('#child_link').addClass('double');
243 242 var _html = '';
244 243 _html +='<a title="__title__" href="__url__">__rev__</a> '
245 244 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
246 245 .replace('__title__', data.results[0].message)
247 246 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
248 247 _html +=' | '
249 248 _html +='<a title="__title__" href="__url__">__rev__</a> '
250 249 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
251 250 .replace('__title__', data.results[1].message)
252 251 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
253 252 $('#child_link').html(_html);
254 253 }
255 254 }
256 255 });
257 256 e.preventDefault();
258 257 }
259 258 });
260 259
261 260 // prev links
262 261 $('#parent_link').on('click', function(e){
263 262 // fetch via ajax what is going to be the next link, if we have
264 263 // >1 links show them to user to choose
265 264 if(!$('#parent_link').hasClass('disabled')){
266 265 $.ajax({
267 266 url: '${h.url('changeset_parents',repo_name=c.repo_name, revision=c.commit.raw_id)}',
268 267 success: function(data) {
269 268 if(data.results.length === 0){
270 269 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
271 270 }
272 271 if(data.results.length === 1){
273 272 var commit = data.results[0];
274 273 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
275 274 }
276 275 else if(data.results.length === 2){
277 276 $('#parent_link').addClass('disabled');
278 277 $('#parent_link').addClass('double');
279 278 var _html = '';
280 279 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
281 280 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
282 281 .replace('__title__', data.results[0].message)
283 282 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
284 283 _html +=' | '
285 284 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
286 285 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
287 286 .replace('__title__', data.results[1].message)
288 287 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
289 288 $('#parent_link').html(_html);
290 289 }
291 290 }
292 291 });
293 292 e.preventDefault();
294 293 }
295 294 });
296 295
297 296 if (location.hash) {
298 297 var result = splitDelimitedHash(location.hash);
299 298 var line = $('html').find(result.loc);
300 299 if (line.length > 0){
301 300 offsetScroll(line, 70);
302 301 }
303 302 }
304 303
305 304 // browse tree @ revision
306 305 $('#files_link').on('click', function(e){
307 306 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
308 307 e.preventDefault();
309 308 });
310 309
311 310 // inject comments into their proper positions
312 311 var file_comments = $('.inline-comment-placeholder');
313 renderInlineComments(file_comments, true);
314 312 })
315 313 </script>
316 314
317 315 </%def>
@@ -1,312 +1,287 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.html"/>
7 7
8 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
10 class="comment
11 ${'comment-inline' if inline else ''}
12 ${'comment-outdated' if comment.outdated else 'comment-current'}"
13 "
14 id="comment-${comment.comment_id}"
15 line="${comment.line_no}"
16 data-comment-id="${comment.comment_id}">
10 17 <div class="meta">
11 18 <div class="author">
12 19 ${base.gravatar_with_user(comment.author.email, 16)}
13 20 </div>
14 21 <div class="date">
15 22 ${h.age_component(comment.modified_at, time_is_local=True)}
16 23 </div>
17 24 <div class="status-change">
18 25 %if comment.pull_request:
19 26 <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 27 %if comment.status_change:
21 28 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
22 29 %else:
23 30 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
24 31 %endif
25 32 </a>
26 33 %else:
27 34 %if comment.status_change:
28 35 ${_('Status change on commit')}:
29 36 %else:
30 37 ${_('Comment on commit')}
31 38 %endif
32 39 %endif
33 40 </div>
34 41 %if comment.status_change:
35 42 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
36 43 <div title="${_('Commit status')}" class="changeset-status-lbl">
37 44 ${comment.status_change[0].status_lbl}
38 45 </div>
39 46 %endif
40 47 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
41 48
42 49
43 50 <div class="comment-links-block">
44 51
45 52 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
46 53 ## only super-admin, repo admin OR comment owner can delete
47 54 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
48 55 %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>
50 %if inline:
51 <div class="comment-links-divider"> | </div>
56 ## TODO: dan: add edit comment here
57 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
58 %if not comment.outdated:
59 <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
60 <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
52 61 %endif
53 62 %endif
54 63 %endif
55 64
56 %if inline:
57
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>
60 </div>
61
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>
64 </div>
65 %endif
66
67 65 </div>
68 66 </div>
69 67 <div class="text">
70 68 ${comment.render(mentions=True)|n}
71 69 </div>
72 70 </div>
73 71 </%def>
74 72
75 73 <%def name="comment_block_outdated(comment)">
76 74 <div class="comments" id="comment-${comment.comment_id}">
77 75 <div class="comment comment-wrapp">
78 76 <div class="meta">
79 77 <div class="author">
80 78 ${base.gravatar_with_user(comment.author.email, 16)}
81 79 </div>
82 80 <div class="date">
83 81 ${h.age_component(comment.modified_at, time_is_local=True)}
84 82 </div>
85 83 %if comment.status_change:
86 84 <span class="changeset-status-container">
87 85 <span class="changeset-status-ico">
88 86 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
89 87 </span>
90 88 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
91 89 </span>
92 90 %endif
93 91 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
94 92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
95 93 ## only super-admin, repo admin OR comment owner can delete
96 94 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
97 95 <div class="comment-links-block">
98 96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
99 97 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
100 98 %endif
101 99 </div>
102 100 %endif
103 101 </div>
104 102 <div class="text">
105 103 ${comment.render(mentions=True)|n}
106 104 </div>
107 105 </div>
108 106 </div>
109 107 </%def>
110 108
111 109 <%def name="comment_inline_form()">
112 110 <div id="comment-inline-form-template" style="display: none;">
113 111 <div class="comment-inline-form ac">
114 112 %if c.rhodecode_user.username != h.DEFAULT_USER:
115 113 ${h.form('#', class_='inline-form', method='get')}
116 114 <div id="edit-container_{1}" class="clearfix">
117 115 <div class="comment-title pull-left">
118 116 ${_('Create a comment on line {1}.')}
119 117 </div>
120 118 <div class="comment-help pull-right">
121 119 ${(_('Comments parsed using %s syntax with %s support.') % (
122 120 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
123 121 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
124 122 )
125 123 )|n
126 124 }
127 125 </div>
128 126 <div style="clear: both"></div>
129 127 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
130 128 </div>
131 129 <div id="preview-container_{1}" class="clearfix" style="display: none;">
132 130 <div class="comment-help">
133 131 ${_('Comment preview')}
134 132 </div>
135 133 <div id="preview-box_{1}" class="preview-box"></div>
136 134 </div>
137 135 <div class="comment-footer">
138 136 <div class="comment-button hide-inline-form-button cancel-button">
139 137 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
140 138 </div>
141 139 <div class="action-buttons">
142 140 <input type="hidden" name="f_path" value="{0}">
143 141 <input type="hidden" name="line" value="{1}">
144 142 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
145 143 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
146 144 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
147 145 </div>
148 146 ${h.end_form()}
149 147 </div>
150 148 %else:
151 149 ${h.form('', class_='inline-form comment-form-login', method='get')}
152 150 <div class="pull-left">
153 151 <div class="comment-help pull-right">
154 152 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
155 153 </div>
156 154 </div>
157 155 <div class="comment-button pull-right">
158 156 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
159 157 </div>
160 158 <div class="clearfix"></div>
161 159 ${h.end_form()}
162 160 %endif
163 161 </div>
164 162 </div>
165 163 </%def>
166 164
167 165
168 ## generates inlines taken from c.comments var
169 <%def name="inlines(is_pull_request=False)">
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>
172 %else:
173 <h2 id="comments">${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}</h2>
174 %endif
175 %for path, lines_comments in c.inline_comments:
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))}">
178 ## for each comment in particular line
179 %for comment in comments:
180 ${comment_block(comment, inline=True)}
181 %endfor
182 </div>
183 %endfor
184 %endfor
185
186 </%def>
187
188 ## generate inline comments and the main ones
166 ## generate main comments
189 167 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
190 ## generate inlines for this changeset
191 ${inlines(is_pull_request)}
192
193 168 %for comment in c.comments:
194 169 <div id="comment-tr-${comment.comment_id}">
195 170 ## only render comments that are not from pull request, or from
196 171 ## pull request and a status change
197 172 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
198 173 ${comment_block(comment)}
199 174 %endif
200 175 </div>
201 176 %endfor
202 177 ## to anchor ajax comments
203 178 <div id="injected_page_comments"></div>
204 179 </%def>
205 180
206 181 ## MAIN COMMENT FORM
207 182 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
208 183 %if is_compare:
209 184 <% form_id = "comments_form_compare" %>
210 185 %else:
211 186 <% form_id = "comments_form" %>
212 187 %endif
213 188
214 189
215 190 %if is_pull_request:
216 191 <div class="pull-request-merge">
217 192 %if c.allowed_to_merge:
218 193 <div class="pull-request-wrap">
219 194 <div class="pull-right">
220 195 ${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 196 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
222 197 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
223 198 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
224 199 ${h.end_form()}
225 200 </div>
226 201 </div>
227 202 %else:
228 203 <div class="pull-request-wrap">
229 204 <div class="pull-right">
230 205 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
231 206 </div>
232 207 </div>
233 208 %endif
234 209 </div>
235 210 %endif
236 211 <div class="comments">
237 212 %if c.rhodecode_user.username != h.DEFAULT_USER:
238 213 <div class="comment-form ac">
239 214 ${h.secure_form(post_url, id_=form_id)}
240 215 <div id="edit-container" class="clearfix">
241 216 <div class="comment-title pull-left">
242 217 %if is_pull_request:
243 218 ${(_('Create a comment on this Pull Request.'))}
244 219 %elif is_compare:
245 220 ${(_('Create comments on this Commit range.'))}
246 221 %else:
247 222 ${(_('Create a comment on this Commit.'))}
248 223 %endif
249 224 </div>
250 225 <div class="comment-help pull-right">
251 226 ${(_('Comments parsed using %s syntax with %s support.') % (
252 227 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
253 228 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
254 229 )
255 230 )|n
256 231 }
257 232 </div>
258 233 <div style="clear: both"></div>
259 234 ${h.textarea('text', class_="comment-block-ta")}
260 235 </div>
261 236
262 237 <div id="preview-container" class="clearfix" style="display: none;">
263 238 <div class="comment-title">
264 239 ${_('Comment preview')}
265 240 </div>
266 241 <div id="preview-box" class="preview-box"></div>
267 242 </div>
268 243
269 244 <div id="comment_form_extras">
270 245 %if form_extras and isinstance(form_extras, (list, tuple)):
271 246 % for form_ex_el in form_extras:
272 247 ${form_ex_el|n}
273 248 % endfor
274 249 %endif
275 250 </div>
276 251 <div class="comment-footer">
277 252 %if change_status:
278 253 <div class="status_box">
279 254 <select id="change_status" name="changeset_status">
280 255 <option></option> # Placeholder
281 256 %for status,lbl in c.commit_statuses:
282 257 <option value="${status}" data-status="${status}">${lbl}</option>
283 258 %if is_pull_request and change_status and status in ('approved', 'rejected'):
284 259 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
285 260 %endif
286 261 %endfor
287 262 </select>
288 263 </div>
289 264 %endif
290 265 <div class="action-buttons">
291 266 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
292 267 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
293 268 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
294 269 </div>
295 270 </div>
296 271 ${h.end_form()}
297 272 </div>
298 273 %endif
299 274 </div>
300 275 <script>
301 276 // init active elements of commentForm
302 277 var commitId = templateContext.commit_data.commit_id;
303 278 var pullRequestId = templateContext.pull_request_data.pull_request_id;
304 279 var lineNo;
305 280
306 281 var mainCommentForm = new CommentForm(
307 282 "#${form_id}", commitId, pullRequestId, lineNo, true);
308 283
309 284 mainCommentForm.initStatusChangeSelector();
310 285 bindToggleButtons();
311 286 </script>
312 287 </%def>
@@ -1,71 +1,71 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Commits') % c.repo_name} -
6 6 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
7 7 ...
8 8 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 9 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 ${_('Commits')} -
17 17 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
18 18 ...
19 19 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
20 20 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='changelog')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32 <div class="summary-header">
33 33 <div class="title">
34 34 <div class="title-content">
35 35 ${self.repo_page_title(c.rhodecode_db_repo)}
36 36 </div>
37 37 </div>
38 38 <div class="header-buttons">
39 39 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}"
40 40 class="btn btn-default">
41 41 ${_('Show combined compare')}
42 42 </a>
43 43 </div>
44 44 </div>
45 45
46 46 <div class="summary-detail">
47 47 <div class="title">
48 48 <h2>
49 49 ${self.breadcrumbs_links()}
50 50 </h2>
51 51 </div>
52 52 </div>
53 53 <div id="changeset_compare_view_content">
54 54 ##CS
55 55 <%include file="../compare/compare_commits.html"/>
56 56 <div class="cs_files">
57 ${cbdiffs.render_diffset_menu()}
58 57 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
59 58 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
60 59 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
60 ${cbdiffs.render_diffset_menu()}
61 61 %for commit in c.commit_ranges:
62 ${cbdifss.render_diffset(
62 ${cbdiffs.render_diffset(
63 63 diffset=c.changes[commit.raw_id],
64 64 collapse_when_files_over=5,
65 65 commit=commit,
66 66 )}
67 67 %endfor
68 68 </table>
69 69 </div>
70 70 </div>
71 71 </%def>
@@ -1,420 +1,542 b''
1 1 <%def name="diff_line_anchor(filename, line, type)"><%
2 2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 3 %></%def>
4 4
5 5 <%def name="action_class(action)"><%
6 6 return {
7 7 '-': 'cb-deletion',
8 8 '+': 'cb-addition',
9 9 ' ': 'cb-context',
10 10 }.get(action, 'cb-empty')
11 11 %></%def>
12 12
13 13 <%def name="op_class(op_id)"><%
14 14 return {
15 15 DEL_FILENODE: 'deletion', # file deleted
16 16 BIN_FILENODE: 'warning' # binary diff hidden
17 17 }.get(op_id, 'addition')
18 18 %></%def>
19 19
20 20 <%def name="link_for(**kw)"><%
21 21 new_args = request.GET.mixed()
22 22 new_args.update(kw)
23 23 return h.url('', **new_args)
24 24 %></%def>
25 25
26 26 <%def name="render_diffset(diffset, commit=None,
27 27
28 28 # collapse all file diff entries when there are more than this amount of files in the diff
29 29 collapse_when_files_over=20,
30 30
31 31 # collapse lines in the diff when more than this amount of lines changed in the file diff
32 32 lines_changed_limit=500,
33 33
34 34 # add a ruler at to the output
35 35 ruler_at_chars=0,
36 36
37 # turn on inline comments
38 use_comments=False,
39
37 40 )">
41
42 %if use_comments:
43 <div id="cb-comments-inline-container-template" class="js-template">
44 ${inline_comments_container([])}
45 </div>
46 <div class="js-template" id="cb-comment-inline-form-template">
47 <div class="comment-inline-form ac">
48 %if c.rhodecode_user.username != h.DEFAULT_USER:
49 ${h.form('#', method='get')}
50 <div id="edit-container_{1}" class="clearfix">
51 <div class="comment-title pull-left">
52 ${_('Create a comment on line {1}.')}
53 </div>
54 <div class="comment-help pull-right">
55 ${(_('Comments parsed using %s syntax with %s support.') % (
56 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
57 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
58 )
59 )|n
60 }
61 </div>
62 <div style="clear: both"></div>
63 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
64 </div>
65 <div id="preview-container_{1}" class="clearfix" style="display: none;">
66 <div class="comment-help">
67 ${_('Comment preview')}
68 </div>
69 <div id="preview-box_{1}" class="preview-box"></div>
70 </div>
71 <div class="comment-footer">
72 <div class="action-buttons">
73 <input type="hidden" name="f_path" value="{0}">
74 <input type="hidden" name="line" value="{1}">
75 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
76 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
77 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
78 </div>
79 <div class="comment-button">
80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 ${_('Cancel')}
82 </button>
83 </div>
84 ${h.end_form()}
85 </div>
86 %else:
87 ${h.form('', class_='inline-form comment-form-login', method='get')}
88 <div class="pull-left">
89 <div class="comment-help pull-right">
90 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
91 </div>
92 </div>
93 <div class="comment-button pull-right">
94 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
95 ${_('Cancel')}
96 </button>
97 </div>
98 <div class="clearfix"></div>
99 ${h.end_form()}
100 %endif
101 </div>
102 </div>
103
104 %endif
38 105 <%
39 106 collapse_all = len(diffset.files) > collapse_when_files_over
40 107 %>
41 108
42 109 %if c.diffmode == 'sideside':
43 110 <style>
44 111 .wrapper {
45 112 max-width: 1600px !important;
46 113 }
47 114 </style>
48 115 %endif
49 116 %if ruler_at_chars:
50 117 <style>
51 118 .diff table.cb .cb-content:after {
52 119 content: "";
53 120 border-left: 1px solid blue;
54 121 position: absolute;
55 122 top: 0;
56 123 height: 18px;
57 124 opacity: .2;
58 125 z-index: 10;
59 126 ## +5 to account for diff action (+/-)
60 127 left: ${ruler_at_chars + 5}ch;
61 128 </style>
62 129 %endif
63 130
64 131 <div class="diffset">
65 132 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
66 133 %if commit:
67 134 <div class="pull-right">
68 135 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
69 136 ${_('Browse Files')}
70 137 </a>
71 138 </div>
72 139 %endif
73 140 <h2 class="clearinner">
74 141 %if commit:
75 142 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
76 143 ${h.age_component(commit.date)} -
77 144 %endif
78 145 %if diffset.limited_diff:
79 146 ${_('The requested commit is too big and content was truncated.')}
80 147
81 148 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
82 149 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
83 150 %else:
84 151 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
85 152 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
86 153 %endif
87 154 </h2>
88 155 </div>
89 156
90 157 %if not diffset.files:
91 158 <p class="empty_data">${_('No files')}</p>
92 159 %endif
93 160
94 161 <div class="filediffs">
95 162 %for i, filediff in enumerate(diffset.files):
96 163 <%
97 164 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
98 165 over_lines_changed_limit = lines_changed > lines_changed_limit
99 166 %>
100 167 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
101 168 <div
102 169 class="filediff"
103 170 data-f-path="${filediff['patch']['filename']}"
104 id="a_${h.FID(commit and commit.raw_id or '', filediff['patch']['filename'])}">
171 id="a_${h.FID('', filediff['patch']['filename'])}">
105 172 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
106 173 <div class="filediff-collapse-indicator"></div>
107 174 ${diff_ops(filediff)}
108 175 </label>
109 ${diff_menu(filediff)}
176 ${diff_menu(filediff, use_comments=use_comments)}
110 177 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
111 178 %if not filediff.hunks:
112 179 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
113 180 <tr>
114 181 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
115 182 %if op_id == DEL_FILENODE:
116 183 ${_('File was deleted')}
117 184 %elif op_id == BIN_FILENODE:
118 185 ${_('Binary file hidden')}
119 186 %else:
120 187 ${op_text}
121 188 %endif
122 189 </td>
123 190 </tr>
124 191 %endfor
125 192 %endif
126 193 %if over_lines_changed_limit:
127 194 <tr class="cb-warning cb-collapser">
128 195 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
129 196 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
130 197 <a href="#" class="cb-expand"
131 198 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
132 199 </a>
133 200 <a href="#" class="cb-collapse"
134 201 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
135 202 </a>
136 203 </td>
137 204 </tr>
138 205 %endif
139 206 %if filediff.patch['is_limited_diff']:
140 207 <tr class="cb-warning cb-collapser">
141 208 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
142 209 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
143 210 </td>
144 211 </tr>
145 212 %endif
146 213 %for hunk in filediff.hunks:
147 214 <tr class="cb-hunk">
148 215 <td ${c.diffmode == 'unified' and 'colspan=2' or ''}>
149 216 ## TODO: dan: add ajax loading of more context here
150 217 ## <a href="#">
151 218 <i class="icon-more"></i>
152 219 ## </a>
153 220 </td>
154 221 <td ${c.diffmode == 'sideside' and 'colspan=3' or ''}>
155 222 @@
156 223 -${hunk.source_start},${hunk.source_length}
157 224 +${hunk.target_start},${hunk.target_length}
158 225 ${hunk.section_header}
159 226 </td>
160 227 </tr>
161 228 %if c.diffmode == 'unified':
162 ${render_hunk_lines_unified(hunk)}
229 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
163 230 %elif c.diffmode == 'sideside':
164 ${render_hunk_lines_sideside(hunk)}
231 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
165 232 %else:
166 233 <tr class="cb-line">
167 234 <td>unknown diff mode</td>
168 235 </tr>
169 236 %endif
170 237 %endfor
171 238 </table>
172 239 </div>
173 240 %endfor
174 241 </div>
175 242 </div>
176 243 </%def>
177 244
178 245 <%def name="diff_ops(filediff)">
179 246 <%
180 247 stats = filediff['patch']['stats']
181 248 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
182 249 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
183 250 %>
184 251 <span class="pill">
185 252 %if filediff.source_file_path and filediff.target_file_path:
186 253 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
187 254 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
188 255 %else:
189 256 ## file was modified
190 257 <strong>${filediff.source_file_path}</strong>
191 258 %endif
192 259 %else:
193 260 %if filediff.source_file_path:
194 261 ## file was deleted
195 262 <strong>${filediff.source_file_path}</strong>
196 263 %else:
197 264 ## file was added
198 265 <strong>${filediff.target_file_path}</strong>
199 266 %endif
200 267 %endif
201 268 </span>
202 269 <span class="pill-group" style="float: left">
203 270 %if filediff.patch['is_limited_diff']:
204 271 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
205 272 %endif
206 273 %if RENAMED_FILENODE in stats['ops']:
207 274 <span class="pill" op="renamed">renamed</span>
208 275 %endif
209 276
210 277 %if NEW_FILENODE in stats['ops']:
211 278 <span class="pill" op="created">created</span>
212 279 %if filediff['target_mode'].startswith('120'):
213 280 <span class="pill" op="symlink">symlink</span>
214 281 %else:
215 282 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
216 283 %endif
217 284 %endif
218 285
219 286 %if DEL_FILENODE in stats['ops']:
220 287 <span class="pill" op="removed">removed</span>
221 288 %endif
222 289
223 290 %if CHMOD_FILENODE in stats['ops']:
224 291 <span class="pill" op="mode">
225 292 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
226 293 </span>
227 294 %endif
228 295 </span>
229 296
230 <a class="pill filediff-anchor" href="#a_${h.FID(commit and commit.raw_id or '', filediff.patch['filename'])}">ΒΆ</a>
297 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
231 298
232 299 <span class="pill-group" style="float: right">
233 300 %if BIN_FILENODE in stats['ops']:
234 301 <span class="pill" op="binary">binary</span>
235 302 %if MOD_FILENODE in stats['ops']:
236 303 <span class="pill" op="modified">modified</span>
237 304 %endif
238 305 %endif
239 306 %if stats['added']:
240 307 <span class="pill" op="added">+${stats['added']}</span>
241 308 %endif
242 309 %if stats['deleted']:
243 310 <span class="pill" op="deleted">-${stats['deleted']}</span>
244 311 %endif
245 312 </span>
246 313
247 314 </%def>
248 315
249 316 <%def name="nice_mode(filemode)">
250 317 ${filemode.startswith('100') and filemode[3:] or filemode}
251 318 </%def>
252 319
253 <%def name="diff_menu(filediff)">
320 <%def name="diff_menu(filediff, use_comments=False)">
254 321 <div class="filediff-menu">
255 322 %if filediff.diffset.source_ref:
256 323 %if filediff.patch['operation'] in ['D', 'M']:
257 324 <a
258 325 class="tooltip"
259 326 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
260 327 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
261 328 >
262 329 ${_('Show file before')}
263 330 </a>
264 331 %else:
265 332 <span
266 333 class="tooltip"
267 334 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
268 335 >
269 336 ${_('Show file before')}
270 337 </span>
271 338 %endif
272 339 %if filediff.patch['operation'] in ['A', 'M']:
273 340 <a
274 341 class="tooltip"
275 342 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
276 343 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
277 344 >
278 345 ${_('Show file after')}
279 346 </a>
280 347 %else:
281 348 <span
282 349 class="tooltip"
283 350 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
284 351 >
285 352 ${_('Show file after')}
286 353 </span>
287 354 %endif
288 355 <a
289 356 class="tooltip"
290 357 title="${h.tooltip(_('Raw diff'))}"
291 358 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
292 359 >
293 360 ${_('Raw diff')}
294 361 </a>
295 362 <a
296 363 class="tooltip"
297 364 title="${h.tooltip(_('Download diff'))}"
298 365 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
299 366 >
300 367 ${_('Download diff')}
301 368 </a>
369
370 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
371 %if hasattr(c, 'ignorews_url'):
372 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
373 %endif
374 %if hasattr(c, 'context_url'):
375 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
376 %endif
377
378
379 %if use_comments:
380 <a href="#" onclick="$(this).closest('.filediff').toggleClass('hide-comments'); return false;">
381 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
382 </a>
383 %endif
302 384 %endif
303 385 </div>
304 386 </%def>
305 387
306 388
307 <%def name="render_hunk_lines_sideside(hunk)">
389 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
390 <%def name="inline_comments_container(comments)">
391 <div class="inline-comments">
392 %for comment in comments:
393 ${commentblock.comment_block(comment, inline=True)}
394 %endfor
395 <span onclick="return Rhodecode.comments.createComment(this)"
396 class="btn btn-secondary cb-comment-add-button">
397 ${_('Add another comment')}
398 </span>
399 </div>
400 </%def>
401
402
403 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
308 404 %for i, line in enumerate(hunk.sideside):
309 405 <%
310 406 old_line_anchor, new_line_anchor = None, None
311 407 if line.original.lineno:
312 408 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
313 409 if line.modified.lineno:
314 410 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
315 411 %>
316 412 <tr class="cb-line">
317 413 <td class="cb-lineno ${action_class(line.original.action)}"
318 414 data-line-number="${line.original.lineno}"
319 415 %if old_line_anchor:
320 416 id="${old_line_anchor}"
321 417 %endif
322 418 >
323 419 %if line.original.lineno:
324 420 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
325 421 %endif
326 422 </td>
327 423 <td class="cb-content ${action_class(line.original.action)}"
328 424 data-line-number="o${line.original.lineno}"
329 ><span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
425 >
426 %if use_comments and line.original.lineno:
427 ${render_add_comment_button()}
428 %endif
429 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
430 %if use_comments and line.original.lineno and line.original.comments:
431 ${inline_comments_container(line.original.comments)}
432 %endif
330 433 </td>
331 434 <td class="cb-lineno ${action_class(line.modified.action)}"
332 435 data-line-number="${line.modified.lineno}"
333 436 %if new_line_anchor:
334 437 id="${new_line_anchor}"
335 438 %endif
336 439 >
337 440 %if line.modified.lineno:
338 441 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
339 442 %endif
340 443 </td>
341 444 <td class="cb-content ${action_class(line.modified.action)}"
342 445 data-line-number="n${line.modified.lineno}"
343 446 >
447 %if use_comments and line.modified.lineno:
448 ${render_add_comment_button()}
449 %endif
344 450 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
451 %if use_comments and line.modified.lineno and line.modified.comments:
452 ${inline_comments_container(line.modified.comments)}
453 %endif
345 454 </td>
346 455 </tr>
347 456 %endfor
348 457 </%def>
349 458
350 459
351 <%def name="render_hunk_lines_unified(hunk)">
352 %for old_line_no, new_line_no, action, content in hunk.unified:
460 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
461 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
353 462 <%
354 463 old_line_anchor, new_line_anchor = None, None
355 464 if old_line_no:
356 465 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
357 466 if new_line_no:
358 467 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
359 468 %>
360 469 <tr class="cb-line">
361 470 <td class="cb-lineno ${action_class(action)}"
362 471 data-line-number="${old_line_no}"
363 472 %if old_line_anchor:
364 473 id="${old_line_anchor}"
365 474 %endif
366 475 >
367 476 %if old_line_anchor:
368 477 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
369 478 %endif
370 479 </td>
371 480 <td class="cb-lineno ${action_class(action)}"
372 481 data-line-number="${new_line_no}"
373 482 %if new_line_anchor:
374 483 id="${new_line_anchor}"
375 484 %endif
376 485 >
377 486 %if new_line_anchor:
378 487 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
379 488 %endif
380 489 </td>
381 490 <td class="cb-content ${action_class(action)}"
382 491 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
383 ><span class="cb-code">${action} ${content or '' | n}</span>
384 </td>
492 >
493 %if use_comments:
494 ${render_add_comment_button()}
495 %endif
496 <span class="cb-code">${action} ${content or '' | n}</span>
497 %if use_comments and comments:
498 ${inline_comments_container(comments)}
499 %endif
500 </td>
385 501 </tr>
386 502 %endfor
387 503 </%def>
388 504
505 <%def name="render_add_comment_button()">
506 <button
507 class="btn btn-small btn-primary cb-comment-box-opener"
508 onclick="return Rhodecode.comments.createComment(this)"
509 >+</button>
510 </%def>
389 511
390 512 <%def name="render_diffset_menu()">
391 513 <div class="diffset-menu clearinner">
392 514 <div class="pull-right">
393 515 <div class="btn-group">
394 516 <a
395 517 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
396 518 title="${_('View side by side')}"
397 519 href="${h.url_replace(diffmode='sideside')}">
398 520 <span>${_('Side by Side')}</span>
399 521 </a>
400 522 <a
401 523 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
402 524 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
403 525 <span>${_('Unified')}</span>
404 526 </a>
405 527 </div>
406 528 </div>
407 529 <div class="pull-left">
408 530 <div class="btn-group">
409 531 <a
410 532 class="btn"
411 533 href="#"
412 534 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
413 535 <a
414 536 class="btn"
415 537 href="#"
416 538 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
417 539 </div>
418 540 </div>
419 541 </div>
420 542 </%def>
@@ -1,299 +1,302 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.helpers import _shorten_commit_id
24 24 from rhodecode.tests import url
25 25
26 26
27 27 @pytest.mark.usefixtures("app")
28 28 class TestChangesetController(object):
29 29
30 30 def test_index(self, backend):
31 31 commit_id = self.commit_id[backend.alias]
32 32 response = self.app.get(url(
33 33 controller='changeset', action='index',
34 34 repo_name=backend.repo_name, revision=commit_id))
35 35 response.mustcontain('Added a symlink')
36 36 response.mustcontain(commit_id)
37 37 response.mustcontain('No newline at end of file')
38 38
39 39 def test_index_raw(self, backend):
40 40 commit_id = self.commit_id[backend.alias]
41 41 response = self.app.get(url(
42 42 controller='changeset', action='changeset_raw',
43 43 repo_name=backend.repo_name, revision=commit_id))
44 44 assert response.body == self.diffs[backend.alias]
45 45
46 46 def test_index_raw_patch(self, backend):
47 47 response = self.app.get(url(
48 48 controller='changeset', action='changeset_patch',
49 49 repo_name=backend.repo_name,
50 50 revision=self.commit_id[backend.alias]))
51 51 assert response.body == self.patches[backend.alias]
52 52
53 53 def test_index_changeset_download(self, backend):
54 54 response = self.app.get(url(
55 55 controller='changeset', action='changeset_download',
56 56 repo_name=backend.repo_name,
57 57 revision=self.commit_id[backend.alias]))
58 58 assert response.body == self.diffs[backend.alias]
59 59
60 60 def test_single_commit_page_different_ops(self, backend):
61 61 commit_id = {
62 62 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
63 63 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
64 64 'svn': '337',
65 65 }
66 66 commit_id = commit_id[backend.alias]
67 67 response = self.app.get(url(
68 68 controller='changeset', action='index',
69 69 repo_name=backend.repo_name, revision=commit_id))
70 70
71 71 response.mustcontain(_shorten_commit_id(commit_id))
72 72 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
73 73
74 74 # files op files
75 75 response.mustcontain('File no longer present at commit: %s' %
76 76 _shorten_commit_id(commit_id))
77 77
78 78 # svn uses a different filename
79 79 if backend.alias == 'svn':
80 80 response.mustcontain('new file 10644')
81 81 else:
82 82 response.mustcontain('new file 100644')
83 83 response.mustcontain('Changed theme to ADC theme') # commit msg
84 84
85 self._check_diff_menus(response, right_menu=True)
85 self._check_new_diff_menus(response, right_menu=True)
86 86
87 87 def test_commit_range_page_different_ops(self, backend):
88 88 commit_id_range = {
89 89 'hg': (
90 90 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
91 91 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
92 92 'git': (
93 93 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
94 94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
95 95 'svn': (
96 96 '335',
97 97 '337'),
98 98 }
99 99 commit_ids = commit_id_range[backend.alias]
100 100 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
101 101 response = self.app.get(url(
102 102 controller='changeset', action='index',
103 103 repo_name=backend.repo_name, revision=commit_id))
104 104
105 105 response.mustcontain(_shorten_commit_id(commit_ids[0]))
106 106 response.mustcontain(_shorten_commit_id(commit_ids[1]))
107 107
108 108 # svn is special
109 109 if backend.alias == 'svn':
110 110 response.mustcontain('new file 10644')
111 response.mustcontain('34 files changed: 1184 inserted, 311 deleted')
111 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
112 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
113 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
112 114 else:
113 115 response.mustcontain('new file 100644')
114 response.mustcontain('33 files changed: 1165 inserted, 308 deleted')
116 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
117 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
115 118
116 119 # files op files
117 120 response.mustcontain('File no longer present at commit: %s' %
118 121 _shorten_commit_id(commit_ids[1]))
119 122 response.mustcontain('Added docstrings to vcs.cli') # commit msg
120 123 response.mustcontain('Changed theme to ADC theme') # commit msg
121 124
122 self._check_diff_menus(response)
125 self._check_new_diff_menus(response)
123 126
124 127 def test_combined_compare_commit_page_different_ops(self, backend):
125 128 commit_id_range = {
126 129 'hg': (
127 130 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
128 131 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
129 132 'git': (
130 133 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
131 134 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
132 135 'svn': (
133 136 '335',
134 137 '337'),
135 138 }
136 139 commit_ids = commit_id_range[backend.alias]
137 140 response = self.app.get(url(
138 141 controller='compare', action='compare',
139 142 repo_name=backend.repo_name,
140 143 source_ref_type='rev', source_ref=commit_ids[0],
141 144 target_ref_type='rev', target_ref=commit_ids[1], ))
142 145
143 146 response.mustcontain(_shorten_commit_id(commit_ids[0]))
144 147 response.mustcontain(_shorten_commit_id(commit_ids[1]))
145 148
146 149 # files op files
147 150 response.mustcontain('File no longer present at commit: %s' %
148 151 _shorten_commit_id(commit_ids[1]))
149 152
150 153 # svn is special
151 154 if backend.alias == 'svn':
152 155 response.mustcontain('new file 10644')
153 156 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
154 157 else:
155 158 response.mustcontain('new file 100644')
156 159 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
157 160
158 161 response.mustcontain('Added docstrings to vcs.cli') # commit msg
159 162 response.mustcontain('Changed theme to ADC theme') # commit msg
160 163
161 164 self._check_new_diff_menus(response)
162 165
163 166 def test_changeset_range(self, backend):
164 167 self._check_changeset_range(
165 168 backend, self.commit_id_range, self.commit_id_range_result)
166 169
167 170 def test_changeset_range_with_initial_commit(self, backend):
168 171 commit_id_range = {
169 172 'hg': (
170 173 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
171 174 '...6cba7170863a2411822803fa77a0a264f1310b35'),
172 175 'git': (
173 176 'c1214f7e79e02fc37156ff215cd71275450cffc3'
174 177 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
175 178 'svn': '1...3',
176 179 }
177 180 commit_id_range_result = {
178 181 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
179 182 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
180 183 'svn': ['1', '2', '3'],
181 184 }
182 185 self._check_changeset_range(
183 186 backend, commit_id_range, commit_id_range_result)
184 187
185 188 def _check_changeset_range(
186 189 self, backend, commit_id_ranges, commit_id_range_result):
187 190 response = self.app.get(
188 191 url(controller='changeset', action='index',
189 192 repo_name=backend.repo_name,
190 193 revision=commit_id_ranges[backend.alias]))
191 194 expected_result = commit_id_range_result[backend.alias]
192 195 response.mustcontain('{} commits'.format(len(expected_result)))
193 196 for commit_id in expected_result:
194 197 response.mustcontain(commit_id)
195 198
196 199 commit_id = {
197 200 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
198 201 'svn': '393',
199 202 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
200 203 }
201 204
202 205 commit_id_range = {
203 206 'hg': (
204 207 'a53d9201d4bc278910d416d94941b7ea007ecd52'
205 208 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
206 209 'git': (
207 210 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
208 211 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
209 212 'svn': '391...393',
210 213 }
211 214
212 215 commit_id_range_result = {
213 216 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
214 217 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
215 218 'svn': ['391', '392', '393'],
216 219 }
217 220
218 221 diffs = {
219 222 'hg': r"""diff --git a/README b/README
220 223 new file mode 120000
221 224 --- /dev/null
222 225 +++ b/README
223 226 @@ -0,0 +1,1 @@
224 227 +README.rst
225 228 \ No newline at end of file
226 229 """,
227 230 'git': r"""diff --git a/README b/README
228 231 new file mode 120000
229 232 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
230 233 --- /dev/null
231 234 +++ b/README
232 235 @@ -0,0 +1 @@
233 236 +README.rst
234 237 \ No newline at end of file
235 238 """,
236 239 'svn': """Index: README
237 240 ===================================================================
238 241 diff --git a/README b/README
239 242 new file mode 10644
240 243 --- /dev/null\t(revision 0)
241 244 +++ b/README\t(revision 393)
242 245 @@ -0,0 +1 @@
243 246 +link README.rst
244 247 \\ No newline at end of file
245 248 """,
246 249 }
247 250
248 251 patches = {
249 252 'hg': r"""# HG changeset patch
250 253 # User Marcin Kuzminski <marcin@python-works.com>
251 254 # Date 2014-01-07 12:21:40
252 255 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
253 256 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
254 257
255 258 Added a symlink
256 259
257 260 """ + diffs['hg'],
258 261 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
259 262 From: Marcin Kuzminski <marcin@python-works.com>
260 263 Date: 2014-01-07 12:22:20
261 264 Subject: [PATCH] Added a symlink
262 265
263 266 ---
264 267
265 268 """ + diffs['git'],
266 269 'svn': r"""# SVN changeset patch
267 270 # User marcin
268 271 # Date 2014-09-02 12:25:22.071142
269 272 # Revision 393
270 273
271 274 Added a symlink
272 275
273 276 """ + diffs['svn'],
274 277 }
275 278
276 279 def _check_diff_menus(self, response, right_menu=False,):
277 280 # diff menus
278 281 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
279 282 'Raw Diff', 'Download Diff']:
280 283 response.mustcontain(elem)
281 284
282 285 # right pane diff menus
283 286 if right_menu:
284 287 for elem in ['Ignore whitespace', 'Increase context',
285 288 'Hide comments']:
286 289 response.mustcontain(elem)
287 290
288 291
289 292 def _check_new_diff_menus(self, response, right_menu=False,):
290 293 # diff menus
291 294 for elem in ['Show file before', 'Show file after',
292 295 'Raw diff', 'Download diff']:
293 296 response.mustcontain(elem)
294 297
295 298 # right pane diff menus
296 299 if right_menu:
297 300 for elem in ['Ignore whitespace', 'Increase context',
298 301 'Hide comments']:
299 302 response.mustcontain(elem)
@@ -1,277 +1,282 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from pylons.i18n import ungettext
22 22 import pytest
23 23
24 24 from rhodecode.tests import *
25 25 from rhodecode.model.db import (
26 26 ChangesetComment, Notification, UserNotification)
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.lib import helpers as h
29 29
30 30
31 31 @pytest.mark.backends("git", "hg", "svn")
32 32 class TestCommitCommentsController(TestController):
33 33
34 34 @pytest.fixture(autouse=True)
35 35 def prepare(self, request, pylonsapp):
36 36 for x in ChangesetComment.query().all():
37 37 Session().delete(x)
38 38 Session().commit()
39 39
40 40 for x in Notification.query().all():
41 41 Session().delete(x)
42 42 Session().commit()
43 43
44 44 request.addfinalizer(self.cleanup)
45 45
46 46 def cleanup(self):
47 47 for x in ChangesetComment.query().all():
48 48 Session().delete(x)
49 49 Session().commit()
50 50
51 51 for x in Notification.query().all():
52 52 Session().delete(x)
53 53 Session().commit()
54 54
55 55 def test_create(self, backend):
56 56 self.log_user()
57 57 commit = backend.repo.get_commit('300')
58 58 commit_id = commit.raw_id
59 59 text = u'CommentOnCommit'
60 60
61 61 params = {'text': text, 'csrf_token': self.csrf_token}
62 62 self.app.post(
63 63 url(controller='changeset', action='comment',
64 64 repo_name=backend.repo_name, revision=commit_id), params=params)
65 65
66 66 response = self.app.get(
67 67 url(controller='changeset', action='index',
68 68 repo_name=backend.repo_name, revision=commit_id))
69 69
70 70 # test DB
71 71 assert ChangesetComment.query().count() == 1
72 72 assert_comment_links(response, ChangesetComment.query().count(), 0)
73 73
74 74 assert Notification.query().count() == 1
75 75 assert ChangesetComment.query().count() == 1
76 76
77 77 notification = Notification.query().all()[0]
78 78
79 79 comment_id = ChangesetComment.query().first().comment_id
80 80 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
81 81
82 82 sbj = 'commented on commit `{0}` in the {1} repository'.format(
83 83 h.show_id(commit), backend.repo_name)
84 84 assert sbj in notification.subject
85 85
86 86 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
87 87 backend.repo_name, commit_id, comment_id))
88 88 assert lnk in notification.body
89 89
90 90 def test_create_inline(self, backend):
91 91 self.log_user()
92 92 commit = backend.repo.get_commit('300')
93 93 commit_id = commit.raw_id
94 94 text = u'CommentOnCommit'
95 95 f_path = 'vcs/web/simplevcs/views/repository.py'
96 96 line = 'n1'
97 97
98 98 params = {'text': text, 'f_path': f_path, 'line': line,
99 99 'csrf_token': self.csrf_token}
100 100
101 101 self.app.post(
102 102 url(controller='changeset', action='comment',
103 103 repo_name=backend.repo_name, revision=commit_id), params=params)
104 104
105 105 response = self.app.get(
106 106 url(controller='changeset', action='index',
107 107 repo_name=backend.repo_name, revision=commit_id))
108 108
109 109 # test DB
110 110 assert ChangesetComment.query().count() == 1
111 111 assert_comment_links(response, 0, ChangesetComment.query().count())
112 response.mustcontain(
113 '''class="inline-comment-placeholder" '''
114 '''path="vcs/web/simplevcs/views/repository.py" '''
115 '''target_id="vcswebsimplevcsviewsrepositorypy"'''
116 )
112
113 if backend.alias == 'svn':
114 response.mustcontain(
115 '''data-f-path="vcs/commands/summary.py" '''
116 '''id="a_c--ad05457a43f8"'''
117 )
118 else:
119 response.mustcontain(
120 '''data-f-path="vcs/backends/hg.py" '''
121 '''id="a_c--9c390eb52cd6"'''
122 )
117 123
118 124 assert Notification.query().count() == 1
119 125 assert ChangesetComment.query().count() == 1
120 126
121 127 notification = Notification.query().all()[0]
122 128 comment = ChangesetComment.query().first()
123 129 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
124 130
125 131 assert comment.revision == commit_id
126 132 sbj = 'commented on commit `{commit}` ' \
127 133 '(file: `{f_path}`) in the {repo} repository'.format(
128 134 commit=h.show_id(commit),
129 135 f_path=f_path, line=line, repo=backend.repo_name)
130 136 assert sbj in notification.subject
131 137
132 138 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
133 139 backend.repo_name, commit_id, comment.comment_id))
134 140 assert lnk in notification.body
135 141 assert 'on line n1' in notification.body
136 142
137 143 def test_create_with_mention(self, backend):
138 144 self.log_user()
139 145
140 146 commit_id = backend.repo.get_commit('300').raw_id
141 147 text = u'@test_regular check CommentOnCommit'
142 148
143 149 params = {'text': text, 'csrf_token': self.csrf_token}
144 150 self.app.post(
145 151 url(controller='changeset', action='comment',
146 152 repo_name=backend.repo_name, revision=commit_id), params=params)
147 153
148 154 response = self.app.get(
149 155 url(controller='changeset', action='index',
150 156 repo_name=backend.repo_name, revision=commit_id))
151 157 # test DB
152 158 assert ChangesetComment.query().count() == 1
153 159 assert_comment_links(response, ChangesetComment.query().count(), 0)
154 160
155 161 notification = Notification.query().one()
156 162
157 163 assert len(notification.recipients) == 2
158 164 users = [x.username for x in notification.recipients]
159 165
160 166 # test_regular gets notification by @mention
161 167 assert sorted(users) == [u'test_admin', u'test_regular']
162 168
163 169 def test_create_with_status_change(self, backend):
164 170 self.log_user()
165 171 commit = backend.repo.get_commit('300')
166 172 commit_id = commit.raw_id
167 173 text = u'CommentOnCommit'
168 174 f_path = 'vcs/web/simplevcs/views/repository.py'
169 175 line = 'n1'
170 176
171 177 params = {'text': text, 'changeset_status': 'approved',
172 178 'csrf_token': self.csrf_token}
173 179
174 180 self.app.post(
175 181 url(controller='changeset', action='comment',
176 182 repo_name=backend.repo_name, revision=commit_id), params=params)
177 183
178 184 response = self.app.get(
179 185 url(controller='changeset', action='index',
180 186 repo_name=backend.repo_name, revision=commit_id))
181 187
182 188 # test DB
183 189 assert ChangesetComment.query().count() == 1
184 190 assert_comment_links(response, ChangesetComment.query().count(), 0)
185 191
186 192 assert Notification.query().count() == 1
187 193 assert ChangesetComment.query().count() == 1
188 194
189 195 notification = Notification.query().all()[0]
190 196
191 197 comment_id = ChangesetComment.query().first().comment_id
192 198 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
193 199
194 200 sbj = 'commented on commit `{0}` (status: Approved) ' \
195 201 'in the {1} repository'.format(
196 202 h.show_id(commit), backend.repo_name)
197 203 assert sbj in notification.subject
198 204
199 205 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
200 206 backend.repo_name, commit_id, comment_id))
201 207 assert lnk in notification.body
202 208
203 209 def test_delete(self, backend):
204 210 self.log_user()
205 211 commit_id = backend.repo.get_commit('300').raw_id
206 212 text = u'CommentOnCommit'
207 213
208 214 params = {'text': text, 'csrf_token': self.csrf_token}
209 215 self.app.post(
210 216 url(
211 217 controller='changeset', action='comment',
212 218 repo_name=backend.repo_name, revision=commit_id),
213 219 params=params)
214 220
215 221 comments = ChangesetComment.query().all()
216 222 assert len(comments) == 1
217 223 comment_id = comments[0].comment_id
218 224
219 225 self.app.post(
220 226 url(controller='changeset', action='delete_comment',
221 227 repo_name=backend.repo_name, comment_id=comment_id),
222 228 params={'_method': 'delete', 'csrf_token': self.csrf_token})
223 229
224 230 comments = ChangesetComment.query().all()
225 231 assert len(comments) == 0
226 232
227 233 response = self.app.get(
228 234 url(controller='changeset', action='index',
229 235 repo_name=backend.repo_name, revision=commit_id))
230 236 assert_comment_links(response, 0, 0)
231 237
232 238 @pytest.mark.parametrize('renderer, input, output', [
233 239 ('rst', 'plain text', '<p>plain text</p>'),
234 240 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
235 241 ('rst', '*italics*', '<em>italics</em>'),
236 242 ('rst', '**bold**', '<strong>bold</strong>'),
237 243 ('markdown', 'plain text', '<p>plain text</p>'),
238 244 ('markdown', '# header', '<h1>header</h1>'),
239 245 ('markdown', '*italics*', '<em>italics</em>'),
240 246 ('markdown', '**bold**', '<strong>bold</strong>'),
241 247 ])
242 248 def test_preview(self, renderer, input, output, backend):
243 249 self.log_user()
244 250 params = {
245 251 'renderer': renderer,
246 252 'text': input,
247 253 'csrf_token': self.csrf_token
248 254 }
249 255 environ = {
250 256 'HTTP_X_PARTIAL_XHR': 'true'
251 257 }
252 258 response = self.app.post(
253 259 url(controller='changeset',
254 260 action='preview_comment',
255 261 repo_name=backend.repo_name),
256 262 params=params,
257 263 extra_environ=environ)
258 264
259 265 response.mustcontain(output)
260 266
261 267
262 268 def assert_comment_links(response, comments, inline_comments):
263 269 comments_text = ungettext("%d Commit comment",
264 270 "%d Commit comments", comments) % comments
265 271 if comments:
266 272 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
267 273 else:
268 274 response.mustcontain(comments_text)
269 275
270 276 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
271 277 inline_comments) % inline_comments
272 278 if inline_comments:
273 279 response.mustcontain(
274 '<a href="#inline-comments" '
275 'id="inline-comments-counter">%s</a>' % inline_comments_text)
280 'id="inline-comments-counter">%s</' % inline_comments_text)
276 281 else:
277 282 response.mustcontain(inline_comments_text)
General Comments 0
You need to be logged in to leave comments. Login now