##// END OF EJS Templates
pull-request: introduced new merge-checks....
marcink -
r1334:68703a99 default
parent child Browse files
Show More

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

@@ -0,0 +1,37 b''
1
2 <div class="pull-request-wrap">
3
4 <ul>
5 % for pr_check_type, pr_check_msg in c.pr_merge_checks:
6 <li>
7 <span class="merge-message ${pr_check_type}" data-role="merge-message">
8 % if pr_check_type in ['success']:
9 <i class="icon-true"></i>
10 % else:
11 <i class="icon-false"></i>
12 % endif
13 ${pr_check_msg}
14 </span>
15 </li>
16 % endfor
17 </ul>
18
19 <div class="pull-request-merge-actions">
20 % if c.allowed_to_merge:
21 <div class="pull-right">
22 ${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')}
23 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
24 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
25 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
26 ${h.end_form()}
27 </div>
28 % elif c.rhodecode_user.username != h.DEFAULT_USER:
29 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
30 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
31 % else:
32 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
33 % endif
34 </div>
35
36 </div>
37
@@ -1,1018 +1,1046 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = CommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636
637 todos = CommentsModel().get_unresolved_todos(pull_request)
638 if todos:
639 log.debug("Cannot merge, unresolved todos left.")
640 if len(todos) == 1:
641 msg = _('Cannot merge, {} todo still not resolved.').format(
642 len(todos))
643 else:
644 msg = _('Cannot merge, {} todos still not resolved.').format(
645 len(todos))
646 h.flash(msg, category='error')
647 return False
636 648 return True
637 649
638 650 def _merge_pull_request(self, pull_request, user, extras):
639 651 merge_resp = PullRequestModel().merge(
640 652 pull_request, user, extras=extras)
641 653
642 654 if merge_resp.executed:
643 655 log.debug("The merge was successful, closing the pull request.")
644 656 PullRequestModel().close_pull_request(
645 657 pull_request.pull_request_id, user)
646 658 Session().commit()
647 659 msg = _('Pull request was successfully merged and closed.')
648 660 h.flash(msg, category='success')
649 661 else:
650 662 log.debug(
651 663 "The merge was not successful. Merge response: %s",
652 664 merge_resp)
653 665 msg = PullRequestModel().merge_status_message(
654 666 merge_resp.failure_reason)
655 667 h.flash(msg, category='error')
656 668
657 669 def _update_reviewers(self, pull_request_id, review_members):
658 670 reviewers = [
659 671 (int(r['user_id']), r['reasons']) for r in review_members]
660 672 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 673 Session().commit()
662 674
663 675 def _reject_close(self, pull_request):
664 676 if pull_request.is_closed():
665 677 raise HTTPForbidden()
666 678
667 679 PullRequestModel().close_pull_request_with_comment(
668 680 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 681 Session().commit()
670 682
671 683 @LoginRequired()
672 684 @NotAnonymous()
673 685 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 686 'repository.admin')
675 687 @auth.CSRFRequired()
676 688 @jsonify
677 689 def delete(self, repo_name, pull_request_id):
678 690 pull_request_id = safe_int(pull_request_id)
679 691 pull_request = PullRequest.get_or_404(pull_request_id)
680 692 # only owner can delete it !
681 693 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 694 PullRequestModel().delete(pull_request)
683 695 Session().commit()
684 696 h.flash(_('Successfully deleted pull request'),
685 697 category='success')
686 698 return redirect(url('my_account_pullrequests'))
687 699 raise HTTPForbidden()
688 700
689 701 def _get_pr_version(self, pull_request_id, version=None):
690 702 pull_request_id = safe_int(pull_request_id)
691 703 at_version = None
692 704
693 705 if version and version == 'latest':
694 706 pull_request_ver = PullRequest.get(pull_request_id)
695 707 pull_request_obj = pull_request_ver
696 708 _org_pull_request_obj = pull_request_obj
697 709 at_version = 'latest'
698 710 elif version:
699 711 pull_request_ver = PullRequestVersion.get_or_404(version)
700 712 pull_request_obj = pull_request_ver
701 713 _org_pull_request_obj = pull_request_ver.pull_request
702 714 at_version = pull_request_ver.pull_request_version_id
703 715 else:
704 716 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 717
706 718 pull_request_display_obj = PullRequest.get_pr_display_object(
707 719 pull_request_obj, _org_pull_request_obj)
708 720 return _org_pull_request_obj, pull_request_obj, \
709 721 pull_request_display_obj, at_version
710 722
711 723 def _get_pr_version_changes(self, version, pull_request_latest):
712 724 """
713 725 Generate changes commits, and diff data based on the current pr version
714 726 """
715 727
716 728 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 729
718 730 # fake the version to add the "initial" state object
719 731 pull_request_initial = PullRequest.get_pr_display_object(
720 732 pull_request_latest, pull_request_latest,
721 733 internal_methods=['get_commit', 'versions'])
722 734 pull_request_initial.revisions = []
723 735 pull_request_initial.source_repo.get_commit = types.MethodType(
724 736 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 737 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 738 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 739
728 740 _changes_versions = [pull_request_latest] + \
729 741 list(reversed(c.versions)) + \
730 742 [pull_request_initial]
731 743
732 744 if version == 'latest':
733 745 index = 0
734 746 else:
735 747 for pos, prver in enumerate(_changes_versions):
736 748 ver = getattr(prver, 'pull_request_version_id', -1)
737 749 if ver == safe_int(version):
738 750 index = pos
739 751 break
740 752 else:
741 753 index = 0
742 754
743 755 cur_obj = _changes_versions[index]
744 756 prev_obj = _changes_versions[index + 1]
745 757
746 758 old_commit_ids = set(prev_obj.revisions)
747 759 new_commit_ids = set(cur_obj.revisions)
748 760
749 761 changes = PullRequestModel()._calculate_commit_id_changes(
750 762 old_commit_ids, new_commit_ids)
751 763
752 764 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 765 cur_obj, prev_obj)
754 766 file_changes = PullRequestModel()._calculate_file_changes(
755 767 old_diff_data, new_diff_data)
756 768 return changes, file_changes
757 769
758 770 @LoginRequired()
759 771 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 772 'repository.admin')
761 773 def show(self, repo_name, pull_request_id):
762 774 pull_request_id = safe_int(pull_request_id)
763 775 version = request.GET.get('version')
776 merge_checks = request.GET.get('merge_checks')
764 777
765 778 (pull_request_latest,
766 779 pull_request_at_ver,
767 780 pull_request_display_obj,
768 781 at_version) = self._get_pr_version(pull_request_id, version=version)
769 782
770 783 c.template_context['pull_request_data']['pull_request_id'] = \
771 784 pull_request_id
772 785
773 786 # pull_requests repo_name we opened it against
774 787 # ie. target_repo must match
775 788 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 789 raise HTTPNotFound
777 790
778 791 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 792 pull_request_at_ver)
780 793
794 c.ancestor = None # TODO: add ancestor here
795 c.pull_request = pull_request_display_obj
796 c.pull_request_latest = pull_request_latest
797
781 798 pr_closed = pull_request_latest.is_closed()
782 799 if at_version and not at_version == 'latest':
783 800 c.allowed_to_change_status = False
784 801 c.allowed_to_update = False
785 802 c.allowed_to_merge = False
786 803 c.allowed_to_delete = False
787 804 c.allowed_to_comment = False
788 805 else:
789 806 c.allowed_to_change_status = PullRequestModel(). \
790 807 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 808 c.allowed_to_update = PullRequestModel().check_user_update(
792 809 pull_request_latest, c.rhodecode_user) and not pr_closed
793 810 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 811 pull_request_latest, c.rhodecode_user) and not pr_closed
795 812 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 813 pull_request_latest, c.rhodecode_user) and not pr_closed
797 814 c.allowed_to_comment = not pr_closed
798 815
799 816 cc_model = CommentsModel()
800 817
801 818 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 819 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
809 820
810 821 c.versions = pull_request_display_obj.versions()
811 822 c.at_version = at_version
812 823 c.at_version_num = at_version if at_version and at_version != 'latest' else None
813 824 c.at_version_pos = ChangesetComment.get_index_from_version(
814 825 c.at_version_num, c.versions)
815 826
816 827 # GENERAL COMMENTS with versions #
817 828 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
818 829 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
819 830
820 831 # pick comments we want to render at current version
821 832 c.comment_versions = cc_model.aggregate_comments(
822 833 general_comments, c.versions, c.at_version_num)
823 834 c.comments = c.comment_versions[c.at_version_num]['until']
824 835
825 836 # INLINE COMMENTS with versions #
826 837 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
827 838 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
828 839 c.inline_versions = cc_model.aggregate_comments(
829 840 inline_comments, c.versions, c.at_version_num, inline=True)
830 841
831 842 # if we use version, then do not show later comments
832 843 # than current version
833 paths = collections.defaultdict(lambda: collections.defaultdict(list))
844 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
834 845 for co in inline_comments:
835 846 if c.at_version_num:
836 847 # pick comments that are at least UPTO given version, so we
837 848 # don't render comments for higher version
838 849 should_render = co.pull_request_version_id and \
839 850 co.pull_request_version_id <= c.at_version_num
840 851 else:
841 852 # showing all, for 'latest'
842 853 should_render = True
843 854
844 855 if should_render:
845 paths[co.f_path][co.line_no].append(co)
846 inline_comments = paths
856 display_inline_comments[co.f_path][co.line_no].append(co)
857
858 c.pr_merge_checks = []
859 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
860 pull_request_at_ver)
861 c.pr_merge_checks.append(['warning' if not c.pr_merge_status else 'success', c.pr_merge_msg])
862
863 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
864 approval_msg = _('Reviewer approval is pending.')
865 c.pr_merge_status = False
866 c.pr_merge_checks.append(['warning', approval_msg])
867
868 todos = cc_model.get_unresolved_todos(pull_request_latest)
869 if todos:
870 c.pr_merge_status = False
871 if len(todos) == 1:
872 msg = _('{} todo still not resolved.').format(len(todos))
873 else:
874 msg = _('{} todos still not resolved.').format(len(todos))
875 c.pr_merge_checks.append(['warning', msg])
876
877 if merge_checks:
878 return render('/pullrequests/pullrequest_merge_checks.mako')
847 879
848 880 # load compare data into template context
849 self._load_compare_data(pull_request_at_ver, inline_comments)
881 self._load_compare_data(pull_request_at_ver, display_inline_comments)
850 882
851 883 # this is a hack to properly display links, when creating PR, the
852 884 # compare view and others uses different notation, and
853 885 # compare_commits.mako renders links based on the target_repo.
854 886 # We need to swap that here to generate it properly on the html side
855 887 c.target_repo = c.source_repo
856 888
857 889 if c.allowed_to_update:
858 890 force_close = ('forced_closed', _('Close Pull Request'))
859 891 statuses = ChangesetStatus.STATUSES + [force_close]
860 892 else:
861 893 statuses = ChangesetStatus.STATUSES
862 894 c.commit_statuses = statuses
863 895
864 c.ancestor = None # TODO: add ancestor here
865 c.pull_request = pull_request_display_obj
866 c.pull_request_latest = pull_request_latest
867
868 896 c.changes = None
869 897 c.file_changes = None
870 898
871 899 c.show_version_changes = 1 # control flag, not used yet
872 900
873 901 if at_version and c.show_version_changes:
874 902 c.changes, c.file_changes = self._get_pr_version_changes(
875 903 version, pull_request_latest)
876 904
877 905 return render('/pullrequests/pullrequest_show.mako')
878 906
879 907 @LoginRequired()
880 908 @NotAnonymous()
881 909 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
882 910 'repository.admin')
883 911 @auth.CSRFRequired()
884 912 @jsonify
885 913 def comment(self, repo_name, pull_request_id):
886 914 pull_request_id = safe_int(pull_request_id)
887 915 pull_request = PullRequest.get_or_404(pull_request_id)
888 916 if pull_request.is_closed():
889 917 raise HTTPForbidden()
890 918
891 919 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
892 920 # as a changeset status, still we want to send it in one value.
893 921 status = request.POST.get('changeset_status', None)
894 922 text = request.POST.get('text')
895 923 comment_type = request.POST.get('comment_type')
896 924 resolves_comment_id = request.POST.get('resolves_comment_id', None)
897 925
898 926 if status and '_closed' in status:
899 927 close_pr = True
900 928 status = status.replace('_closed', '')
901 929 else:
902 930 close_pr = False
903 931
904 932 forced = (status == 'forced')
905 933 if forced:
906 934 status = 'rejected'
907 935
908 936 allowed_to_change_status = PullRequestModel().check_user_change_status(
909 937 pull_request, c.rhodecode_user)
910 938
911 939 if status and allowed_to_change_status:
912 940 message = (_('Status change %(transition_icon)s %(status)s')
913 941 % {'transition_icon': '>',
914 942 'status': ChangesetStatus.get_status_lbl(status)})
915 943 if close_pr:
916 944 message = _('Closing with') + ' ' + message
917 945 text = text or message
918 946 comm = CommentsModel().create(
919 947 text=text,
920 948 repo=c.rhodecode_db_repo.repo_id,
921 949 user=c.rhodecode_user.user_id,
922 950 pull_request=pull_request_id,
923 951 f_path=request.POST.get('f_path'),
924 952 line_no=request.POST.get('line'),
925 953 status_change=(ChangesetStatus.get_status_lbl(status)
926 954 if status and allowed_to_change_status else None),
927 955 status_change_type=(status
928 956 if status and allowed_to_change_status else None),
929 957 closing_pr=close_pr,
930 958 comment_type=comment_type,
931 959 resolves_comment_id=resolves_comment_id
932 960 )
933 961
934 962 if allowed_to_change_status:
935 963 old_calculated_status = pull_request.calculated_review_status()
936 964 # get status if set !
937 965 if status:
938 966 ChangesetStatusModel().set_status(
939 967 c.rhodecode_db_repo.repo_id,
940 968 status,
941 969 c.rhodecode_user.user_id,
942 970 comm,
943 971 pull_request=pull_request_id
944 972 )
945 973
946 974 Session().flush()
947 975 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
948 976 # we now calculate the status of pull request, and based on that
949 977 # calculation we set the commits status
950 978 calculated_status = pull_request.calculated_review_status()
951 979 if old_calculated_status != calculated_status:
952 980 PullRequestModel()._trigger_pull_request_hook(
953 981 pull_request, c.rhodecode_user, 'review_status_change')
954 982
955 983 calculated_status_lbl = ChangesetStatus.get_status_lbl(
956 984 calculated_status)
957 985
958 986 if close_pr:
959 987 status_completed = (
960 988 calculated_status in [ChangesetStatus.STATUS_APPROVED,
961 989 ChangesetStatus.STATUS_REJECTED])
962 990 if forced or status_completed:
963 991 PullRequestModel().close_pull_request(
964 992 pull_request_id, c.rhodecode_user)
965 993 else:
966 994 h.flash(_('Closing pull request on other statuses than '
967 995 'rejected or approved is forbidden. '
968 996 'Calculated status from all reviewers '
969 997 'is currently: %s') % calculated_status_lbl,
970 998 category='warning')
971 999
972 1000 Session().commit()
973 1001
974 1002 if not request.is_xhr:
975 1003 return redirect(h.url('pullrequest_show', repo_name=repo_name,
976 1004 pull_request_id=pull_request_id))
977 1005
978 1006 data = {
979 1007 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
980 1008 }
981 1009 if comm:
982 1010 c.co = comm
983 1011 c.inline_comment = True if comm.line_no else False
984 1012 data.update(comm.get_dict())
985 1013 data.update({'rendered_text':
986 1014 render('changeset/changeset_comment_block.mako')})
987 1015
988 1016 return data
989 1017
990 1018 @LoginRequired()
991 1019 @NotAnonymous()
992 1020 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
993 1021 'repository.admin')
994 1022 @auth.CSRFRequired()
995 1023 @jsonify
996 1024 def delete_comment(self, repo_name, comment_id):
997 1025 return self._delete_comment(comment_id)
998 1026
999 1027 def _delete_comment(self, comment_id):
1000 1028 comment_id = safe_int(comment_id)
1001 1029 co = ChangesetComment.get_or_404(comment_id)
1002 1030 if co.pull_request.is_closed():
1003 1031 # don't allow deleting comments on closed pull request
1004 1032 raise HTTPForbidden()
1005 1033
1006 1034 is_owner = co.author.user_id == c.rhodecode_user.user_id
1007 1035 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1008 1036 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1009 1037 old_calculated_status = co.pull_request.calculated_review_status()
1010 1038 CommentsModel().delete(comment=co)
1011 1039 Session().commit()
1012 1040 calculated_status = co.pull_request.calculated_review_status()
1013 1041 if old_calculated_status != calculated_status:
1014 1042 PullRequestModel()._trigger_pull_request_hook(
1015 1043 co.pull_request, c.rhodecode_user, 'review_status_change')
1016 1044 return True
1017 1045 else:
1018 1046 raise HTTPForbidden()
@@ -1,600 +1,612 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47 from rhodecode.model.validation_schema.schemas import comment_schema
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class CommentsModel(BaseModel):
54 54
55 55 cls = ChangesetComment
56 56
57 57 DIFF_CONTEXT_BEFORE = 3
58 58 DIFF_CONTEXT_AFTER = 3
59 59
60 60 def __get_commit_comment(self, changeset_comment):
61 61 return self._get_instance(ChangesetComment, changeset_comment)
62 62
63 63 def __get_pull_request(self, pull_request):
64 64 return self._get_instance(PullRequest, pull_request)
65 65
66 66 def _extract_mentions(self, s):
67 67 user_objects = []
68 68 for username in extract_mentioned_users(s):
69 69 user_obj = User.get_by_username(username, case_insensitive=True)
70 70 if user_obj:
71 71 user_objects.append(user_obj)
72 72 return user_objects
73 73
74 74 def _get_renderer(self, global_renderer='rst'):
75 75 try:
76 76 # try reading from visual context
77 77 from pylons import tmpl_context
78 78 global_renderer = tmpl_context.visual.default_renderer
79 79 except AttributeError:
80 80 log.debug("Renderer not set, falling back "
81 81 "to default renderer '%s'", global_renderer)
82 82 except Exception:
83 83 log.error(traceback.format_exc())
84 84 return global_renderer
85 85
86 86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 87 # group by versions, and count until, and display objects
88 88
89 89 comment_groups = collections.defaultdict(list)
90 90 [comment_groups[
91 91 _co.pull_request_version_id].append(_co) for _co in comments]
92 92
93 93 def yield_comments(pos):
94 94 for co in comment_groups[pos]:
95 95 yield co
96 96
97 97 comment_versions = collections.defaultdict(
98 98 lambda: collections.defaultdict(list))
99 99 prev_prvid = -1
100 100 # fake last entry with None, to aggregate on "latest" version which
101 101 # doesn't have an pull_request_version_id
102 102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 103 prvid = ver.pull_request_version_id
104 104 if prev_prvid == -1:
105 105 prev_prvid = prvid
106 106
107 107 for co in yield_comments(prvid):
108 108 comment_versions[prvid]['at'].append(co)
109 109
110 110 # save until
111 111 current = comment_versions[prvid]['at']
112 112 prev_until = comment_versions[prev_prvid]['until']
113 113 cur_until = prev_until + current
114 114 comment_versions[prvid]['until'].extend(cur_until)
115 115
116 116 # save outdated
117 117 if inline:
118 118 outdated = [x for x in cur_until
119 119 if x.outdated_at_version(show_version)]
120 120 else:
121 121 outdated = [x for x in cur_until
122 122 if x.older_than_version(show_version)]
123 123 display = [x for x in cur_until if x not in outdated]
124 124
125 125 comment_versions[prvid]['outdated'] = outdated
126 126 comment_versions[prvid]['display'] = display
127 127
128 128 prev_prvid = prvid
129 129
130 130 return comment_versions
131 131
132 def get_unresolved_todos(self, pull_request):
133
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO) \
139 .filter(coalesce(ChangesetComment.display_state, '') !=
140 ChangesetComment.COMMENT_OUTDATED).all()
141
142 return todos
143
132 144 def create(self, text, repo, user, commit_id=None, pull_request=None,
133 145 f_path=None, line_no=None, status_change=None,
134 146 status_change_type=None, comment_type=None,
135 147 resolves_comment_id=None, closing_pr=False, send_email=True,
136 148 renderer=None):
137 149 """
138 150 Creates new comment for commit or pull request.
139 151 IF status_change is not none this comment is associated with a
140 152 status change of commit or commit associated with pull request
141 153
142 154 :param text:
143 155 :param repo:
144 156 :param user:
145 157 :param commit_id:
146 158 :param pull_request:
147 159 :param f_path:
148 160 :param line_no:
149 161 :param status_change: Label for status change
150 162 :param comment_type: Type of comment
151 163 :param status_change_type: type of status change
152 164 :param closing_pr:
153 165 :param send_email:
154 166 :param renderer: pick renderer for this comment
155 167 """
156 168 if not text:
157 169 log.warning('Missing text for comment, skipping...')
158 170 return
159 171
160 172 if not renderer:
161 173 renderer = self._get_renderer()
162 174
163 175 repo = self._get_repo(repo)
164 176 user = self._get_user(user)
165 177
166 178 schema = comment_schema.CommentSchema()
167 179 validated_kwargs = schema.deserialize(dict(
168 180 comment_body=text,
169 181 comment_type=comment_type,
170 182 comment_file=f_path,
171 183 comment_line=line_no,
172 184 renderer_type=renderer,
173 185 status_change=status_change_type,
174 186 resolves_comment_id=resolves_comment_id,
175 187 repo=repo.repo_id,
176 188 user=user.user_id,
177 189 ))
178 190
179 191 comment = ChangesetComment()
180 192 comment.renderer = validated_kwargs['renderer_type']
181 193 comment.text = validated_kwargs['comment_body']
182 194 comment.f_path = validated_kwargs['comment_file']
183 195 comment.line_no = validated_kwargs['comment_line']
184 196 comment.comment_type = validated_kwargs['comment_type']
185 197
186 198 comment.repo = repo
187 199 comment.author = user
188 200 comment.resolved_comment = self.__get_commit_comment(
189 201 validated_kwargs['resolves_comment_id'])
190 202
191 203 pull_request_id = pull_request
192 204
193 205 commit_obj = None
194 206 pull_request_obj = None
195 207
196 208 if commit_id:
197 209 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
198 210 # do a lookup, so we don't pass something bad here
199 211 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
200 212 comment.revision = commit_obj.raw_id
201 213
202 214 elif pull_request_id:
203 215 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
204 216 pull_request_obj = self.__get_pull_request(pull_request_id)
205 217 comment.pull_request = pull_request_obj
206 218 else:
207 219 raise Exception('Please specify commit or pull_request_id')
208 220
209 221 Session().add(comment)
210 222 Session().flush()
211 223 kwargs = {
212 224 'user': user,
213 225 'renderer_type': renderer,
214 226 'repo_name': repo.repo_name,
215 227 'status_change': status_change,
216 228 'status_change_type': status_change_type,
217 229 'comment_body': text,
218 230 'comment_file': f_path,
219 231 'comment_line': line_no,
220 232 }
221 233
222 234 if commit_obj:
223 235 recipients = ChangesetComment.get_users(
224 236 revision=commit_obj.raw_id)
225 237 # add commit author if it's in RhodeCode system
226 238 cs_author = User.get_from_cs_author(commit_obj.author)
227 239 if not cs_author:
228 240 # use repo owner if we cannot extract the author correctly
229 241 cs_author = repo.user
230 242 recipients += [cs_author]
231 243
232 244 commit_comment_url = self.get_url(comment)
233 245
234 246 target_repo_url = h.link_to(
235 247 repo.repo_name,
236 248 h.url('summary_home',
237 249 repo_name=repo.repo_name, qualified=True))
238 250
239 251 # commit specifics
240 252 kwargs.update({
241 253 'commit': commit_obj,
242 254 'commit_message': commit_obj.message,
243 255 'commit_target_repo': target_repo_url,
244 256 'commit_comment_url': commit_comment_url,
245 257 })
246 258
247 259 elif pull_request_obj:
248 260 # get the current participants of this pull request
249 261 recipients = ChangesetComment.get_users(
250 262 pull_request_id=pull_request_obj.pull_request_id)
251 263 # add pull request author
252 264 recipients += [pull_request_obj.author]
253 265
254 266 # add the reviewers to notification
255 267 recipients += [x.user for x in pull_request_obj.reviewers]
256 268
257 269 pr_target_repo = pull_request_obj.target_repo
258 270 pr_source_repo = pull_request_obj.source_repo
259 271
260 272 pr_comment_url = h.url(
261 273 'pullrequest_show',
262 274 repo_name=pr_target_repo.repo_name,
263 275 pull_request_id=pull_request_obj.pull_request_id,
264 276 anchor='comment-%s' % comment.comment_id,
265 277 qualified=True,)
266 278
267 279 # set some variables for email notification
268 280 pr_target_repo_url = h.url(
269 281 'summary_home', repo_name=pr_target_repo.repo_name,
270 282 qualified=True)
271 283
272 284 pr_source_repo_url = h.url(
273 285 'summary_home', repo_name=pr_source_repo.repo_name,
274 286 qualified=True)
275 287
276 288 # pull request specifics
277 289 kwargs.update({
278 290 'pull_request': pull_request_obj,
279 291 'pr_id': pull_request_obj.pull_request_id,
280 292 'pr_target_repo': pr_target_repo,
281 293 'pr_target_repo_url': pr_target_repo_url,
282 294 'pr_source_repo': pr_source_repo,
283 295 'pr_source_repo_url': pr_source_repo_url,
284 296 'pr_comment_url': pr_comment_url,
285 297 'pr_closing': closing_pr,
286 298 })
287 299 if send_email:
288 300 # pre-generate the subject for notification itself
289 301 (subject,
290 302 _h, _e, # we don't care about those
291 303 body_plaintext) = EmailNotificationModel().render_email(
292 304 notification_type, **kwargs)
293 305
294 306 mention_recipients = set(
295 307 self._extract_mentions(text)).difference(recipients)
296 308
297 309 # create notification objects, and emails
298 310 NotificationModel().create(
299 311 created_by=user,
300 312 notification_subject=subject,
301 313 notification_body=body_plaintext,
302 314 notification_type=notification_type,
303 315 recipients=recipients,
304 316 mention_recipients=mention_recipients,
305 317 email_kwargs=kwargs,
306 318 )
307 319
308 320 action = (
309 321 'user_commented_pull_request:{}'.format(
310 322 comment.pull_request.pull_request_id)
311 323 if comment.pull_request
312 324 else 'user_commented_revision:{}'.format(comment.revision)
313 325 )
314 326 action_logger(user, action, comment.repo)
315 327
316 328 registry = get_current_registry()
317 329 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
318 330 channelstream_config = rhodecode_plugins.get('channelstream', {})
319 331 msg_url = ''
320 332 if commit_obj:
321 333 msg_url = commit_comment_url
322 334 repo_name = repo.repo_name
323 335 elif pull_request_obj:
324 336 msg_url = pr_comment_url
325 337 repo_name = pr_target_repo.repo_name
326 338
327 339 if channelstream_config.get('enabled'):
328 340 message = '<strong>{}</strong> {} - ' \
329 341 '<a onclick="window.location=\'{}\';' \
330 342 'window.location.reload()">' \
331 343 '<strong>{}</strong></a>'
332 344 message = message.format(
333 345 user.username, _('made a comment'), msg_url,
334 346 _('Show it now'))
335 347 channel = '/repo${}$/pr/{}'.format(
336 348 repo_name,
337 349 pull_request_id
338 350 )
339 351 payload = {
340 352 'type': 'message',
341 353 'timestamp': datetime.utcnow(),
342 354 'user': 'system',
343 355 'exclude_users': [user.username],
344 356 'channel': channel,
345 357 'message': {
346 358 'message': message,
347 359 'level': 'info',
348 360 'topic': '/notifications'
349 361 }
350 362 }
351 363 channelstream_request(channelstream_config, [payload],
352 364 '/message', raise_exc=False)
353 365
354 366 return comment
355 367
356 368 def delete(self, comment):
357 369 """
358 370 Deletes given comment
359 371
360 372 :param comment_id:
361 373 """
362 374 comment = self.__get_commit_comment(comment)
363 375 Session().delete(comment)
364 376
365 377 return comment
366 378
367 379 def get_all_comments(self, repo_id, revision=None, pull_request=None):
368 380 q = ChangesetComment.query()\
369 381 .filter(ChangesetComment.repo_id == repo_id)
370 382 if revision:
371 383 q = q.filter(ChangesetComment.revision == revision)
372 384 elif pull_request:
373 385 pull_request = self.__get_pull_request(pull_request)
374 386 q = q.filter(ChangesetComment.pull_request == pull_request)
375 387 else:
376 388 raise Exception('Please specify commit or pull_request')
377 389 q = q.order_by(ChangesetComment.created_on)
378 390 return q.all()
379 391
380 392 def get_url(self, comment):
381 393 comment = self.__get_commit_comment(comment)
382 394 if comment.pull_request:
383 395 return h.url(
384 396 'pullrequest_show',
385 397 repo_name=comment.pull_request.target_repo.repo_name,
386 398 pull_request_id=comment.pull_request.pull_request_id,
387 399 anchor='comment-%s' % comment.comment_id,
388 400 qualified=True,)
389 401 else:
390 402 return h.url(
391 403 'changeset_home',
392 404 repo_name=comment.repo.repo_name,
393 405 revision=comment.revision,
394 406 anchor='comment-%s' % comment.comment_id,
395 407 qualified=True,)
396 408
397 409 def get_comments(self, repo_id, revision=None, pull_request=None):
398 410 """
399 411 Gets main comments based on revision or pull_request_id
400 412
401 413 :param repo_id:
402 414 :param revision:
403 415 :param pull_request:
404 416 """
405 417
406 418 q = ChangesetComment.query()\
407 419 .filter(ChangesetComment.repo_id == repo_id)\
408 420 .filter(ChangesetComment.line_no == None)\
409 421 .filter(ChangesetComment.f_path == None)
410 422 if revision:
411 423 q = q.filter(ChangesetComment.revision == revision)
412 424 elif pull_request:
413 425 pull_request = self.__get_pull_request(pull_request)
414 426 q = q.filter(ChangesetComment.pull_request == pull_request)
415 427 else:
416 428 raise Exception('Please specify commit or pull_request')
417 429 q = q.order_by(ChangesetComment.created_on)
418 430 return q.all()
419 431
420 432 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
421 433 q = self._get_inline_comments_query(repo_id, revision, pull_request)
422 434 return self._group_comments_by_path_and_line_number(q)
423 435
424 436 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
425 437 version=None):
426 438 inline_cnt = 0
427 439 for fname, per_line_comments in inline_comments.iteritems():
428 440 for lno, comments in per_line_comments.iteritems():
429 441 for comm in comments:
430 442 if not comm.outdated_at_version(version) and skip_outdated:
431 443 inline_cnt += 1
432 444
433 445 return inline_cnt
434 446
435 447 def get_outdated_comments(self, repo_id, pull_request):
436 448 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
437 449 # of a pull request.
438 450 q = self._all_inline_comments_of_pull_request(pull_request)
439 451 q = q.filter(
440 452 ChangesetComment.display_state ==
441 453 ChangesetComment.COMMENT_OUTDATED
442 454 ).order_by(ChangesetComment.comment_id.asc())
443 455
444 456 return self._group_comments_by_path_and_line_number(q)
445 457
446 458 def _get_inline_comments_query(self, repo_id, revision, pull_request):
447 459 # TODO: johbo: Split this into two methods: One for PR and one for
448 460 # commit.
449 461 if revision:
450 462 q = Session().query(ChangesetComment).filter(
451 463 ChangesetComment.repo_id == repo_id,
452 464 ChangesetComment.line_no != null(),
453 465 ChangesetComment.f_path != null(),
454 466 ChangesetComment.revision == revision)
455 467
456 468 elif pull_request:
457 469 pull_request = self.__get_pull_request(pull_request)
458 470 if not CommentsModel.use_outdated_comments(pull_request):
459 471 q = self._visible_inline_comments_of_pull_request(pull_request)
460 472 else:
461 473 q = self._all_inline_comments_of_pull_request(pull_request)
462 474
463 475 else:
464 476 raise Exception('Please specify commit or pull_request_id')
465 477 q = q.order_by(ChangesetComment.comment_id.asc())
466 478 return q
467 479
468 480 def _group_comments_by_path_and_line_number(self, q):
469 481 comments = q.all()
470 482 paths = collections.defaultdict(lambda: collections.defaultdict(list))
471 483 for co in comments:
472 484 paths[co.f_path][co.line_no].append(co)
473 485 return paths
474 486
475 487 @classmethod
476 488 def needed_extra_diff_context(cls):
477 489 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
478 490
479 491 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
480 492 if not CommentsModel.use_outdated_comments(pull_request):
481 493 return
482 494
483 495 comments = self._visible_inline_comments_of_pull_request(pull_request)
484 496 comments_to_outdate = comments.all()
485 497
486 498 for comment in comments_to_outdate:
487 499 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
488 500
489 501 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
490 502 diff_line = _parse_comment_line_number(comment.line_no)
491 503
492 504 try:
493 505 old_context = old_diff_proc.get_context_of_line(
494 506 path=comment.f_path, diff_line=diff_line)
495 507 new_context = new_diff_proc.get_context_of_line(
496 508 path=comment.f_path, diff_line=diff_line)
497 509 except (diffs.LineNotInDiffException,
498 510 diffs.FileNotInDiffException):
499 511 comment.display_state = ChangesetComment.COMMENT_OUTDATED
500 512 return
501 513
502 514 if old_context == new_context:
503 515 return
504 516
505 517 if self._should_relocate_diff_line(diff_line):
506 518 new_diff_lines = new_diff_proc.find_context(
507 519 path=comment.f_path, context=old_context,
508 520 offset=self.DIFF_CONTEXT_BEFORE)
509 521 if not new_diff_lines:
510 522 comment.display_state = ChangesetComment.COMMENT_OUTDATED
511 523 else:
512 524 new_diff_line = self._choose_closest_diff_line(
513 525 diff_line, new_diff_lines)
514 526 comment.line_no = _diff_to_comment_line_number(new_diff_line)
515 527 else:
516 528 comment.display_state = ChangesetComment.COMMENT_OUTDATED
517 529
518 530 def _should_relocate_diff_line(self, diff_line):
519 531 """
520 532 Checks if relocation shall be tried for the given `diff_line`.
521 533
522 534 If a comment points into the first lines, then we can have a situation
523 535 that after an update another line has been added on top. In this case
524 536 we would find the context still and move the comment around. This
525 537 would be wrong.
526 538 """
527 539 should_relocate = (
528 540 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
529 541 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
530 542 return should_relocate
531 543
532 544 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
533 545 candidate = new_diff_lines[0]
534 546 best_delta = _diff_line_delta(diff_line, candidate)
535 547 for new_diff_line in new_diff_lines[1:]:
536 548 delta = _diff_line_delta(diff_line, new_diff_line)
537 549 if delta < best_delta:
538 550 candidate = new_diff_line
539 551 best_delta = delta
540 552 return candidate
541 553
542 554 def _visible_inline_comments_of_pull_request(self, pull_request):
543 555 comments = self._all_inline_comments_of_pull_request(pull_request)
544 556 comments = comments.filter(
545 557 coalesce(ChangesetComment.display_state, '') !=
546 558 ChangesetComment.COMMENT_OUTDATED)
547 559 return comments
548 560
549 561 def _all_inline_comments_of_pull_request(self, pull_request):
550 562 comments = Session().query(ChangesetComment)\
551 563 .filter(ChangesetComment.line_no != None)\
552 564 .filter(ChangesetComment.f_path != None)\
553 565 .filter(ChangesetComment.pull_request == pull_request)
554 566 return comments
555 567
556 568 def _all_general_comments_of_pull_request(self, pull_request):
557 569 comments = Session().query(ChangesetComment)\
558 570 .filter(ChangesetComment.line_no == None)\
559 571 .filter(ChangesetComment.f_path == None)\
560 572 .filter(ChangesetComment.pull_request == pull_request)
561 573 return comments
562 574
563 575 @staticmethod
564 576 def use_outdated_comments(pull_request):
565 577 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
566 578 settings = settings_model.get_general_settings()
567 579 return settings.get('rhodecode_use_outdated_comments', False)
568 580
569 581
570 582 def _parse_comment_line_number(line_no):
571 583 """
572 584 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
573 585 """
574 586 old_line = None
575 587 new_line = None
576 588 if line_no.startswith('o'):
577 589 old_line = int(line_no[1:])
578 590 elif line_no.startswith('n'):
579 591 new_line = int(line_no[1:])
580 592 else:
581 593 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
582 594 return diffs.DiffLineNumber(old_line, new_line)
583 595
584 596
585 597 def _diff_to_comment_line_number(diff_line):
586 598 if diff_line.new is not None:
587 599 return u'n{}'.format(diff_line.new)
588 600 elif diff_line.old is not None:
589 601 return u'o{}'.format(diff_line.old)
590 602 return u''
591 603
592 604
593 605 def _diff_line_delta(a, b):
594 606 if None not in (a.new, b.new):
595 607 return abs(a.new - b.new)
596 608 elif None not in (a.old, b.old):
597 609 return abs(a.old - b.old)
598 610 else:
599 611 raise ValueError(
600 612 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2223 +1,2257 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wide-mode-wrapper {
113 113 max-width:4000px !important;
114 114 }
115 115
116 116 .wrapper {
117 117 position: relative;
118 118 max-width: @wrapper-maxwidth;
119 119 margin: 0 auto;
120 120 }
121 121
122 122 #content {
123 123 clear: both;
124 124 padding: 0 @contentpadding;
125 125 }
126 126
127 127 .advanced-settings-fields{
128 128 input{
129 129 margin-left: @textmargin;
130 130 margin-right: @padding/2;
131 131 }
132 132 }
133 133
134 134 .cs_files_title {
135 135 margin: @pagepadding 0 0;
136 136 }
137 137
138 138 input.inline[type="file"] {
139 139 display: inline;
140 140 }
141 141
142 142 .error_page {
143 143 margin: 10% auto;
144 144
145 145 h1 {
146 146 color: @grey2;
147 147 }
148 148
149 149 .alert {
150 150 margin: @padding 0;
151 151 }
152 152
153 153 .error-branding {
154 154 font-family: @text-semibold;
155 155 color: @grey4;
156 156 }
157 157
158 158 .error_message {
159 159 font-family: @text-regular;
160 160 }
161 161
162 162 .sidebar {
163 163 min-height: 275px;
164 164 margin: 0;
165 165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 166 border: none;
167 167 }
168 168
169 169 .main-content {
170 170 position: relative;
171 171 margin: 0 @sidebarpadding @sidebarpadding;
172 172 padding: 0 0 0 @sidebarpadding;
173 173 border-left: @border-thickness solid @grey5;
174 174
175 175 @media (max-width:767px) {
176 176 clear: both;
177 177 width: 100%;
178 178 margin: 0;
179 179 border: none;
180 180 }
181 181 }
182 182
183 183 .inner-column {
184 184 float: left;
185 185 width: 29.75%;
186 186 min-height: 150px;
187 187 margin: @sidebarpadding 2% 0 0;
188 188 padding: 0 2% 0 0;
189 189 border-right: @border-thickness solid @grey5;
190 190
191 191 @media (max-width:767px) {
192 192 clear: both;
193 193 width: 100%;
194 194 border: none;
195 195 }
196 196
197 197 ul {
198 198 padding-left: 1.25em;
199 199 }
200 200
201 201 &:last-child {
202 202 margin: @sidebarpadding 0 0;
203 203 border: none;
204 204 }
205 205
206 206 h4 {
207 207 margin: 0 0 @padding;
208 208 font-family: @text-semibold;
209 209 }
210 210 }
211 211 }
212 212 .error-page-logo {
213 213 width: 130px;
214 214 height: 160px;
215 215 }
216 216
217 217 // HEADER
218 218 .header {
219 219
220 220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 221 // for the header and then remove the min-height. I chose a smaller value
222 222 // intentionally here to avoid rendering issues in the main navigation.
223 223 min-height: 49px;
224 224
225 225 position: relative;
226 226 vertical-align: bottom;
227 227 padding: 0 @header-padding;
228 228 background-color: @grey2;
229 229 color: @grey5;
230 230
231 231 .title {
232 232 overflow: visible;
233 233 }
234 234
235 235 &:before,
236 236 &:after {
237 237 content: "";
238 238 clear: both;
239 239 width: 100%;
240 240 }
241 241
242 242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 243 .select2-container .select2-choice .select2-arrow {
244 244 display: none;
245 245 }
246 246 }
247 247
248 248 #header-inner {
249 249 &.title {
250 250 margin: 0;
251 251 }
252 252 &:before,
253 253 &:after {
254 254 content: "";
255 255 clear: both;
256 256 }
257 257 }
258 258
259 259 // Gists
260 260 #files_data {
261 261 clear: both; //for firefox
262 262 }
263 263 #gistid {
264 264 margin-right: @padding;
265 265 }
266 266
267 267 // Global Settings Editor
268 268 .textarea.editor {
269 269 float: left;
270 270 position: relative;
271 271 max-width: @texteditor-width;
272 272
273 273 select {
274 274 position: absolute;
275 275 top:10px;
276 276 right:0;
277 277 }
278 278
279 279 .CodeMirror {
280 280 margin: 0;
281 281 }
282 282
283 283 .help-block {
284 284 margin: 0 0 @padding;
285 285 padding:.5em;
286 286 background-color: @grey6;
287 287 }
288 288 }
289 289
290 290 ul.auth_plugins {
291 291 margin: @padding 0 @padding @legend-width;
292 292 padding: 0;
293 293
294 294 li {
295 295 margin-bottom: @padding;
296 296 line-height: 1em;
297 297 list-style-type: none;
298 298
299 299 .auth_buttons .btn {
300 300 margin-right: @padding;
301 301 }
302 302
303 303 &:before { content: none; }
304 304 }
305 305 }
306 306
307 307
308 308 // My Account PR list
309 309
310 310 #show_closed {
311 311 margin: 0 1em 0 0;
312 312 }
313 313
314 314 .pullrequestlist {
315 315 .closed {
316 316 background-color: @grey6;
317 317 }
318 318 .td-status {
319 319 padding-left: .5em;
320 320 }
321 321 .log-container .truncate {
322 322 height: 2.75em;
323 323 white-space: pre-line;
324 324 }
325 325 table.rctable .user {
326 326 padding-left: 0;
327 327 }
328 328 table.rctable {
329 329 td.td-description,
330 330 .rc-user {
331 331 min-width: auto;
332 332 }
333 333 }
334 334 }
335 335
336 336 // Pull Requests
337 337
338 338 .pullrequests_section_head {
339 339 display: block;
340 340 clear: both;
341 341 margin: @padding 0;
342 342 font-family: @text-bold;
343 343 }
344 344
345 345 .pr-origininfo, .pr-targetinfo {
346 346 position: relative;
347 347
348 348 .tag {
349 349 display: inline-block;
350 350 margin: 0 1em .5em 0;
351 351 }
352 352
353 353 .clone-url {
354 354 display: inline-block;
355 355 margin: 0 0 .5em 0;
356 356 padding: 0;
357 357 line-height: 1.2em;
358 358 }
359 359 }
360 360
361 361 .pr-pullinfo {
362 362 clear: both;
363 363 margin: .5em 0;
364 364 }
365 365
366 366 #pr-title-input {
367 367 width: 72%;
368 368 font-size: 1em;
369 369 font-family: @text-bold;
370 370 margin: 0;
371 371 padding: 0 0 0 @padding/4;
372 372 line-height: 1.7em;
373 373 color: @text-color;
374 374 letter-spacing: .02em;
375 375 }
376 376
377 377 #pullrequest_title {
378 378 width: 100%;
379 379 box-sizing: border-box;
380 380 }
381 381
382 382 #pr_open_message {
383 383 border: @border-thickness solid #fff;
384 384 border-radius: @border-radius;
385 385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 386 text-align: right;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .pr-submit-button {
391 391 float: right;
392 392 margin: 0 0 0 5px;
393 393 }
394 394
395 395 .pr-spacing-container {
396 396 padding: 20px;
397 397 clear: both
398 398 }
399 399
400 400 #pr-description-input {
401 401 margin-bottom: 0;
402 402 }
403 403
404 404 .pr-description-label {
405 405 vertical-align: top;
406 406 }
407 407
408 408 .perms_section_head {
409 409 min-width: 625px;
410 410
411 411 h2 {
412 412 margin-bottom: 0;
413 413 }
414 414
415 415 .label-checkbox {
416 416 float: left;
417 417 }
418 418
419 419 &.field {
420 420 margin: @space 0 @padding;
421 421 }
422 422
423 423 &:first-child.field {
424 424 margin-top: 0;
425 425
426 426 .label {
427 427 margin-top: 0;
428 428 padding-top: 0;
429 429 }
430 430
431 431 .radios {
432 432 padding-top: 0;
433 433 }
434 434 }
435 435
436 436 .radios {
437 437 float: right;
438 438 position: relative;
439 439 width: 405px;
440 440 }
441 441 }
442 442
443 443 //--- MODULES ------------------//
444 444
445 445
446 446 // Server Announcement
447 447 #server-announcement {
448 448 width: 95%;
449 449 margin: @padding auto;
450 450 padding: @padding;
451 451 border-width: 2px;
452 452 border-style: solid;
453 453 .border-radius(2px);
454 454 font-family: @text-bold;
455 455
456 456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 461 }
462 462
463 463 // Fixed Sidebar Column
464 464 .sidebar-col-wrapper {
465 465 padding-left: @sidebar-all-width;
466 466
467 467 .sidebar {
468 468 width: @sidebar-width;
469 469 margin-left: -@sidebar-all-width;
470 470 }
471 471 }
472 472
473 473 .sidebar-col-wrapper.scw-small {
474 474 padding-left: @sidebar-small-all-width;
475 475
476 476 .sidebar {
477 477 width: @sidebar-small-width;
478 478 margin-left: -@sidebar-small-all-width;
479 479 }
480 480 }
481 481
482 482
483 483 // FOOTER
484 484 #footer {
485 485 padding: 0;
486 486 text-align: center;
487 487 vertical-align: middle;
488 488 color: @grey2;
489 489 background-color: @grey6;
490 490
491 491 p {
492 492 margin: 0;
493 493 padding: 1em;
494 494 line-height: 1em;
495 495 }
496 496
497 497 .server-instance { //server instance
498 498 display: none;
499 499 }
500 500
501 501 .title {
502 502 float: none;
503 503 margin: 0 auto;
504 504 }
505 505 }
506 506
507 507 button.close {
508 508 padding: 0;
509 509 cursor: pointer;
510 510 background: transparent;
511 511 border: 0;
512 512 .box-shadow(none);
513 513 -webkit-appearance: none;
514 514 }
515 515
516 516 .close {
517 517 float: right;
518 518 font-size: 21px;
519 519 font-family: @text-bootstrap;
520 520 line-height: 1em;
521 521 font-weight: bold;
522 522 color: @grey2;
523 523
524 524 &:hover,
525 525 &:focus {
526 526 color: @grey1;
527 527 text-decoration: none;
528 528 cursor: pointer;
529 529 }
530 530 }
531 531
532 532 // GRID
533 533 .sorting,
534 534 .sorting_desc,
535 535 .sorting_asc {
536 536 cursor: pointer;
537 537 }
538 538 .sorting_desc:after {
539 539 content: "\00A0\25B2";
540 540 font-size: .75em;
541 541 }
542 542 .sorting_asc:after {
543 543 content: "\00A0\25BC";
544 544 font-size: .68em;
545 545 }
546 546
547 547
548 548 .user_auth_tokens {
549 549
550 550 &.truncate {
551 551 white-space: nowrap;
552 552 overflow: hidden;
553 553 text-overflow: ellipsis;
554 554 }
555 555
556 556 .fields .field .input {
557 557 margin: 0;
558 558 }
559 559
560 560 input#description {
561 561 width: 100px;
562 562 margin: 0;
563 563 }
564 564
565 565 .drop-menu {
566 566 // TODO: johbo: Remove this, should work out of the box when
567 567 // having multiple inputs inline
568 568 margin: 0 0 0 5px;
569 569 }
570 570 }
571 571 #user_list_table {
572 572 .closed {
573 573 background-color: @grey6;
574 574 }
575 575 }
576 576
577 577
578 578 input {
579 579 &.disabled {
580 580 opacity: .5;
581 581 }
582 582 }
583 583
584 584 // remove extra padding in firefox
585 585 input::-moz-focus-inner { border:0; padding:0 }
586 586
587 587 .adjacent input {
588 588 margin-bottom: @padding;
589 589 }
590 590
591 591 .permissions_boxes {
592 592 display: block;
593 593 }
594 594
595 595 //TODO: lisa: this should be in tables
596 596 .show_more_col {
597 597 width: 20px;
598 598 }
599 599
600 600 //FORMS
601 601
602 602 .medium-inline,
603 603 input#description.medium-inline {
604 604 display: inline;
605 605 width: @medium-inline-input-width;
606 606 min-width: 100px;
607 607 }
608 608
609 609 select {
610 610 //reset
611 611 -webkit-appearance: none;
612 612 -moz-appearance: none;
613 613
614 614 display: inline-block;
615 615 height: 28px;
616 616 width: auto;
617 617 margin: 0 @padding @padding 0;
618 618 padding: 0 18px 0 8px;
619 619 line-height:1em;
620 620 font-size: @basefontsize;
621 621 border: @border-thickness solid @rcblue;
622 622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 623 color: @rcblue;
624 624
625 625 &:after {
626 626 content: "\00A0\25BE";
627 627 }
628 628
629 629 &:focus {
630 630 outline: none;
631 631 }
632 632 }
633 633
634 634 option {
635 635 &:focus {
636 636 outline: none;
637 637 }
638 638 }
639 639
640 640 input,
641 641 textarea {
642 642 padding: @input-padding;
643 643 border: @input-border-thickness solid @border-highlight-color;
644 644 .border-radius (@border-radius);
645 645 font-family: @text-light;
646 646 font-size: @basefontsize;
647 647
648 648 &.input-sm {
649 649 padding: 5px;
650 650 }
651 651
652 652 &#description {
653 653 min-width: @input-description-minwidth;
654 654 min-height: 1em;
655 655 padding: 10px;
656 656 }
657 657 }
658 658
659 659 .field-sm {
660 660 input,
661 661 textarea {
662 662 padding: 5px;
663 663 }
664 664 }
665 665
666 666 textarea {
667 667 display: block;
668 668 clear: both;
669 669 width: 100%;
670 670 min-height: 100px;
671 671 margin-bottom: @padding;
672 672 .box-sizing(border-box);
673 673 overflow: auto;
674 674 }
675 675
676 676 label {
677 677 font-family: @text-light;
678 678 }
679 679
680 680 // GRAVATARS
681 681 // centers gravatar on username to the right
682 682
683 683 .gravatar {
684 684 display: inline;
685 685 min-width: 16px;
686 686 min-height: 16px;
687 687 margin: -5px 0;
688 688 padding: 0;
689 689 line-height: 1em;
690 690 border: 1px solid @grey4;
691 691 box-sizing: content-box;
692 692
693 693 &.gravatar-large {
694 694 margin: -0.5em .25em -0.5em 0;
695 695 }
696 696
697 697 & + .user {
698 698 display: inline;
699 699 margin: 0;
700 700 padding: 0 0 0 .17em;
701 701 line-height: 1em;
702 702 }
703 703 }
704 704
705 705 .user-inline-data {
706 706 display: inline-block;
707 707 float: left;
708 708 padding-left: .5em;
709 709 line-height: 1.3em;
710 710 }
711 711
712 712 .rc-user { // gravatar + user wrapper
713 713 float: left;
714 714 position: relative;
715 715 min-width: 100px;
716 716 max-width: 200px;
717 717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
718 718 display: block;
719 719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
720 720
721 721
722 722 .gravatar {
723 723 display: block;
724 724 position: absolute;
725 725 top: 0;
726 726 left: 0;
727 727 min-width: @gravatar-size;
728 728 min-height: @gravatar-size;
729 729 margin: 0;
730 730 }
731 731
732 732 .user {
733 733 display: block;
734 734 max-width: 175px;
735 735 padding-top: 2px;
736 736 overflow: hidden;
737 737 text-overflow: ellipsis;
738 738 }
739 739 }
740 740
741 741 .gist-gravatar,
742 742 .journal_container {
743 743 .gravatar-large {
744 744 margin: 0 .5em -10px 0;
745 745 }
746 746 }
747 747
748 748
749 749 // ADMIN SETTINGS
750 750
751 751 // Tag Patterns
752 752 .tag_patterns {
753 753 .tag_input {
754 754 margin-bottom: @padding;
755 755 }
756 756 }
757 757
758 758 .locked_input {
759 759 position: relative;
760 760
761 761 input {
762 762 display: inline;
763 763 margin-top: 3px;
764 764 }
765 765
766 766 br {
767 767 display: none;
768 768 }
769 769
770 770 .error-message {
771 771 float: left;
772 772 width: 100%;
773 773 }
774 774
775 775 .lock_input_button {
776 776 display: inline;
777 777 }
778 778
779 779 .help-block {
780 780 clear: both;
781 781 }
782 782 }
783 783
784 784 // Notifications
785 785
786 786 .notifications_buttons {
787 787 margin: 0 0 @space 0;
788 788 padding: 0;
789 789
790 790 .btn {
791 791 display: inline-block;
792 792 }
793 793 }
794 794
795 795 .notification-list {
796 796
797 797 div {
798 798 display: inline-block;
799 799 vertical-align: middle;
800 800 }
801 801
802 802 .container {
803 803 display: block;
804 804 margin: 0 0 @padding 0;
805 805 }
806 806
807 807 .delete-notifications {
808 808 margin-left: @padding;
809 809 text-align: right;
810 810 cursor: pointer;
811 811 }
812 812
813 813 .read-notifications {
814 814 margin-left: @padding/2;
815 815 text-align: right;
816 816 width: 35px;
817 817 cursor: pointer;
818 818 }
819 819
820 820 .icon-minus-sign {
821 821 color: @alert2;
822 822 }
823 823
824 824 .icon-ok-sign {
825 825 color: @alert1;
826 826 }
827 827 }
828 828
829 829 .user_settings {
830 830 float: left;
831 831 clear: both;
832 832 display: block;
833 833 width: 100%;
834 834
835 835 .gravatar_box {
836 836 margin-bottom: @padding;
837 837
838 838 &:after {
839 839 content: " ";
840 840 clear: both;
841 841 width: 100%;
842 842 }
843 843 }
844 844
845 845 .fields .field {
846 846 clear: both;
847 847 }
848 848 }
849 849
850 850 .advanced_settings {
851 851 margin-bottom: @space;
852 852
853 853 .help-block {
854 854 margin-left: 0;
855 855 }
856 856
857 857 button + .help-block {
858 858 margin-top: @padding;
859 859 }
860 860 }
861 861
862 862 // admin settings radio buttons and labels
863 863 .label-2 {
864 864 float: left;
865 865 width: @label2-width;
866 866
867 867 label {
868 868 color: @grey1;
869 869 }
870 870 }
871 871 .checkboxes {
872 872 float: left;
873 873 width: @checkboxes-width;
874 874 margin-bottom: @padding;
875 875
876 876 .checkbox {
877 877 width: 100%;
878 878
879 879 label {
880 880 margin: 0;
881 881 padding: 0;
882 882 }
883 883 }
884 884
885 885 .checkbox + .checkbox {
886 886 display: inline-block;
887 887 }
888 888
889 889 label {
890 890 margin-right: 1em;
891 891 }
892 892 }
893 893
894 894 // CHANGELOG
895 895 .container_header {
896 896 float: left;
897 897 display: block;
898 898 width: 100%;
899 899 margin: @padding 0 @padding;
900 900
901 901 #filter_changelog {
902 902 float: left;
903 903 margin-right: @padding;
904 904 }
905 905
906 906 .breadcrumbs_light {
907 907 display: inline-block;
908 908 }
909 909 }
910 910
911 911 .info_box {
912 912 float: right;
913 913 }
914 914
915 915
916 916 #graph_nodes {
917 917 padding-top: 43px;
918 918 }
919 919
920 920 #graph_content{
921 921
922 922 // adjust for table headers so that graph renders properly
923 923 // #graph_nodes padding - table cell padding
924 924 padding-top: (@space - (@basefontsize * 2.4));
925 925
926 926 &.graph_full_width {
927 927 width: 100%;
928 928 max-width: 100%;
929 929 }
930 930 }
931 931
932 932 #graph {
933 933 .flag_status {
934 934 margin: 0;
935 935 }
936 936
937 937 .pagination-left {
938 938 float: left;
939 939 clear: both;
940 940 }
941 941
942 942 .log-container {
943 943 max-width: 345px;
944 944
945 945 .message{
946 946 max-width: 340px;
947 947 }
948 948 }
949 949
950 950 .graph-col-wrapper {
951 951 padding-left: 110px;
952 952
953 953 #graph_nodes {
954 954 width: 100px;
955 955 margin-left: -110px;
956 956 float: left;
957 957 clear: left;
958 958 }
959 959 }
960 960 }
961 961
962 962 #filter_changelog {
963 963 float: left;
964 964 }
965 965
966 966
967 967 //--- THEME ------------------//
968 968
969 969 #logo {
970 970 float: left;
971 971 margin: 9px 0 0 0;
972 972
973 973 .header {
974 974 background-color: transparent;
975 975 }
976 976
977 977 a {
978 978 display: inline-block;
979 979 }
980 980
981 981 img {
982 982 height:30px;
983 983 }
984 984 }
985 985
986 986 .logo-wrapper {
987 987 float:left;
988 988 }
989 989
990 990 .branding{
991 991 float: left;
992 992 padding: 9px 2px;
993 993 line-height: 1em;
994 994 font-size: @navigation-fontsize;
995 995 }
996 996
997 997 img {
998 998 border: none;
999 999 outline: none;
1000 1000 }
1001 1001 user-profile-header
1002 1002 label {
1003 1003
1004 1004 input[type="checkbox"] {
1005 1005 margin-right: 1em;
1006 1006 }
1007 1007 input[type="radio"] {
1008 1008 margin-right: 1em;
1009 1009 }
1010 1010 }
1011 1011
1012 1012 .flag_status {
1013 1013 margin: 2px 8px 6px 2px;
1014 1014 &.under_review {
1015 1015 .circle(5px, @alert3);
1016 1016 }
1017 1017 &.approved {
1018 1018 .circle(5px, @alert1);
1019 1019 }
1020 1020 &.rejected,
1021 1021 &.forced_closed{
1022 1022 .circle(5px, @alert2);
1023 1023 }
1024 1024 &.not_reviewed {
1025 1025 .circle(5px, @grey5);
1026 1026 }
1027 1027 }
1028 1028
1029 1029 .flag_status_comment_box {
1030 1030 margin: 5px 6px 0px 2px;
1031 1031 }
1032 1032 .test_pattern_preview {
1033 1033 margin: @space 0;
1034 1034
1035 1035 p {
1036 1036 margin-bottom: 0;
1037 1037 border-bottom: @border-thickness solid @border-default-color;
1038 1038 color: @grey3;
1039 1039 }
1040 1040
1041 1041 .btn {
1042 1042 margin-bottom: @padding;
1043 1043 }
1044 1044 }
1045 1045 #test_pattern_result {
1046 1046 display: none;
1047 1047 &:extend(pre);
1048 1048 padding: .9em;
1049 1049 color: @grey3;
1050 1050 background-color: @grey7;
1051 1051 border-right: @border-thickness solid @border-default-color;
1052 1052 border-bottom: @border-thickness solid @border-default-color;
1053 1053 border-left: @border-thickness solid @border-default-color;
1054 1054 }
1055 1055
1056 1056 #repo_vcs_settings {
1057 1057 #inherit_overlay_vcs_default {
1058 1058 display: none;
1059 1059 }
1060 1060 #inherit_overlay_vcs_custom {
1061 1061 display: custom;
1062 1062 }
1063 1063 &.inherited {
1064 1064 #inherit_overlay_vcs_default {
1065 1065 display: block;
1066 1066 }
1067 1067 #inherit_overlay_vcs_custom {
1068 1068 display: none;
1069 1069 }
1070 1070 }
1071 1071 }
1072 1072
1073 1073 .issue-tracker-link {
1074 1074 color: @rcblue;
1075 1075 }
1076 1076
1077 1077 // Issue Tracker Table Show/Hide
1078 1078 #repo_issue_tracker {
1079 1079 #inherit_overlay {
1080 1080 display: none;
1081 1081 }
1082 1082 #custom_overlay {
1083 1083 display: custom;
1084 1084 }
1085 1085 &.inherited {
1086 1086 #inherit_overlay {
1087 1087 display: block;
1088 1088 }
1089 1089 #custom_overlay {
1090 1090 display: none;
1091 1091 }
1092 1092 }
1093 1093 }
1094 1094 table.issuetracker {
1095 1095 &.readonly {
1096 1096 tr, td {
1097 1097 color: @grey3;
1098 1098 }
1099 1099 }
1100 1100 .edit {
1101 1101 display: none;
1102 1102 }
1103 1103 .editopen {
1104 1104 .edit {
1105 1105 display: inline;
1106 1106 }
1107 1107 .entry {
1108 1108 display: none;
1109 1109 }
1110 1110 }
1111 1111 tr td.td-action {
1112 1112 min-width: 117px;
1113 1113 }
1114 1114 td input {
1115 1115 max-width: none;
1116 1116 min-width: 30px;
1117 1117 width: 80%;
1118 1118 }
1119 1119 .issuetracker_pref input {
1120 1120 width: 40%;
1121 1121 }
1122 1122 input.edit_issuetracker_update {
1123 1123 margin-right: 0;
1124 1124 width: auto;
1125 1125 }
1126 1126 }
1127 1127
1128 1128 table.integrations {
1129 1129 .td-icon {
1130 1130 width: 20px;
1131 1131 .integration-icon {
1132 1132 height: 20px;
1133 1133 width: 20px;
1134 1134 }
1135 1135 }
1136 1136 }
1137 1137
1138 1138 .integrations {
1139 1139 a.integration-box {
1140 1140 color: @text-color;
1141 1141 &:hover {
1142 1142 .panel {
1143 1143 background: #fbfbfb;
1144 1144 }
1145 1145 }
1146 1146 .integration-icon {
1147 1147 width: 30px;
1148 1148 height: 30px;
1149 1149 margin-right: 20px;
1150 1150 float: left;
1151 1151 }
1152 1152
1153 1153 .panel-body {
1154 1154 padding: 10px;
1155 1155 }
1156 1156 .panel {
1157 1157 margin-bottom: 10px;
1158 1158 }
1159 1159 h2 {
1160 1160 display: inline-block;
1161 1161 margin: 0;
1162 1162 min-width: 140px;
1163 1163 }
1164 1164 }
1165 1165 }
1166 1166
1167 1167 //Permissions Settings
1168 1168 #add_perm {
1169 1169 margin: 0 0 @padding;
1170 1170 cursor: pointer;
1171 1171 }
1172 1172
1173 1173 .perm_ac {
1174 1174 input {
1175 1175 width: 95%;
1176 1176 }
1177 1177 }
1178 1178
1179 1179 .autocomplete-suggestions {
1180 1180 width: auto !important; // overrides autocomplete.js
1181 1181 margin: 0;
1182 1182 border: @border-thickness solid @rcblue;
1183 1183 border-radius: @border-radius;
1184 1184 color: @rcblue;
1185 1185 background-color: white;
1186 1186 }
1187 1187 .autocomplete-selected {
1188 1188 background: #F0F0F0;
1189 1189 }
1190 1190 .ac-container-wrap {
1191 1191 margin: 0;
1192 1192 padding: 8px;
1193 1193 border-bottom: @border-thickness solid @rclightblue;
1194 1194 list-style-type: none;
1195 1195 cursor: pointer;
1196 1196
1197 1197 &:hover {
1198 1198 background-color: @rclightblue;
1199 1199 }
1200 1200
1201 1201 img {
1202 1202 height: @gravatar-size;
1203 1203 width: @gravatar-size;
1204 1204 margin-right: 1em;
1205 1205 }
1206 1206
1207 1207 strong {
1208 1208 font-weight: normal;
1209 1209 }
1210 1210 }
1211 1211
1212 1212 // Settings Dropdown
1213 1213 .user-menu .container {
1214 1214 padding: 0 4px;
1215 1215 margin: 0;
1216 1216 }
1217 1217
1218 1218 .user-menu .gravatar {
1219 1219 cursor: pointer;
1220 1220 }
1221 1221
1222 1222 .codeblock {
1223 1223 margin-bottom: @padding;
1224 1224 clear: both;
1225 1225
1226 1226 .stats{
1227 1227 overflow: hidden;
1228 1228 }
1229 1229
1230 1230 .message{
1231 1231 textarea{
1232 1232 margin: 0;
1233 1233 }
1234 1234 }
1235 1235
1236 1236 .code-header {
1237 1237 .stats {
1238 1238 line-height: 2em;
1239 1239
1240 1240 .revision_id {
1241 1241 margin-left: 0;
1242 1242 }
1243 1243 .buttons {
1244 1244 padding-right: 0;
1245 1245 }
1246 1246 }
1247 1247
1248 1248 .item{
1249 1249 margin-right: 0.5em;
1250 1250 }
1251 1251 }
1252 1252
1253 1253 #editor_container{
1254 1254 position: relative;
1255 1255 margin: @padding;
1256 1256 }
1257 1257 }
1258 1258
1259 1259 #file_history_container {
1260 1260 display: none;
1261 1261 }
1262 1262
1263 1263 .file-history-inner {
1264 1264 margin-bottom: 10px;
1265 1265 }
1266 1266
1267 1267 // Pull Requests
1268 1268 .summary-details {
1269 1269 width: 72%;
1270 1270 }
1271 1271 .pr-summary {
1272 1272 border-bottom: @border-thickness solid @grey5;
1273 1273 margin-bottom: @space;
1274 1274 }
1275 1275 .reviewers-title {
1276 1276 width: 25%;
1277 1277 min-width: 200px;
1278 1278 }
1279 1279 .reviewers {
1280 1280 width: 25%;
1281 1281 min-width: 200px;
1282 1282 }
1283 1283 .reviewers ul li {
1284 1284 position: relative;
1285 1285 width: 100%;
1286 1286 margin-bottom: 8px;
1287 1287 }
1288 1288 .reviewers_member {
1289 1289 width: 100%;
1290 1290 overflow: auto;
1291 1291 }
1292 1292 .reviewer_reason {
1293 1293 padding-left: 20px;
1294 1294 }
1295 1295 .reviewer_status {
1296 1296 display: inline-block;
1297 1297 vertical-align: top;
1298 1298 width: 7%;
1299 1299 min-width: 20px;
1300 1300 height: 1.2em;
1301 1301 margin-top: 3px;
1302 1302 line-height: 1em;
1303 1303 }
1304 1304
1305 1305 .reviewer_name {
1306 1306 display: inline-block;
1307 1307 max-width: 83%;
1308 1308 padding-right: 20px;
1309 1309 vertical-align: middle;
1310 1310 line-height: 1;
1311 1311
1312 1312 .rc-user {
1313 1313 min-width: 0;
1314 1314 margin: -2px 1em 0 0;
1315 1315 }
1316 1316
1317 1317 .reviewer {
1318 1318 float: left;
1319 1319 }
1320 1320
1321 1321 &.to-delete {
1322 1322 .user,
1323 1323 .reviewer {
1324 1324 text-decoration: line-through;
1325 1325 }
1326 1326 }
1327 1327 }
1328 1328
1329 1329 .reviewer_member_remove {
1330 1330 position: absolute;
1331 1331 right: 0;
1332 1332 top: 0;
1333 1333 width: 16px;
1334 1334 margin-bottom: 10px;
1335 1335 padding: 0;
1336 1336 color: black;
1337 1337 }
1338 1338 .reviewer_member_status {
1339 1339 margin-top: 5px;
1340 1340 }
1341 1341 .pr-summary #summary{
1342 1342 width: 100%;
1343 1343 }
1344 1344 .pr-summary .action_button:hover {
1345 1345 border: 0;
1346 1346 cursor: pointer;
1347 1347 }
1348 1348 .pr-details-title {
1349 1349 padding-bottom: 8px;
1350 1350 border-bottom: @border-thickness solid @grey5;
1351 1351
1352 1352 .action_button.disabled {
1353 1353 color: @grey4;
1354 1354 cursor: inherit;
1355 1355 }
1356 1356 .action_button {
1357 1357 color: @rcblue;
1358 1358 }
1359 1359 }
1360 1360 .pr-details-content {
1361 1361 margin-top: @textmargin;
1362 1362 margin-bottom: @textmargin;
1363 1363 }
1364 1364 .pr-description {
1365 1365 white-space:pre-wrap;
1366 1366 }
1367 1367 .group_members {
1368 1368 margin-top: 0;
1369 1369 padding: 0;
1370 1370 list-style: outside none none;
1371 1371
1372 1372 img {
1373 1373 height: @gravatar-size;
1374 1374 width: @gravatar-size;
1375 1375 margin-right: .5em;
1376 1376 margin-left: 3px;
1377 1377 }
1378 1378
1379 1379 .to-delete {
1380 1380 .user {
1381 1381 text-decoration: line-through;
1382 1382 }
1383 1383 }
1384 1384 }
1385 1385
1386 1386 .compare_view_commits_title {
1387 1387 .disabled {
1388 1388 cursor: inherit;
1389 1389 &:hover{
1390 1390 background-color: inherit;
1391 1391 color: inherit;
1392 1392 }
1393 1393 }
1394 1394 }
1395 1395
1396 1396 // new entry in group_members
1397 1397 .td-author-new-entry {
1398 1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1399 1399 }
1400 1400
1401 1401 .usergroup_member_remove {
1402 1402 width: 16px;
1403 1403 margin-bottom: 10px;
1404 1404 padding: 0;
1405 1405 color: black !important;
1406 1406 cursor: pointer;
1407 1407 }
1408 1408
1409 1409 .reviewer_ac .ac-input {
1410 1410 width: 92%;
1411 1411 margin-bottom: 1em;
1412 1412 }
1413 1413
1414 1414 .compare_view_commits tr{
1415 1415 height: 20px;
1416 1416 }
1417 1417 .compare_view_commits td {
1418 1418 vertical-align: top;
1419 1419 padding-top: 10px;
1420 1420 }
1421 1421 .compare_view_commits .author {
1422 1422 margin-left: 5px;
1423 1423 }
1424 1424
1425 1425 .compare_view_files {
1426 1426 width: 100%;
1427 1427
1428 1428 td {
1429 1429 vertical-align: middle;
1430 1430 }
1431 1431 }
1432 1432
1433 1433 .compare_view_filepath {
1434 1434 color: @grey1;
1435 1435 }
1436 1436
1437 1437 .show_more {
1438 1438 display: inline-block;
1439 1439 position: relative;
1440 1440 vertical-align: middle;
1441 1441 width: 4px;
1442 1442 height: @basefontsize;
1443 1443
1444 1444 &:after {
1445 1445 content: "\00A0\25BE";
1446 1446 display: inline-block;
1447 1447 width:10px;
1448 1448 line-height: 5px;
1449 1449 font-size: 12px;
1450 1450 cursor: pointer;
1451 1451 }
1452 1452 }
1453 1453
1454 1454 .journal_more .show_more {
1455 1455 display: inline;
1456 1456
1457 1457 &:after {
1458 1458 content: none;
1459 1459 }
1460 1460 }
1461 1461
1462 1462 .open .show_more:after,
1463 1463 .select2-dropdown-open .show_more:after {
1464 1464 .rotate(180deg);
1465 1465 margin-left: 4px;
1466 1466 }
1467 1467
1468 1468
1469 1469 .compare_view_commits .collapse_commit:after {
1470 1470 cursor: pointer;
1471 1471 content: "\00A0\25B4";
1472 1472 margin-left: -3px;
1473 1473 font-size: 17px;
1474 1474 color: @grey4;
1475 1475 }
1476 1476
1477 1477 .diff_links {
1478 1478 margin-left: 8px;
1479 1479 }
1480 1480
1481 1481 div.ancestor {
1482 1482 margin: -30px 0px;
1483 1483 }
1484 1484
1485 1485 .cs_icon_td input[type="checkbox"] {
1486 1486 display: none;
1487 1487 }
1488 1488
1489 1489 .cs_icon_td .expand_file_icon:after {
1490 1490 cursor: pointer;
1491 1491 content: "\00A0\25B6";
1492 1492 font-size: 12px;
1493 1493 color: @grey4;
1494 1494 }
1495 1495
1496 1496 .cs_icon_td .collapse_file_icon:after {
1497 1497 cursor: pointer;
1498 1498 content: "\00A0\25BC";
1499 1499 font-size: 12px;
1500 1500 color: @grey4;
1501 1501 }
1502 1502
1503 1503 /*new binary
1504 1504 NEW_FILENODE = 1
1505 1505 DEL_FILENODE = 2
1506 1506 MOD_FILENODE = 3
1507 1507 RENAMED_FILENODE = 4
1508 1508 COPIED_FILENODE = 5
1509 1509 CHMOD_FILENODE = 6
1510 1510 BIN_FILENODE = 7
1511 1511 */
1512 1512 .cs_files_expand {
1513 1513 font-size: @basefontsize + 5px;
1514 1514 line-height: 1.8em;
1515 1515 float: right;
1516 1516 }
1517 1517
1518 1518 .cs_files_expand span{
1519 1519 color: @rcblue;
1520 1520 cursor: pointer;
1521 1521 }
1522 1522 .cs_files {
1523 1523 clear: both;
1524 1524 padding-bottom: @padding;
1525 1525
1526 1526 .cur_cs {
1527 1527 margin: 10px 2px;
1528 1528 font-weight: bold;
1529 1529 }
1530 1530
1531 1531 .node {
1532 1532 float: left;
1533 1533 }
1534 1534
1535 1535 .changes {
1536 1536 float: right;
1537 1537 color: white;
1538 1538 font-size: @basefontsize - 4px;
1539 1539 margin-top: 4px;
1540 1540 opacity: 0.6;
1541 1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1542 1542
1543 1543 .added {
1544 1544 background-color: @alert1;
1545 1545 float: left;
1546 1546 text-align: center;
1547 1547 }
1548 1548
1549 1549 .deleted {
1550 1550 background-color: @alert2;
1551 1551 float: left;
1552 1552 text-align: center;
1553 1553 }
1554 1554
1555 1555 .bin {
1556 1556 background-color: @alert1;
1557 1557 text-align: center;
1558 1558 }
1559 1559
1560 1560 /*new binary*/
1561 1561 .bin.bin1 {
1562 1562 background-color: @alert1;
1563 1563 text-align: center;
1564 1564 }
1565 1565
1566 1566 /*deleted binary*/
1567 1567 .bin.bin2 {
1568 1568 background-color: @alert2;
1569 1569 text-align: center;
1570 1570 }
1571 1571
1572 1572 /*mod binary*/
1573 1573 .bin.bin3 {
1574 1574 background-color: @grey2;
1575 1575 text-align: center;
1576 1576 }
1577 1577
1578 1578 /*rename file*/
1579 1579 .bin.bin4 {
1580 1580 background-color: @alert4;
1581 1581 text-align: center;
1582 1582 }
1583 1583
1584 1584 /*copied file*/
1585 1585 .bin.bin5 {
1586 1586 background-color: @alert4;
1587 1587 text-align: center;
1588 1588 }
1589 1589
1590 1590 /*chmod file*/
1591 1591 .bin.bin6 {
1592 1592 background-color: @grey2;
1593 1593 text-align: center;
1594 1594 }
1595 1595 }
1596 1596 }
1597 1597
1598 1598 .cs_files .cs_added, .cs_files .cs_A,
1599 1599 .cs_files .cs_added, .cs_files .cs_M,
1600 1600 .cs_files .cs_added, .cs_files .cs_D {
1601 1601 height: 16px;
1602 1602 padding-right: 10px;
1603 1603 margin-top: 7px;
1604 1604 text-align: left;
1605 1605 }
1606 1606
1607 1607 .cs_icon_td {
1608 1608 min-width: 16px;
1609 1609 width: 16px;
1610 1610 }
1611 1611
1612 1612 .pull-request-merge {
1613 padding: 10px 0;
1613 border: 1px solid @grey5;
1614 padding: 10px 0px 20px;
1614 1615 margin-top: 10px;
1615 1616 margin-bottom: 20px;
1616 1617 }
1617 1618
1619 .pull-request-merge ul {
1620 padding: 0px 0px;
1621 }
1622
1623 .pull-request-merge li:before{
1624 content:none;
1625 }
1626
1618 1627 .pull-request-merge .pull-request-wrap {
1619 height: 25px;
1620 padding: 5px 0;
1628 height: auto;
1629 padding: 0px 0px;
1630 text-align: right;
1621 1631 }
1622 1632
1623 1633 .pull-request-merge span {
1624 margin-right: 10px;
1625 }
1634 margin-right: 5px;
1635 }
1636
1637 .pull-request-merge-actions {
1638 height: 30px;
1639 padding: 0px 0px;
1640 }
1641
1642 .merge-message {
1643 font-size: 1.2em
1644 }
1645 .merge-message li{
1646 text-decoration: none;
1647 }
1648
1649 .merge-message.success i {
1650 color:@alert1;
1651 }
1652 .merge-message.warning i {
1653 color: @alert3;
1654 }
1655 .merge-message.error i {
1656 color:@alert2;
1657 }
1658
1659
1626 1660
1627 1661 .pr-versions {
1628 1662 position: relative;
1629 1663 top: 6px;
1630 1664 }
1631 1665
1632 1666 #close_pull_request {
1633 1667 margin-right: 0px;
1634 1668 }
1635 1669
1636 1670 .empty_data {
1637 1671 color: @grey4;
1638 1672 }
1639 1673
1640 1674 #changeset_compare_view_content {
1641 1675 margin-bottom: @space;
1642 1676 clear: both;
1643 1677 width: 100%;
1644 1678 box-sizing: border-box;
1645 1679 .border-radius(@border-radius);
1646 1680
1647 1681 .help-block {
1648 1682 margin: @padding 0;
1649 1683 color: @text-color;
1650 1684 }
1651 1685
1652 1686 .empty_data {
1653 1687 margin: @padding 0;
1654 1688 }
1655 1689
1656 1690 .alert {
1657 1691 margin-bottom: @space;
1658 1692 }
1659 1693 }
1660 1694
1661 1695 .table_disp {
1662 1696 .status {
1663 1697 width: auto;
1664 1698
1665 1699 .flag_status {
1666 1700 float: left;
1667 1701 }
1668 1702 }
1669 1703 }
1670 1704
1671 1705 .status_box_menu {
1672 1706 margin: 0;
1673 1707 }
1674 1708
1675 1709 .notification-table{
1676 1710 margin-bottom: @space;
1677 1711 display: table;
1678 1712 width: 100%;
1679 1713
1680 1714 .container{
1681 1715 display: table-row;
1682 1716
1683 1717 .notification-header{
1684 1718 border-bottom: @border-thickness solid @border-default-color;
1685 1719 }
1686 1720
1687 1721 .notification-subject{
1688 1722 display: table-cell;
1689 1723 }
1690 1724 }
1691 1725 }
1692 1726
1693 1727 // Notifications
1694 1728 .notification-header{
1695 1729 display: table;
1696 1730 width: 100%;
1697 1731 padding: floor(@basefontsize/2) 0;
1698 1732 line-height: 1em;
1699 1733
1700 1734 .desc, .delete-notifications, .read-notifications{
1701 1735 display: table-cell;
1702 1736 text-align: left;
1703 1737 }
1704 1738
1705 1739 .desc{
1706 1740 width: 1163px;
1707 1741 }
1708 1742
1709 1743 .delete-notifications, .read-notifications{
1710 1744 width: 35px;
1711 1745 min-width: 35px; //fixes when only one button is displayed
1712 1746 }
1713 1747 }
1714 1748
1715 1749 .notification-body {
1716 1750 .markdown-block,
1717 1751 .rst-block {
1718 1752 padding: @padding 0;
1719 1753 }
1720 1754
1721 1755 .notification-subject {
1722 1756 padding: @textmargin 0;
1723 1757 border-bottom: @border-thickness solid @border-default-color;
1724 1758 }
1725 1759 }
1726 1760
1727 1761
1728 1762 .notifications_buttons{
1729 1763 float: right;
1730 1764 }
1731 1765
1732 1766 #notification-status{
1733 1767 display: inline;
1734 1768 }
1735 1769
1736 1770 // Repositories
1737 1771
1738 1772 #summary.fields{
1739 1773 display: table;
1740 1774
1741 1775 .field{
1742 1776 display: table-row;
1743 1777
1744 1778 .label-summary{
1745 1779 display: table-cell;
1746 1780 min-width: @label-summary-minwidth;
1747 1781 padding-top: @padding/2;
1748 1782 padding-bottom: @padding/2;
1749 1783 padding-right: @padding/2;
1750 1784 }
1751 1785
1752 1786 .input{
1753 1787 display: table-cell;
1754 1788 padding: @padding/2;
1755 1789
1756 1790 input{
1757 1791 min-width: 29em;
1758 1792 padding: @padding/4;
1759 1793 }
1760 1794 }
1761 1795 .statistics, .downloads{
1762 1796 .disabled{
1763 1797 color: @grey4;
1764 1798 }
1765 1799 }
1766 1800 }
1767 1801 }
1768 1802
1769 1803 #summary{
1770 1804 width: 70%;
1771 1805 }
1772 1806
1773 1807
1774 1808 // Journal
1775 1809 .journal.title {
1776 1810 h5 {
1777 1811 float: left;
1778 1812 margin: 0;
1779 1813 width: 70%;
1780 1814 }
1781 1815
1782 1816 ul {
1783 1817 float: right;
1784 1818 display: inline-block;
1785 1819 margin: 0;
1786 1820 width: 30%;
1787 1821 text-align: right;
1788 1822
1789 1823 li {
1790 1824 display: inline;
1791 1825 font-size: @journal-fontsize;
1792 1826 line-height: 1em;
1793 1827
1794 1828 &:before { content: none; }
1795 1829 }
1796 1830 }
1797 1831 }
1798 1832
1799 1833 .filterexample {
1800 1834 position: absolute;
1801 1835 top: 95px;
1802 1836 left: @contentpadding;
1803 1837 color: @rcblue;
1804 1838 font-size: 11px;
1805 1839 font-family: @text-regular;
1806 1840 cursor: help;
1807 1841
1808 1842 &:hover {
1809 1843 color: @rcdarkblue;
1810 1844 }
1811 1845
1812 1846 @media (max-width:768px) {
1813 1847 position: relative;
1814 1848 top: auto;
1815 1849 left: auto;
1816 1850 display: block;
1817 1851 }
1818 1852 }
1819 1853
1820 1854
1821 1855 #journal{
1822 1856 margin-bottom: @space;
1823 1857
1824 1858 .journal_day{
1825 1859 margin-bottom: @textmargin/2;
1826 1860 padding-bottom: @textmargin/2;
1827 1861 font-size: @journal-fontsize;
1828 1862 border-bottom: @border-thickness solid @border-default-color;
1829 1863 }
1830 1864
1831 1865 .journal_container{
1832 1866 margin-bottom: @space;
1833 1867
1834 1868 .journal_user{
1835 1869 display: inline-block;
1836 1870 }
1837 1871 .journal_action_container{
1838 1872 display: block;
1839 1873 margin-top: @textmargin;
1840 1874
1841 1875 div{
1842 1876 display: inline;
1843 1877 }
1844 1878
1845 1879 div.journal_action_params{
1846 1880 display: block;
1847 1881 }
1848 1882
1849 1883 div.journal_repo:after{
1850 1884 content: "\A";
1851 1885 white-space: pre;
1852 1886 }
1853 1887
1854 1888 div.date{
1855 1889 display: block;
1856 1890 margin-bottom: @textmargin;
1857 1891 }
1858 1892 }
1859 1893 }
1860 1894 }
1861 1895
1862 1896 // Files
1863 1897 .edit-file-title {
1864 1898 border-bottom: @border-thickness solid @border-default-color;
1865 1899
1866 1900 .breadcrumbs {
1867 1901 margin-bottom: 0;
1868 1902 }
1869 1903 }
1870 1904
1871 1905 .edit-file-fieldset {
1872 1906 margin-top: @sidebarpadding;
1873 1907
1874 1908 .fieldset {
1875 1909 .left-label {
1876 1910 width: 13%;
1877 1911 }
1878 1912 .right-content {
1879 1913 width: 87%;
1880 1914 max-width: 100%;
1881 1915 }
1882 1916 .filename-label {
1883 1917 margin-top: 13px;
1884 1918 }
1885 1919 .commit-message-label {
1886 1920 margin-top: 4px;
1887 1921 }
1888 1922 .file-upload-input {
1889 1923 input {
1890 1924 display: none;
1891 1925 }
1892 1926 }
1893 1927 p {
1894 1928 margin-top: 5px;
1895 1929 }
1896 1930
1897 1931 }
1898 1932 .custom-path-link {
1899 1933 margin-left: 5px;
1900 1934 }
1901 1935 #commit {
1902 1936 resize: vertical;
1903 1937 }
1904 1938 }
1905 1939
1906 1940 .delete-file-preview {
1907 1941 max-height: 250px;
1908 1942 }
1909 1943
1910 1944 .new-file,
1911 1945 #filter_activate,
1912 1946 #filter_deactivate {
1913 1947 float: left;
1914 1948 margin: 0 0 0 15px;
1915 1949 }
1916 1950
1917 1951 h3.files_location{
1918 1952 line-height: 2.4em;
1919 1953 }
1920 1954
1921 1955 .browser-nav {
1922 1956 display: table;
1923 1957 margin-bottom: @space;
1924 1958
1925 1959
1926 1960 .info_box {
1927 1961 display: inline-table;
1928 1962 height: 2.5em;
1929 1963
1930 1964 .browser-cur-rev, .info_box_elem {
1931 1965 display: table-cell;
1932 1966 vertical-align: middle;
1933 1967 }
1934 1968
1935 1969 .info_box_elem {
1936 1970 border-top: @border-thickness solid @rcblue;
1937 1971 border-bottom: @border-thickness solid @rcblue;
1938 1972
1939 1973 #at_rev, a {
1940 1974 padding: 0.6em 0.9em;
1941 1975 margin: 0;
1942 1976 .box-shadow(none);
1943 1977 border: 0;
1944 1978 height: 12px;
1945 1979 }
1946 1980
1947 1981 input#at_rev {
1948 1982 max-width: 50px;
1949 1983 text-align: right;
1950 1984 }
1951 1985
1952 1986 &.previous {
1953 1987 border: @border-thickness solid @rcblue;
1954 1988 .disabled {
1955 1989 color: @grey4;
1956 1990 cursor: not-allowed;
1957 1991 }
1958 1992 }
1959 1993
1960 1994 &.next {
1961 1995 border: @border-thickness solid @rcblue;
1962 1996 .disabled {
1963 1997 color: @grey4;
1964 1998 cursor: not-allowed;
1965 1999 }
1966 2000 }
1967 2001 }
1968 2002
1969 2003 .browser-cur-rev {
1970 2004
1971 2005 span{
1972 2006 margin: 0;
1973 2007 color: @rcblue;
1974 2008 height: 12px;
1975 2009 display: inline-block;
1976 2010 padding: 0.7em 1em ;
1977 2011 border: @border-thickness solid @rcblue;
1978 2012 margin-right: @padding;
1979 2013 }
1980 2014 }
1981 2015 }
1982 2016
1983 2017 .search_activate {
1984 2018 display: table-cell;
1985 2019 vertical-align: middle;
1986 2020
1987 2021 input, label{
1988 2022 margin: 0;
1989 2023 padding: 0;
1990 2024 }
1991 2025
1992 2026 input{
1993 2027 margin-left: @textmargin;
1994 2028 }
1995 2029
1996 2030 }
1997 2031 }
1998 2032
1999 2033 .browser-cur-rev{
2000 2034 margin-bottom: @textmargin;
2001 2035 }
2002 2036
2003 2037 #node_filter_box_loading{
2004 2038 .info_text;
2005 2039 }
2006 2040
2007 2041 .browser-search {
2008 2042 margin: -25px 0px 5px 0px;
2009 2043 }
2010 2044
2011 2045 .node-filter {
2012 2046 font-size: @repo-title-fontsize;
2013 2047 padding: 4px 0px 0px 0px;
2014 2048
2015 2049 .node-filter-path {
2016 2050 float: left;
2017 2051 color: @grey4;
2018 2052 }
2019 2053 .node-filter-input {
2020 2054 float: left;
2021 2055 margin: -2px 0px 0px 2px;
2022 2056 input {
2023 2057 padding: 2px;
2024 2058 border: none;
2025 2059 font-size: @repo-title-fontsize;
2026 2060 }
2027 2061 }
2028 2062 }
2029 2063
2030 2064
2031 2065 .browser-result{
2032 2066 td a{
2033 2067 margin-left: 0.5em;
2034 2068 display: inline-block;
2035 2069
2036 2070 em{
2037 2071 font-family: @text-bold;
2038 2072 }
2039 2073 }
2040 2074 }
2041 2075
2042 2076 .browser-highlight{
2043 2077 background-color: @grey5-alpha;
2044 2078 }
2045 2079
2046 2080
2047 2081 // Search
2048 2082
2049 2083 .search-form{
2050 2084 #q {
2051 2085 width: @search-form-width;
2052 2086 }
2053 2087 .fields{
2054 2088 margin: 0 0 @space;
2055 2089 }
2056 2090
2057 2091 label{
2058 2092 display: inline-block;
2059 2093 margin-right: @textmargin;
2060 2094 padding-top: 0.25em;
2061 2095 }
2062 2096
2063 2097
2064 2098 .results{
2065 2099 clear: both;
2066 2100 margin: 0 0 @padding;
2067 2101 }
2068 2102 }
2069 2103
2070 2104 div.search-feedback-items {
2071 2105 display: inline-block;
2072 2106 padding:0px 0px 0px 96px;
2073 2107 }
2074 2108
2075 2109 div.search-code-body {
2076 2110 background-color: #ffffff; padding: 5px 0 5px 10px;
2077 2111 pre {
2078 2112 .match { background-color: #faffa6;}
2079 2113 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2080 2114 }
2081 2115 }
2082 2116
2083 2117 .expand_commit.search {
2084 2118 .show_more.open {
2085 2119 height: auto;
2086 2120 max-height: none;
2087 2121 }
2088 2122 }
2089 2123
2090 2124 .search-results {
2091 2125
2092 2126 h2 {
2093 2127 margin-bottom: 0;
2094 2128 }
2095 2129 .codeblock {
2096 2130 border: none;
2097 2131 background: transparent;
2098 2132 }
2099 2133
2100 2134 .codeblock-header {
2101 2135 border: none;
2102 2136 background: transparent;
2103 2137 }
2104 2138
2105 2139 .code-body {
2106 2140 border: @border-thickness solid @border-default-color;
2107 2141 .border-radius(@border-radius);
2108 2142 }
2109 2143
2110 2144 .td-commit {
2111 2145 &:extend(pre);
2112 2146 border-bottom: @border-thickness solid @border-default-color;
2113 2147 }
2114 2148
2115 2149 .message {
2116 2150 height: auto;
2117 2151 max-width: 350px;
2118 2152 white-space: normal;
2119 2153 text-overflow: initial;
2120 2154 overflow: visible;
2121 2155
2122 2156 .match { background-color: #faffa6;}
2123 2157 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2124 2158 }
2125 2159
2126 2160 }
2127 2161
2128 2162 table.rctable td.td-search-results div {
2129 2163 max-width: 100%;
2130 2164 }
2131 2165
2132 2166 #tip-box, .tip-box{
2133 2167 padding: @menupadding/2;
2134 2168 display: block;
2135 2169 border: @border-thickness solid @border-highlight-color;
2136 2170 .border-radius(@border-radius);
2137 2171 background-color: white;
2138 2172 z-index: 99;
2139 2173 white-space: pre-wrap;
2140 2174 }
2141 2175
2142 2176 #linktt {
2143 2177 width: 79px;
2144 2178 }
2145 2179
2146 2180 #help_kb .modal-content{
2147 2181 max-width: 750px;
2148 2182 margin: 10% auto;
2149 2183
2150 2184 table{
2151 2185 td,th{
2152 2186 border-bottom: none;
2153 2187 line-height: 2.5em;
2154 2188 }
2155 2189 th{
2156 2190 padding-bottom: @textmargin/2;
2157 2191 }
2158 2192 td.keys{
2159 2193 text-align: center;
2160 2194 }
2161 2195 }
2162 2196
2163 2197 .block-left{
2164 2198 width: 45%;
2165 2199 margin-right: 5%;
2166 2200 }
2167 2201 .modal-footer{
2168 2202 clear: both;
2169 2203 }
2170 2204 .key.tag{
2171 2205 padding: 0.5em;
2172 2206 background-color: @rcblue;
2173 2207 color: white;
2174 2208 border-color: @rcblue;
2175 2209 .box-shadow(none);
2176 2210 }
2177 2211 }
2178 2212
2179 2213
2180 2214
2181 2215 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2182 2216
2183 2217 @import 'statistics-graph';
2184 2218 @import 'tables';
2185 2219 @import 'forms';
2186 2220 @import 'diff';
2187 2221 @import 'summary';
2188 2222 @import 'navigation';
2189 2223
2190 2224 //--- SHOW/HIDE SECTIONS --//
2191 2225
2192 2226 .btn-collapse {
2193 2227 float: right;
2194 2228 text-align: right;
2195 2229 font-family: @text-light;
2196 2230 font-size: @basefontsize;
2197 2231 cursor: pointer;
2198 2232 border: none;
2199 2233 color: @rcblue;
2200 2234 }
2201 2235
2202 2236 table.rctable,
2203 2237 table.dataTable {
2204 2238 .btn-collapse {
2205 2239 float: right;
2206 2240 text-align: right;
2207 2241 }
2208 2242 }
2209 2243
2210 2244
2211 2245 // TODO: johbo: Fix for IE10, this avoids that we see a border
2212 2246 // and padding around checkboxes and radio boxes. Move to the right place,
2213 2247 // or better: Remove this once we did the form refactoring.
2214 2248 input[type=checkbox],
2215 2249 input[type=radio] {
2216 2250 padding: 0;
2217 2251 border: none;
2218 2252 }
2219 2253
2220 2254 .toggle-ajax-spinner{
2221 2255 height: 16px;
2222 2256 width: 16px;
2223 2257 }
@@ -1,796 +1,802 b''
1 1 // # Copyright (C) 2010-2017 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 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.cmBox = this.withLineNo('#text');
96 96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
97 97
98 98 this.statusChange = this.withLineNo('#change_status');
99 99
100 100 this.submitForm = formElement;
101 101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 102 this.submitButtonText = this.submitButton.val();
103 103
104 104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 105 {'repo_name': templateContext.repo_name});
106 106
107 107 if (resolvesCommentId){
108 108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 110 $(this.commentType).prop('disabled', true);
111 111 $(this.commentType).addClass('disabled');
112 112
113 113 // disable select
114 114 setTimeout(function() {
115 115 $(self.statusChange).select2('readonly', true);
116 116 }, 10);
117 117
118 118 var resolvedInfo = (
119 119 '<li class="">' +
120 120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 122 '</li>'
123 123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 125 }
126 126
127 127 // based on commitId, or pullRequestId decide where do we submit
128 128 // out data
129 129 if (this.commitId){
130 130 this.submitUrl = pyroutes.url('changeset_comment',
131 131 {'repo_name': templateContext.repo_name,
132 132 'revision': this.commitId});
133 133 this.selfUrl = pyroutes.url('changeset_home',
134 134 {'repo_name': templateContext.repo_name,
135 135 'revision': this.commitId});
136 136
137 137 } else if (this.pullRequestId) {
138 138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 139 {'repo_name': templateContext.repo_name,
140 140 'pull_request_id': this.pullRequestId});
141 141 this.selfUrl = pyroutes.url('pullrequest_show',
142 142 {'repo_name': templateContext.repo_name,
143 143 'pull_request_id': this.pullRequestId});
144 144
145 145 } else {
146 146 throw new Error(
147 147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 148 }
149 149
150 150 // FUNCTIONS and helpers
151 151 var self = this;
152 152
153 153 this.isInline = function(){
154 154 return this.lineNo && this.lineNo != 'general';
155 155 };
156 156
157 157 this.getCmInstance = function(){
158 158 return this.cm
159 159 };
160 160
161 161 this.setPlaceholder = function(placeholder) {
162 162 var cm = this.getCmInstance();
163 163 if (cm){
164 164 cm.setOption('placeholder', placeholder);
165 165 }
166 166 };
167 167
168 168 this.getCommentStatus = function() {
169 169 return $(this.submitForm).find(this.statusChange).val();
170 170 };
171 171 this.getCommentType = function() {
172 172 return $(this.submitForm).find(this.commentType).val();
173 173 };
174 174
175 175 this.getResolvesId = function() {
176 176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 177 };
178 178 this.markCommentResolved = function(resolvedCommentId){
179 179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 181 };
182 182
183 183 this.isAllowedToSubmit = function() {
184 184 return !$(this.submitButton).prop('disabled');
185 185 };
186 186
187 187 this.initStatusChangeSelector = function(){
188 188 var formatChangeStatus = function(state, escapeMarkup) {
189 189 var originalOption = state.element;
190 190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 191 '<span>' + escapeMarkup(state.text) + '</span>';
192 192 };
193 193 var formatResult = function(result, container, query, escapeMarkup) {
194 194 return formatChangeStatus(result, escapeMarkup);
195 195 };
196 196
197 197 var formatSelection = function(data, container, escapeMarkup) {
198 198 return formatChangeStatus(data, escapeMarkup);
199 199 };
200 200
201 201 $(this.submitForm).find(this.statusChange).select2({
202 202 placeholder: _gettext('Status Review'),
203 203 formatResult: formatResult,
204 204 formatSelection: formatSelection,
205 205 containerCssClass: "drop-menu status_box_menu",
206 206 dropdownCssClass: "drop-menu-dropdown",
207 207 dropdownAutoWidth: true,
208 208 minimumResultsForSearch: -1
209 209 });
210 210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 211 var status = self.getCommentStatus();
212 212 if (status && !self.isInline()) {
213 213 $(self.submitButton).prop('disabled', false);
214 214 }
215 215
216 216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 217 self.setPlaceholder(placeholderText)
218 218 })
219 219 };
220 220
221 221 // reset the comment form into it's original state
222 222 this.resetCommentFormState = function(content) {
223 223 content = content || '';
224 224
225 225 $(this.editContainer).show();
226 226 $(this.editButton).parent().addClass('active');
227 227
228 228 $(this.previewContainer).hide();
229 229 $(this.previewButton).parent().removeClass('active');
230 230
231 231 this.setActionButtonsDisabled(true);
232 232 self.cm.setValue(content);
233 233 self.cm.setOption("readOnly", false);
234 234
235 235 if (this.resolvesId) {
236 236 // destroy the resolve action
237 237 $(this.resolvesId).parent().remove();
238 238 }
239 239
240 240 $(this.statusChange).select2('readonly', false);
241 241 };
242 242
243 this.globalSubmitSuccessCallback = function(){};
243 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
247 }
248 };
244 249
245 250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
246 251 failHandler = failHandler || function() {};
247 252 var postData = toQueryString(postData);
248 253 var request = $.ajax({
249 254 url: url,
250 255 type: 'POST',
251 256 data: postData,
252 257 headers: {'X-PARTIAL-XHR': true}
253 258 })
254 259 .done(function(data) {
255 260 successHandler(data);
256 261 })
257 262 .fail(function(data, textStatus, errorThrown){
258 263 alert(
259 264 "Error while submitting comment.\n" +
260 265 "Error code {0} ({1}).".format(data.status, data.statusText));
261 266 failHandler()
262 267 });
263 268 return request;
264 269 };
265 270
266 271 // overwrite a submitHandler, we need to do it for inline comments
267 272 this.setHandleFormSubmit = function(callback) {
268 273 this.handleFormSubmit = callback;
269 274 };
270 275
271 276 // overwrite a submitSuccessHandler
272 277 this.setGlobalSubmitSuccessCallback = function(callback) {
273 278 this.globalSubmitSuccessCallback = callback;
274 279 };
275 280
276 281 // default handler for for submit for main comments
277 282 this.handleFormSubmit = function() {
278 283 var text = self.cm.getValue();
279 284 var status = self.getCommentStatus();
280 285 var commentType = self.getCommentType();
281 286 var resolvesCommentId = self.getResolvesId();
282 287
283 288 if (text === "" && !status) {
284 289 return;
285 290 }
286 291
287 292 var excludeCancelBtn = false;
288 293 var submitEvent = true;
289 294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
290 295 self.cm.setOption("readOnly", true);
291 296
292 297 var postData = {
293 298 'text': text,
294 299 'changeset_status': status,
295 300 'comment_type': commentType,
296 301 'csrf_token': CSRF_TOKEN
297 302 };
298 303 if (resolvesCommentId){
299 304 postData['resolves_comment_id'] = resolvesCommentId;
300 305 }
301 306
302 307 var submitSuccessCallback = function(o) {
303 if (status) {
308 // reload page if we change status for single commit.
309 if (status && self.commitId) {
304 310 location.reload(true);
305 311 } else {
306 312 $('#injected_page_comments').append(o.rendered_text);
307 313 self.resetCommentFormState();
308 314 timeagoActivate();
309 315
310 316 // mark visually which comment was resolved
311 317 if (resolvesCommentId) {
312 318 self.markCommentResolved(resolvesCommentId);
313 319 }
314 320 }
315 321
316 322 // run global callback on submit
317 323 self.globalSubmitSuccessCallback();
318 324
319 325 };
320 326 var submitFailCallback = function(){
321 327 self.resetCommentFormState(text);
322 328 };
323 329 self.submitAjaxPOST(
324 330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
325 331 };
326 332
327 333 this.previewSuccessCallback = function(o) {
328 334 $(self.previewBoxSelector).html(o);
329 335 $(self.previewBoxSelector).removeClass('unloaded');
330 336
331 337 // swap buttons, making preview active
332 338 $(self.previewButton).parent().addClass('active');
333 339 $(self.editButton).parent().removeClass('active');
334 340
335 341 // unlock buttons
336 342 self.setActionButtonsDisabled(false);
337 343 };
338 344
339 345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
340 346 excludeCancelBtn = excludeCancelBtn || false;
341 347 submitEvent = submitEvent || false;
342 348
343 349 $(this.editButton).prop('disabled', state);
344 350 $(this.previewButton).prop('disabled', state);
345 351
346 352 if (!excludeCancelBtn) {
347 353 $(this.cancelButton).prop('disabled', state);
348 354 }
349 355
350 356 var submitState = state;
351 357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
352 358 // if the value of commit review status is set, we allow
353 359 // submit button, but only on Main form, lineNo means inline
354 360 submitState = false
355 361 }
356 362 $(this.submitButton).prop('disabled', submitState);
357 363 if (submitEvent) {
358 364 $(this.submitButton).val(_gettext('Submitting...'));
359 365 } else {
360 366 $(this.submitButton).val(this.submitButtonText);
361 367 }
362 368
363 369 };
364 370
365 371 // lock preview/edit/submit buttons on load, but exclude cancel button
366 372 var excludeCancelBtn = true;
367 373 this.setActionButtonsDisabled(true, excludeCancelBtn);
368 374
369 375 // anonymous users don't have access to initialized CM instance
370 376 if (this.cm !== undefined){
371 377 this.cm.on('change', function(cMirror) {
372 378 if (cMirror.getValue() === "") {
373 379 self.setActionButtonsDisabled(true, excludeCancelBtn)
374 380 } else {
375 381 self.setActionButtonsDisabled(false, excludeCancelBtn)
376 382 }
377 383 });
378 384 }
379 385
380 386 $(this.editButton).on('click', function(e) {
381 387 e.preventDefault();
382 388
383 389 $(self.previewButton).parent().removeClass('active');
384 390 $(self.previewContainer).hide();
385 391
386 392 $(self.editButton).parent().addClass('active');
387 393 $(self.editContainer).show();
388 394
389 395 });
390 396
391 397 $(this.previewButton).on('click', function(e) {
392 398 e.preventDefault();
393 399 var text = self.cm.getValue();
394 400
395 401 if (text === "") {
396 402 return;
397 403 }
398 404
399 405 var postData = {
400 406 'text': text,
401 407 'renderer': templateContext.visual.default_renderer,
402 408 'csrf_token': CSRF_TOKEN
403 409 };
404 410
405 411 // lock ALL buttons on preview
406 412 self.setActionButtonsDisabled(true);
407 413
408 414 $(self.previewBoxSelector).addClass('unloaded');
409 415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
410 416
411 417 $(self.editContainer).hide();
412 418 $(self.previewContainer).show();
413 419
414 420 // by default we reset state of comment preserving the text
415 421 var previewFailCallback = function(){
416 422 self.resetCommentFormState(text)
417 423 };
418 424 self.submitAjaxPOST(
419 425 self.previewUrl, postData, self.previewSuccessCallback,
420 426 previewFailCallback);
421 427
422 428 $(self.previewButton).parent().addClass('active');
423 429 $(self.editButton).parent().removeClass('active');
424 430 });
425 431
426 432 $(this.submitForm).submit(function(e) {
427 433 e.preventDefault();
428 434 var allowedToSubmit = self.isAllowedToSubmit();
429 435 if (!allowedToSubmit){
430 436 return false;
431 437 }
432 438 self.handleFormSubmit();
433 439 });
434 440
435 441 }
436 442
437 443 return CommentForm;
438 444 });
439 445
440 446 /* comments controller */
441 447 var CommentsController = function() {
442 448 var mainComment = '#text';
443 449 var self = this;
444 450
445 451 this.cancelComment = function(node) {
446 452 var $node = $(node);
447 453 var $td = $node.closest('td');
448 454 $node.closest('.comment-inline-form').remove();
449 455 return false;
450 456 };
451 457
452 458 this.getLineNumber = function(node) {
453 459 var $node = $(node);
454 460 return $node.closest('td').attr('data-line-number');
455 461 };
456 462
457 463 this.scrollToComment = function(node, offset, outdated) {
458 464 var outdated = outdated || false;
459 465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
460 466
461 467 if (!node) {
462 468 node = $('.comment-selected');
463 469 if (!node.length) {
464 470 node = $('comment-current')
465 471 }
466 472 }
467 473 $comment = $(node).closest(klass);
468 474 $comments = $(klass);
469 475
470 476 $('.comment-selected').removeClass('comment-selected');
471 477
472 478 var nextIdx = $(klass).index($comment) + offset;
473 479 if (nextIdx >= $comments.length) {
474 480 nextIdx = 0;
475 481 }
476 482 var $next = $(klass).eq(nextIdx);
477 483 var $cb = $next.closest('.cb');
478 484 $cb.removeClass('cb-collapsed');
479 485
480 486 var $filediffCollapseState = $cb.closest('.filediff').prev();
481 487 $filediffCollapseState.prop('checked', false);
482 488 $next.addClass('comment-selected');
483 489 scrollToElement($next);
484 490 return false;
485 491 };
486 492
487 493 this.nextComment = function(node) {
488 494 return self.scrollToComment(node, 1);
489 495 };
490 496
491 497 this.prevComment = function(node) {
492 498 return self.scrollToComment(node, -1);
493 499 };
494 500
495 501 this.nextOutdatedComment = function(node) {
496 502 return self.scrollToComment(node, 1, true);
497 503 };
498 504
499 505 this.prevOutdatedComment = function(node) {
500 506 return self.scrollToComment(node, -1, true);
501 507 };
502 508
503 509 this.deleteComment = function(node) {
504 510 if (!confirm(_gettext('Delete this comment?'))) {
505 511 return false;
506 512 }
507 513 var $node = $(node);
508 514 var $td = $node.closest('td');
509 515 var $comment = $node.closest('.comment');
510 516 var comment_id = $comment.attr('data-comment-id');
511 517 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
512 518 var postData = {
513 519 '_method': 'delete',
514 520 'csrf_token': CSRF_TOKEN
515 521 };
516 522
517 523 $comment.addClass('comment-deleting');
518 524 $comment.hide('fast');
519 525
520 526 var success = function(response) {
521 527 $comment.remove();
522 528 return false;
523 529 };
524 530 var failure = function(data, textStatus, xhr) {
525 531 alert("error processing request: " + textStatus);
526 532 $comment.show('fast');
527 533 $comment.removeClass('comment-deleting');
528 534 return false;
529 535 };
530 536 ajaxPOST(url, postData, success, failure);
531 537 };
532 538
533 539 this.toggleWideMode = function (node) {
534 540 if ($('#content').hasClass('wrapper')) {
535 541 $('#content').removeClass("wrapper");
536 542 $('#content').addClass("wide-mode-wrapper");
537 543 $(node).addClass('btn-success');
538 544 } else {
539 545 $('#content').removeClass("wide-mode-wrapper");
540 546 $('#content').addClass("wrapper");
541 547 $(node).removeClass('btn-success');
542 548 }
543 549 return false;
544 550 };
545 551
546 552 this.toggleComments = function(node, show) {
547 553 var $filediff = $(node).closest('.filediff');
548 554 if (show === true) {
549 555 $filediff.removeClass('hide-comments');
550 556 } else if (show === false) {
551 557 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
552 558 $filediff.addClass('hide-comments');
553 559 } else {
554 560 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
555 561 $filediff.toggleClass('hide-comments');
556 562 }
557 563 return false;
558 564 };
559 565
560 566 this.toggleLineComments = function(node) {
561 567 self.toggleComments(node, true);
562 568 var $node = $(node);
563 569 $node.closest('tr').toggleClass('hide-line-comments');
564 570 };
565 571
566 572 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
567 573 var pullRequestId = templateContext.pull_request_data.pull_request_id;
568 574 var commitId = templateContext.commit_data.commit_id;
569 575
570 576 var commentForm = new CommentForm(
571 577 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
572 578 var cm = commentForm.getCmInstance();
573 579
574 580 if (resolvesCommentId){
575 581 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
576 582 }
577 583
578 584 setTimeout(function() {
579 585 // callbacks
580 586 if (cm !== undefined) {
581 587 commentForm.setPlaceholder(placeholderText);
582 588 if (commentForm.isInline()) {
583 589 cm.focus();
584 590 cm.refresh();
585 591 }
586 592 }
587 593 }, 10);
588 594
589 595 // trigger scrolldown to the resolve comment, since it might be away
590 596 // from the clicked
591 597 if (resolvesCommentId){
592 598 var actionNode = $(commentForm.resolvesActionId).offset();
593 599
594 600 setTimeout(function() {
595 601 if (actionNode) {
596 602 $('body, html').animate({scrollTop: actionNode.top}, 10);
597 603 }
598 604 }, 100);
599 605 }
600 606
601 607 return commentForm;
602 608 };
603 609
604 610 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
605 611
606 612 var tmpl = $('#cb-comment-general-form-template').html();
607 613 tmpl = tmpl.format(null, 'general');
608 614 var $form = $(tmpl);
609 615
610 616 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
611 617 var curForm = $formPlaceholder.find('form');
612 618 if (curForm){
613 619 curForm.remove();
614 620 }
615 621 $formPlaceholder.append($form);
616 622
617 623 var _form = $($form[0]);
618 624 var commentForm = this.createCommentForm(
619 625 _form, lineNo, placeholderText, true, resolvesCommentId);
620 626 commentForm.initStatusChangeSelector();
621 627
622 628 return commentForm;
623 629 };
624 630
625 631 this.createComment = function(node, resolutionComment) {
626 632 var resolvesCommentId = resolutionComment || null;
627 633 var $node = $(node);
628 634 var $td = $node.closest('td');
629 635 var $form = $td.find('.comment-inline-form');
630 636
631 637 if (!$form.length) {
632 638
633 639 var $filediff = $node.closest('.filediff');
634 640 $filediff.removeClass('hide-comments');
635 641 var f_path = $filediff.attr('data-f-path');
636 642 var lineno = self.getLineNumber(node);
637 643 // create a new HTML from template
638 644 var tmpl = $('#cb-comment-inline-form-template').html();
639 645 tmpl = tmpl.format(f_path, lineno);
640 646 $form = $(tmpl);
641 647
642 648 var $comments = $td.find('.inline-comments');
643 649 if (!$comments.length) {
644 650 $comments = $(
645 651 $('#cb-comments-inline-container-template').html());
646 652 $td.append($comments);
647 653 }
648 654
649 655 $td.find('.cb-comment-add-button').before($form);
650 656
651 657 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
652 658 var _form = $($form[0]).find('form');
653 659
654 660 var commentForm = this.createCommentForm(
655 661 _form, lineno, placeholderText, false, resolvesCommentId);
656 662
657 663 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
658 664 form: _form,
659 665 parent: $td[0],
660 666 lineno: lineno,
661 667 f_path: f_path}
662 668 );
663 669
664 670 // set a CUSTOM submit handler for inline comments.
665 671 commentForm.setHandleFormSubmit(function(o) {
666 672 var text = commentForm.cm.getValue();
667 673 var commentType = commentForm.getCommentType();
668 674 var resolvesCommentId = commentForm.getResolvesId();
669 675
670 676 if (text === "") {
671 677 return;
672 678 }
673 679
674 680 if (lineno === undefined) {
675 681 alert('missing line !');
676 682 return;
677 683 }
678 684 if (f_path === undefined) {
679 685 alert('missing file path !');
680 686 return;
681 687 }
682 688
683 689 var excludeCancelBtn = false;
684 690 var submitEvent = true;
685 691 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
686 692 commentForm.cm.setOption("readOnly", true);
687 693 var postData = {
688 694 'text': text,
689 695 'f_path': f_path,
690 696 'line': lineno,
691 697 'comment_type': commentType,
692 698 'csrf_token': CSRF_TOKEN
693 699 };
694 700 if (resolvesCommentId){
695 701 postData['resolves_comment_id'] = resolvesCommentId;
696 702 }
697 703
698 704 var submitSuccessCallback = function(json_data) {
699 705 $form.remove();
700 706 try {
701 707 var html = json_data.rendered_text;
702 708 var lineno = json_data.line_no;
703 709 var target_id = json_data.target_id;
704 710
705 711 $comments.find('.cb-comment-add-button').before(html);
706 712
707 713 //mark visually which comment was resolved
708 714 if (resolvesCommentId) {
709 715 commentForm.markCommentResolved(resolvesCommentId);
710 716 }
711 717
712 718 // run global callback on submit
713 719 commentForm.globalSubmitSuccessCallback();
714 720
715 721 } catch (e) {
716 722 console.error(e);
717 723 }
718 724
719 725 // re trigger the linkification of next/prev navigation
720 726 linkifyComments($('.inline-comment-injected'));
721 727 timeagoActivate();
722 728 commentForm.setActionButtonsDisabled(false);
723 729
724 730 };
725 731 var submitFailCallback = function(){
726 732 commentForm.resetCommentFormState(text)
727 733 };
728 734 commentForm.submitAjaxPOST(
729 735 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
730 736 });
731 737 }
732 738
733 739 $form.addClass('comment-inline-form-open');
734 740 };
735 741
736 742 this.createResolutionComment = function(commentId){
737 743 // hide the trigger text
738 744 $('#resolve-comment-{0}'.format(commentId)).hide();
739 745
740 746 var comment = $('#comment-'+commentId);
741 747 var commentData = comment.data();
742 748 if (commentData.commentInline) {
743 749 this.createComment(comment, commentId)
744 750 } else {
745 751 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
746 752 }
747 753
748 754 return false;
749 755 };
750 756
751 757 this.submitResolution = function(commentId){
752 758 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
753 759 var commentForm = form.get(0).CommentForm;
754 760
755 761 var cm = commentForm.getCmInstance();
756 762 var renderer = templateContext.visual.default_renderer;
757 763 if (renderer == 'rst'){
758 764 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
759 765 } else if (renderer == 'markdown') {
760 766 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
761 767 } else {
762 768 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
763 769 }
764 770
765 771 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
766 772 form.submit();
767 773 return false;
768 774 };
769 775
770 776 this.renderInlineComments = function(file_comments) {
771 777 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
772 778
773 779 for (var i = 0; i < file_comments.length; i++) {
774 780 var box = file_comments[i];
775 781
776 782 var target_id = $(box).attr('target_id');
777 783
778 784 // actually comments with line numbers
779 785 var comments = box.children;
780 786
781 787 for (var j = 0; j < comments.length; j++) {
782 788 var data = {
783 789 'rendered_text': comments[j].outerHTML,
784 790 'line_no': $(comments[j]).attr('line'),
785 791 'target_id': target_id
786 792 };
787 793 }
788 794 }
789 795
790 796 // since order of injection is random, we're now re-iterating
791 797 // from correct order and filling in links
792 798 linkifyComments($('.inline-comment-injected'));
793 799 firefoxAnchorFix();
794 800 };
795 801
796 802 };
@@ -1,408 +1,385 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 10 % if inline:
11 11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 12 % else:
13 13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 14 % endif
15 15
16 16
17 17 <div class="comment
18 18 ${'comment-inline' if inline else 'comment-general'}
19 19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 20 id="comment-${comment.comment_id}"
21 21 line="${comment.line_no}"
22 22 data-comment-id="${comment.comment_id}"
23 23 data-comment-type="${comment.comment_type}"
24 24 data-comment-inline=${h.json.dumps(inline)}
25 25 style="${'display: none;' if outdated_at_ver else ''}">
26 26
27 27 <div class="meta">
28 28 <div class="comment-type-label">
29 29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 30 % if comment.comment_type == 'todo':
31 31 % if comment.resolved:
32 32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 34 </div>
35 35 % else:
36 36 <div class="resolved tooltip" style="display: none">
37 37 <span>${comment.comment_type}</span>
38 38 </div>
39 39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 40 ${comment.comment_type}
41 41 </div>
42 42 % endif
43 43 % else:
44 44 % if comment.resolved_comment:
45 45 fix
46 46 % else:
47 47 ${comment.comment_type or 'note'}
48 48 % endif
49 49 % endif
50 50 </div>
51 51 </div>
52 52
53 53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 54 ${base.gravatar_with_user(comment.author.email, 16)}
55 55 </div>
56 56 <div class="date">
57 57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 58 </div>
59 59 % if inline:
60 60 <span></span>
61 61 % else:
62 62 <div class="status-change">
63 63 % if comment.pull_request:
64 64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 65 % if comment.status_change:
66 66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 67 % else:
68 68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 69 % endif
70 70 </a>
71 71 % else:
72 72 % if comment.status_change:
73 73 ${_('Status change on commit')}:
74 74 % endif
75 75 % endif
76 76 </div>
77 77 % endif
78 78
79 79 % if comment.status_change:
80 80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 82 ${comment.status_change[0].status_lbl}
83 83 </div>
84 84 % endif
85 85
86 86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
87 87
88 88 <div class="comment-links-block">
89 89
90 90 % if inline:
91 91 <div class="pr-version-inline">
92 92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
93 93 % if outdated_at_ver:
94 94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
95 95 outdated ${'v{}'.format(pr_index_ver)} |
96 96 </code>
97 97 % elif pr_index_ver:
98 98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
99 99 ${'v{}'.format(pr_index_ver)} |
100 100 </code>
101 101 % endif
102 102 </a>
103 103 </div>
104 104 % else:
105 105 % if comment.pull_request_version_id and pr_index_ver:
106 106 |
107 107 <div class="pr-version">
108 108 % if comment.outdated:
109 109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
110 110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
111 111 </a>
112 112 % else:
113 113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
114 114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
115 115 <code class="pr-version-num">
116 116 ${'v{}'.format(pr_index_ver)}
117 117 </code>
118 118 </a>
119 119 </div>
120 120 % endif
121 121 </div>
122 122 % endif
123 123 % endif
124 124
125 125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
126 126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
127 127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
128 128 ## permissions to delete
129 129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
130 130 ## TODO: dan: add edit comment here
131 131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
132 132 %else:
133 133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
134 134 %endif
135 135 %else:
136 136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
137 137 %endif
138 138
139 139 %if not outdated_at_ver:
140 140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
141 141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
142 142 %endif
143 143
144 144 </div>
145 145 </div>
146 146 <div class="text">
147 147 ${comment.render(mentions=True)|n}
148 148 </div>
149 149
150 150 </div>
151 151 </%def>
152 152
153 153 ## generate main comments
154 154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
155 155 <div id="comments">
156 156 %for comment in comments:
157 157 <div id="comment-tr-${comment.comment_id}">
158 158 ## only render comments that are not from pull request, or from
159 159 ## pull request and a status change
160 160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
161 161 ${comment_block(comment)}
162 162 %endif
163 163 </div>
164 164 %endfor
165 165 ## to anchor ajax comments
166 166 <div id="injected_page_comments"></div>
167 167 </div>
168 168 </%def>
169 169
170 170
171 171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
172 172
173 ## merge status, and merge action
174 %if is_pull_request:
175 <div class="pull-request-merge">
176 %if c.allowed_to_merge:
177 <div class="pull-request-wrap">
178 <div class="pull-right">
179 ${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')}
180 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
182 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
183 ${h.end_form()}
184 </div>
185 </div>
186 %else:
187 <div class="pull-request-wrap">
188 <div class="pull-right">
189 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
190 </div>
191 </div>
192 %endif
193 </div>
194 %endif
195
196 173 <div class="comments">
197 174 <%
198 175 if is_pull_request:
199 176 placeholder = _('Leave a comment on this Pull Request.')
200 177 elif is_compare:
201 178 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
202 179 else:
203 180 placeholder = _('Leave a comment on this Commit.')
204 181 %>
205 182
206 183 % if c.rhodecode_user.username != h.DEFAULT_USER:
207 184 <div class="js-template" id="cb-comment-general-form-template">
208 185 ## template generated for injection
209 186 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
210 187 </div>
211 188
212 189 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
213 190 ## inject form here
214 191 </div>
215 192 <script type="text/javascript">
216 193 var lineNo = 'general';
217 194 var resolvesCommentId = null;
218 195 var generalCommentForm = Rhodecode.comments.createGeneralComment(
219 196 lineNo, "${placeholder}", resolvesCommentId);
220 197
221 198 // set custom success callback on rangeCommit
222 199 % if is_compare:
223 200 generalCommentForm.setHandleFormSubmit(function(o) {
224 201 var self = generalCommentForm;
225 202
226 203 var text = self.cm.getValue();
227 204 var status = self.getCommentStatus();
228 205 var commentType = self.getCommentType();
229 206
230 207 if (text === "" && !status) {
231 208 return;
232 209 }
233 210
234 211 // we can pick which commits we want to make the comment by
235 212 // selecting them via click on preview pane, this will alter the hidden inputs
236 213 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
237 214
238 215 var commitIds = [];
239 216 $('#changeset_compare_view_content .compare_select').each(function(el) {
240 217 var commitId = this.id.replace('row-', '');
241 218 if ($(this).hasClass('hl') || !cherryPicked) {
242 219 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
243 220 commitIds.push(commitId);
244 221 } else {
245 222 $("input[data-commit-id='{0}']".format(commitId)).val('')
246 223 }
247 224 });
248 225
249 226 self.setActionButtonsDisabled(true);
250 227 self.cm.setOption("readOnly", true);
251 228 var postData = {
252 229 'text': text,
253 230 'changeset_status': status,
254 231 'comment_type': commentType,
255 232 'commit_ids': commitIds,
256 233 'csrf_token': CSRF_TOKEN
257 234 };
258 235
259 236 var submitSuccessCallback = function(o) {
260 237 location.reload(true);
261 238 };
262 239 var submitFailCallback = function(){
263 240 self.resetCommentFormState(text)
264 241 };
265 242 self.submitAjaxPOST(
266 243 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
267 244 });
268 245 % endif
269 246
270 247
271 248 </script>
272 249 % else:
273 250 ## form state when not logged in
274 251 <div class="comment-form ac">
275 252
276 253 <div class="comment-area">
277 254 <div class="comment-area-header">
278 255 <ul class="nav-links clearfix">
279 256 <li class="active">
280 257 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
281 258 </li>
282 259 <li class="">
283 260 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
284 261 </li>
285 262 </ul>
286 263 </div>
287 264
288 265 <div class="comment-area-write" style="display: block;">
289 266 <div id="edit-container">
290 267 <div style="padding: 40px 0">
291 268 ${_('You need to be logged in to leave comments.')}
292 269 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
293 270 </div>
294 271 </div>
295 272 <div id="preview-container" class="clearfix" style="display: none;">
296 273 <div id="preview-box" class="preview-box"></div>
297 274 </div>
298 275 </div>
299 276
300 277 <div class="comment-area-footer">
301 278 <div class="toolbar">
302 279 <div class="toolbar-text">
303 280 </div>
304 281 </div>
305 282 </div>
306 283 </div>
307 284
308 285 <div class="comment-footer">
309 286 </div>
310 287
311 288 </div>
312 289 % endif
313 290
314 291 <script type="text/javascript">
315 292 bindToggleButtons();
316 293 </script>
317 294 </div>
318 295 </%def>
319 296
320 297
321 298 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
322 299 ## comment injected based on assumption that user is logged in
323 300
324 301 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
325 302
326 303 <div class="comment-area">
327 304 <div class="comment-area-header">
328 305 <ul class="nav-links clearfix">
329 306 <li class="active">
330 307 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
331 308 </li>
332 309 <li class="">
333 310 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
334 311 </li>
335 312 <li class="pull-right">
336 313 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
337 314 % for val in c.visual.comment_types:
338 315 <option value="${val}">${val.upper()}</option>
339 316 % endfor
340 317 </select>
341 318 </li>
342 319 </ul>
343 320 </div>
344 321
345 322 <div class="comment-area-write" style="display: block;">
346 323 <div id="edit-container_${lineno_id}">
347 324 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
348 325 </div>
349 326 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
350 327 <div id="preview-box_${lineno_id}" class="preview-box"></div>
351 328 </div>
352 329 </div>
353 330
354 331 <div class="comment-area-footer">
355 332 <div class="toolbar">
356 333 <div class="toolbar-text">
357 334 ${(_('Comments parsed using %s syntax with %s support.') % (
358 335 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
359 336 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
360 337 )
361 338 )|n}
362 339 </div>
363 340 </div>
364 341 </div>
365 342 </div>
366 343
367 344 <div class="comment-footer">
368 345
369 346 % if review_statuses:
370 347 <div class="status_box">
371 348 <select id="change_status_${lineno_id}" name="changeset_status">
372 349 <option></option> ## Placeholder
373 350 % for status, lbl in review_statuses:
374 351 <option value="${status}" data-status="${status}">${lbl}</option>
375 352 %if is_pull_request and change_status and status in ('approved', 'rejected'):
376 353 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
377 354 %endif
378 355 % endfor
379 356 </select>
380 357 </div>
381 358 % endif
382 359
383 360 ## inject extra inputs into the form
384 361 % if form_extras and isinstance(form_extras, (list, tuple)):
385 362 <div id="comment_form_extras">
386 363 % for form_ex_el in form_extras:
387 364 ${form_ex_el|n}
388 365 % endfor
389 366 </div>
390 367 % endif
391 368
392 369 <div class="action-buttons">
393 370 ## inline for has a file, and line-number together with cancel hide button.
394 371 % if form_type == 'inline':
395 372 <input type="hidden" name="f_path" value="{0}">
396 373 <input type="hidden" name="line" value="${lineno_id}">
397 374 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
398 375 ${_('Cancel')}
399 376 </button>
400 377 % endif
401 378 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
402 379
403 380 </div>
404 381 </div>
405 382
406 383 </form>
407 384
408 385 </%def> No newline at end of file
@@ -1,692 +1,713 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 <span id="pr-title">
12 12 ${c.pull_request.title}
13 13 %if c.pull_request.is_closed():
14 14 (${_('Closed')})
15 15 %endif
16 16 </span>
17 17 <div id="pr-title-edit" class="input" style="display: none;">
18 18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 19 </div>
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='showpullrequest')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31
32 32 <script type="text/javascript">
33 33 // TODO: marcink switch this to pyroutes
34 34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 </script>
37 37 <div class="box">
38 38
39 39 <div class="title">
40 40 ${self.repo_page_title(c.rhodecode_db_repo)}
41 41 </div>
42 42
43 43 ${self.breadcrumbs()}
44 44
45 45 <div class="box pr-summary">
46 46
47 47 <div class="summary-details block-left">
48 48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 49 <div class="pr-details-title">
50 50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 51 %if c.allowed_to_update:
52 52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 53 % if c.allowed_to_delete:
54 54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 57 ${h.end_form()}
58 58 % else:
59 59 ${_('Delete')}
60 60 % endif
61 61 </div>
62 62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 64 %endif
65 65 </div>
66 66
67 67 <div id="summary" class="fields pr-details-content">
68 68 <div class="field">
69 69 <div class="label-summary">
70 70 <label>${_('Origin')}:</label>
71 71 </div>
72 72 <div class="input">
73 73 <div class="pr-origininfo">
74 74 ## branch link is only valid if it is a branch
75 75 <span class="tag">
76 76 %if c.pull_request.source_ref_parts.type == 'branch':
77 77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 78 %else:
79 79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 80 %endif
81 81 </span>
82 82 <span class="clone-url">
83 83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 84 </span>
85 85 </div>
86 86 <div class="pr-pullinfo">
87 87 %if h.is_hg(c.pull_request.source_repo):
88 88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
89 89 %elif h.is_git(c.pull_request.source_repo):
90 90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
91 91 %endif
92 92 </div>
93 93 </div>
94 94 </div>
95 95 <div class="field">
96 96 <div class="label-summary">
97 97 <label>${_('Target')}:</label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-targetinfo">
101 101 ## branch link is only valid if it is a branch
102 102 <span class="tag">
103 103 %if c.pull_request.target_ref_parts.type == 'branch':
104 104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
105 105 %else:
106 106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
107 107 %endif
108 108 </span>
109 109 <span class="clone-url">
110 110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
111 111 </span>
112 112 </div>
113 113 </div>
114 114 </div>
115 115
116 116 ## Link to the shadow repository.
117 117 <div class="field">
118 118 <div class="label-summary">
119 119 <label>${_('Merge')}:</label>
120 120 </div>
121 121 <div class="input">
122 122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
123 123 <div class="pr-mergeinfo">
124 124 %if h.is_hg(c.pull_request.target_repo):
125 125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 126 %elif h.is_git(c.pull_request.target_repo):
127 127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
128 128 %endif
129 129 </div>
130 130 % else:
131 131 <div class="">
132 132 ${_('Shadow repository data not available')}.
133 133 </div>
134 134 % endif
135 135 </div>
136 136 </div>
137 137
138 138 <div class="field">
139 139 <div class="label-summary">
140 140 <label>${_('Review')}:</label>
141 141 </div>
142 142 <div class="input">
143 143 %if c.pull_request_review_status:
144 144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
145 145 <span class="changeset-status-lbl tooltip">
146 146 %if c.pull_request.is_closed():
147 147 ${_('Closed')},
148 148 %endif
149 149 ${h.commit_status_lbl(c.pull_request_review_status)}
150 150 </span>
151 151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
152 152 %endif
153 153 </div>
154 154 </div>
155 155 <div class="field">
156 156 <div class="pr-description-label label-summary">
157 157 <label>${_('Description')}:</label>
158 158 </div>
159 159 <div id="pr-desc" class="input">
160 160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
161 161 </div>
162 162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
163 163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
164 164 </div>
165 165 </div>
166 166
167 167 <div class="field">
168 168 <div class="label-summary">
169 169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
170 170 </div>
171 171
172 172 <div class="pr-versions">
173 173 % if c.show_version_changes:
174 174 <table>
175 175 ## CURRENTLY SELECT PR VERSION
176 176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
177 177 <td>
178 178 % if c.at_version_num is None:
179 179 <i class="icon-ok link"></i>
180 180 % else:
181 181 <i class="icon-comment"></i>
182 182 <code>
183 183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
184 184 </code>
185 185 % endif
186 186 </td>
187 187 <td>
188 188 <code>
189 189 % if c.versions:
190 190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
191 191 % else:
192 192 ${_('initial')}
193 193 % endif
194 194 </code>
195 195 </td>
196 196 <td>
197 197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
198 198 </td>
199 199 <td>
200 200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
201 201 </td>
202 202 <td align="right">
203 203 % if c.versions and c.at_version_num in [None, 'latest']:
204 204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
205 205 % endif
206 206 </td>
207 207 </tr>
208 208
209 209 ## SHOW ALL VERSIONS OF PR
210 210 <% ver_pr = None %>
211 211
212 212 % for data in reversed(list(enumerate(c.versions, 1))):
213 213 <% ver_pos = data[0] %>
214 214 <% ver = data[1] %>
215 215 <% ver_pr = ver.pull_request_version_id %>
216 216
217 217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
218 218 <td>
219 219 % if c.at_version_num == ver_pr:
220 220 <i class="icon-ok link"></i>
221 221 % else:
222 222 <i class="icon-comment"></i>
223 223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
225 225 </code>
226 226 % endif
227 227 </td>
228 228 <td>
229 229 <code>
230 230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
231 231 </code>
232 232 </td>
233 233 <td>
234 234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
235 235 </td>
236 236 <td>
237 237 ${_('created')} ${h.age_component(ver.updated_on)}
238 238 </td>
239 239 <td align="right">
240 240 % if c.at_version_num == ver_pr:
241 241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
242 242 % endif
243 243 </td>
244 244 </tr>
245 245 % endfor
246 246
247 247 ## show comment/inline comments summary
248 248 <tr>
249 249 <td>
250 250 </td>
251 251
252 252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
253 253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
254 254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
255 255
256 256
257 257 % if c.at_version:
258 258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
259 259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
260 260 ${_('Comments at this version')}:
261 261 % else:
262 262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
263 263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
264 264 ${_('Comments for this pull request')}:
265 265 % endif
266 266
267 267 %if general_comm_count_ver:
268 268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
269 269 %else:
270 270 ${_("%d General ") % general_comm_count_ver}
271 271 %endif
272 272
273 273 %if inline_comm_count_ver:
274 274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
275 275 %else:
276 276 , ${_("%d Inline") % inline_comm_count_ver}
277 277 %endif
278 278
279 279 %if outdated_comm_count_ver:
280 280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
281 281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
282 282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
283 283 %else:
284 284 , ${_("%d Outdated") % outdated_comm_count_ver}
285 285 %endif
286 286 </td>
287 287 </tr>
288 288
289 289 <tr>
290 290 <td></td>
291 291 <td colspan="4">
292 292 % if c.at_version:
293 293 <pre>
294 294 Changed commits:
295 295 * added: ${len(c.changes.added)}
296 296 * removed: ${len(c.changes.removed)}
297 297
298 298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
299 299 No file changes found
300 300 % else:
301 301 Changed files:
302 302 %for file_name in c.file_changes.added:
303 303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
304 304 %endfor
305 305 %for file_name in c.file_changes.modified:
306 306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
307 307 %endfor
308 308 %for file_name in c.file_changes.removed:
309 309 * R ${file_name}
310 310 %endfor
311 311 % endif
312 312 </pre>
313 313 % endif
314 314 </td>
315 315 </tr>
316 316 </table>
317 317 % else:
318 318 ${_('Pull request versions not available')}.
319 319 % endif
320 320 </div>
321 321 </div>
322 322
323 323 <div id="pr-save" class="field" style="display: none;">
324 324 <div class="label-summary"></div>
325 325 <div class="input">
326 326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
327 327 </div>
328 328 </div>
329 329 </div>
330 330 </div>
331 331 <div>
332 332 ## AUTHOR
333 333 <div class="reviewers-title block-right">
334 334 <div class="pr-details-title">
335 335 ${_('Author')}
336 336 </div>
337 337 </div>
338 338 <div class="block-right pr-details-content reviewers">
339 339 <ul class="group_members">
340 340 <li>
341 341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
342 342 </li>
343 343 </ul>
344 344 </div>
345 345 ## REVIEWERS
346 346 <div class="reviewers-title block-right">
347 347 <div class="pr-details-title">
348 348 ${_('Pull request reviewers')}
349 349 %if c.allowed_to_update:
350 350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
351 351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
352 352 %endif
353 353 </div>
354 354 </div>
355 355 <div id="reviewers" class="block-right pr-details-content reviewers">
356 356 ## members goes here !
357 357 <input type="hidden" name="__start__" value="review_members:sequence">
358 358 <ul id="review_members" class="group_members">
359 359 %for member,reasons,status in c.pull_request_reviewers:
360 360 <li id="reviewer_${member.user_id}">
361 361 <div class="reviewers_member">
362 362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
363 363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
364 364 </div>
365 365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
366 366 ${self.gravatar_with_user(member.email, 16)}
367 367 </div>
368 368 <input type="hidden" name="__start__" value="reviewer:mapping">
369 369 <input type="hidden" name="__start__" value="reasons:sequence">
370 370 %for reason in reasons:
371 371 <div class="reviewer_reason">- ${reason}</div>
372 372 <input type="hidden" name="reason" value="${reason}">
373 373
374 374 %endfor
375 375 <input type="hidden" name="__end__" value="reasons:sequence">
376 376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
377 377 <input type="hidden" name="__end__" value="reviewer:mapping">
378 378 %if c.allowed_to_update:
379 379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
380 380 <i class="icon-remove-sign" ></i>
381 381 </div>
382 382 %endif
383 383 </div>
384 384 </li>
385 385 %endfor
386 386 </ul>
387 387 <input type="hidden" name="__end__" value="review_members:sequence">
388 388 %if not c.pull_request.is_closed():
389 389 <div id="add_reviewer_input" class='ac' style="display: none;">
390 390 %if c.allowed_to_update:
391 391 <div class="reviewer_ac">
392 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
393 393 <div id="reviewers_container"></div>
394 394 </div>
395 395 <div>
396 396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
397 397 </div>
398 398 %endif
399 399 </div>
400 400 %endif
401 401 </div>
402 402 </div>
403 403 </div>
404 404 <div class="box">
405 405 ##DIFF
406 406 <div class="table" >
407 407 <div id="changeset_compare_view_content">
408 408 ##CS
409 409 % if c.missing_requirements:
410 410 <div class="box">
411 411 <div class="alert alert-warning">
412 412 <div>
413 413 <strong>${_('Missing requirements:')}</strong>
414 414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 415 </div>
416 416 </div>
417 417 </div>
418 418 % elif c.missing_commits:
419 419 <div class="box">
420 420 <div class="alert alert-warning">
421 421 <div>
422 422 <strong>${_('Missing commits')}:</strong>
423 423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 425 </div>
426 426 </div>
427 427 </div>
428 428 % endif
429 429 <div class="compare_view_commits_title">
430 430
431 431 <div class="pull-left">
432 432 <div class="btn-group">
433 433 <a
434 434 class="btn"
435 435 href="#"
436 436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
437 437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
438 438 </a>
439 439 <a
440 440 class="btn"
441 441 href="#"
442 442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
443 443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
444 444 </a>
445 445 </div>
446 446 </div>
447 447
448 448 <div class="pull-right">
449 449 % if c.allowed_to_update and not c.pull_request.is_closed():
450 450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
451 451 % else:
452 452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
453 453 % endif
454 454
455 455 </div>
456 456
457 457 </div>
458 458
459 459 % if not c.missing_commits:
460 460 <%include file="/compare/compare_commits.mako" />
461 461 <div class="cs_files">
462 462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
463 463 ${cbdiffs.render_diffset_menu()}
464 464 ${cbdiffs.render_diffset(
465 465 c.diffset, use_comments=True,
466 466 collapse_when_files_over=30,
467 467 disable_new_comments=not c.allowed_to_comment,
468 468 deleted_files_comments=c.deleted_files_comments)}
469 469 </div>
470 470 % else:
471 471 ## skipping commits we need to clear the view for missing commits
472 472 <div style="clear:both;"></div>
473 473 % endif
474 474
475 475 </div>
476 476 </div>
477 477
478 478 ## template for inline comment form
479 479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
480 480
481 481 ## render general comments
482 482
483 483 <div id="comment-tr-show">
484 484 <div class="comment">
485 % if general_outdated_comm_count_ver:
485 486 <div class="meta">
486 % if general_outdated_comm_count_ver:
487 487 % if general_outdated_comm_count_ver == 1:
488 488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 490 % else:
491 491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
493 493 % endif
494 </div>
494 495 % endif
495 496 </div>
496 497 </div>
497 </div>
498 498
499 499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
500 500
501 501 % if not c.pull_request.is_closed():
502 ## merge status, and merge action
503 <div class="pull-request-merge">
504 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
505 </div>
506
502 507 ## main comment form and it status
503 508 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
504 509 pull_request_id=c.pull_request.pull_request_id),
505 510 c.pull_request_review_status,
506 511 is_pull_request=True, change_status=c.allowed_to_change_status)}
507 512 %endif
508 513
509 514 <script type="text/javascript">
510 515 if (location.hash) {
511 516 var result = splitDelimitedHash(location.hash);
512 517 var line = $('html').find(result.loc);
513 518 // show hidden comments if we use location.hash
514 519 if (line.hasClass('comment-general')) {
515 520 $(line).show();
516 521 } else if (line.hasClass('comment-inline')) {
517 522 $(line).show();
518 523 var $cb = $(line).closest('.cb');
519 524 $cb.removeClass('cb-collapsed')
520 525 }
521 526 if (line.length > 0){
522 527 offsetScroll(line, 70);
523 528 }
524 529 }
525 530
526 531 $(function(){
527 532 ReviewerAutoComplete('user');
528 533 // custom code mirror
529 534 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
530 535
531 536 var PRDetails = {
532 537 editButton: $('#open_edit_pullrequest'),
533 538 closeButton: $('#close_edit_pullrequest'),
534 539 deleteButton: $('#delete_pullrequest'),
535 540 viewFields: $('#pr-desc, #pr-title'),
536 541 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
537 542
538 543 init: function() {
539 544 var that = this;
540 545 this.editButton.on('click', function(e) { that.edit(); });
541 546 this.closeButton.on('click', function(e) { that.view(); });
542 547 },
543 548
544 549 edit: function(event) {
545 550 this.viewFields.hide();
546 551 this.editButton.hide();
547 552 this.deleteButton.hide();
548 553 this.closeButton.show();
549 554 this.editFields.show();
550 555 codeMirrorInstance.refresh();
551 556 },
552 557
553 558 view: function(event) {
554 559 this.editButton.show();
555 560 this.deleteButton.show();
556 561 this.editFields.hide();
557 562 this.closeButton.hide();
558 563 this.viewFields.show();
559 564 }
560 565 };
561 566
562 567 var ReviewersPanel = {
563 568 editButton: $('#open_edit_reviewers'),
564 569 closeButton: $('#close_edit_reviewers'),
565 570 addButton: $('#add_reviewer_input'),
566 571 removeButtons: $('.reviewer_member_remove'),
567 572
568 573 init: function() {
569 574 var that = this;
570 575 this.editButton.on('click', function(e) { that.edit(); });
571 576 this.closeButton.on('click', function(e) { that.close(); });
572 577 },
573 578
574 579 edit: function(event) {
575 580 this.editButton.hide();
576 581 this.closeButton.show();
577 582 this.addButton.show();
578 583 this.removeButtons.css('visibility', 'visible');
579 584 },
580 585
581 586 close: function(event) {
582 587 this.editButton.show();
583 588 this.closeButton.hide();
584 589 this.addButton.hide();
585 590 this.removeButtons.css('visibility', 'hidden');
586 591 }
587 592 };
588 593
589 594 PRDetails.init();
590 595 ReviewersPanel.init();
591 596
592 597 showOutdated = function(self){
593 598 $('.comment-inline.comment-outdated').show();
594 599 $('.filediff-outdated').show();
595 600 $('.showOutdatedComments').hide();
596 601 $('.hideOutdatedComments').show();
597 602 };
598 603
599 604 hideOutdated = function(self){
600 605 $('.comment-inline.comment-outdated').hide();
601 606 $('.filediff-outdated').hide();
602 607 $('.hideOutdatedComments').hide();
603 608 $('.showOutdatedComments').show();
604 609 };
605 610
611 refreshMergeChecks = function(){
612 var loadUrl = "${h.url.current(merge_checks=1)}";
613 $('.pull-request-merge').css('opacity', 0.3);
614 $('.pull-request-merge').load(
615 loadUrl,function() {
616 $('.pull-request-merge').css('opacity', 1);
617 }
618 );
619 };
620
606 621 $('#show-outdated-comments').on('click', function(e){
607 622 var button = $(this);
608 623 var outdated = $('.comment-outdated');
609 624
610 625 if (button.html() === "(Show)") {
611 626 button.html("(Hide)");
612 627 outdated.show();
613 628 } else {
614 629 button.html("(Show)");
615 630 outdated.hide();
616 631 }
617 632 });
618 633
619 634 $('.show-inline-comments').on('change', function(e){
620 635 var show = 'none';
621 636 var target = e.currentTarget;
622 637 if(target.checked){
623 638 show = ''
624 639 }
625 640 var boxid = $(target).attr('id_for');
626 641 var comments = $('#{0} .inline-comments'.format(boxid));
627 642 var fn_display = function(idx){
628 643 $(this).css('display', show);
629 644 };
630 645 $(comments).each(fn_display);
631 646 var btns = $('#{0} .inline-comments-button'.format(boxid));
632 647 $(btns).each(fn_display);
633 648 });
634 649
635 650 $('#merge_pull_request_form').submit(function() {
636 651 if (!$('#merge_pull_request').attr('disabled')) {
637 652 $('#merge_pull_request').attr('disabled', 'disabled');
638 653 }
639 654 return true;
640 655 });
641 656
642 657 $('#edit_pull_request').on('click', function(e){
643 658 var title = $('#pr-title-input').val();
644 659 var description = codeMirrorInstance.getValue();
645 660 editPullRequest(
646 661 "${c.repo_name}", "${c.pull_request.pull_request_id}",
647 662 title, description);
648 663 });
649 664
650 665 $('#update_pull_request').on('click', function(e){
651 666 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
652 667 });
653 668
654 669 $('#update_commits').on('click', function(e){
655 670 var isDisabled = !$(e.currentTarget).attr('disabled');
656 671 $(e.currentTarget).text(_gettext('Updating...'));
657 672 $(e.currentTarget).attr('disabled', 'disabled');
658 673 if(isDisabled){
659 674 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
660 675 }
661 676
662 677 });
663 678 // fixing issue with caches on firefox
664 679 $('#update_commits').removeAttr("disabled");
665 680
666 681 $('#close_pull_request').on('click', function(e){
667 682 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
668 683 });
669 684
670 685 $('.show-inline-comments').on('click', function(e){
671 686 var boxid = $(this).attr('data-comment-id');
672 687 var button = $(this);
673 688
674 689 if(button.hasClass("comments-visible")) {
675 690 $('#{0} .inline-comments'.format(boxid)).each(function(index){
676 691 $(this).hide();
677 692 });
678 693 button.removeClass("comments-visible");
679 694 } else {
680 695 $('#{0} .inline-comments'.format(boxid)).each(function(index){
681 696 $(this).show();
682 697 });
683 698 button.addClass("comments-visible");
684 699 }
685 700 });
701
702 // register submit callback on commentForm form to track TODOs
703 window.commentFormGlobalSubmitSuccessCallback = function(){
704 refreshMergeChecks();
705 };
706
686 707 })
687 708 </script>
688 709
689 710 </div>
690 711 </div>
691 712
692 713 </%def>
General Comments 0
You need to be logged in to leave comments. Login now