##// END OF EJS Templates
comments: re-implemented diff and comments/todos in pull-requests.
marcink -
r3884:761273d6 default
parent child Browse files
Show More
@@ -1,1464 +1,1470 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 # backward compat., we use for OLD PRs a plain renderer
64 64 c.renderer = 'plain'
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 115 'title': _render('pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'state': pr.pull_request_state,
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff, hide_whitespace_changes, diff_context):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id,
218 218 hide_whitespace_changes, diff_context)
219 219
220 220 diff_processor = diffs.DiffProcessor(
221 221 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 222 file_limit=file_limit, show_full_diff=fulldiff)
223 223
224 224 _parsed = diff_processor.prepare()
225 225
226 226 diffset = codeblocks.DiffSet(
227 227 repo_name=self.db_repo_name,
228 228 source_repo_name=source_repo_name,
229 229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 231 )
232 232 diffset = self.path_filter.render_patchset_filtered(
233 233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 234
235 235 return diffset
236 236
237 237 def _get_range_diffset(self, source_scm, source_repo,
238 238 commit1, commit2, diff_limit, file_limit,
239 239 fulldiff, hide_whitespace_changes, diff_context):
240 240 vcs_diff = source_scm.get_diff(
241 241 commit1, commit2,
242 242 ignore_whitespace=hide_whitespace_changes,
243 243 context=diff_context)
244 244
245 245 diff_processor = diffs.DiffProcessor(
246 246 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 247 file_limit=file_limit, show_full_diff=fulldiff)
248 248
249 249 _parsed = diff_processor.prepare()
250 250
251 251 diffset = codeblocks.DiffSet(
252 252 repo_name=source_repo.repo_name,
253 253 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 254 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 255
256 256 diffset = self.path_filter.render_patchset_filtered(
257 257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 _ = self.request.translate
269 269 c = self.load_default_context()
270 270
271 271 pull_request = PullRequest.get_or_404(
272 272 self.request.matchdict['pull_request_id'])
273 273 pull_request_id = pull_request.pull_request_id
274 274
275 275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 276 log.debug('show: forbidden because pull request is in state %s',
277 277 pull_request.pull_request_state)
278 278 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 280 pull_request.pull_request_state)
281 281 h.flash(msg, category='error')
282 282 raise HTTPFound(h.route_path('pullrequest_show_all',
283 283 repo_name=self.db_repo_name))
284 284
285 285 version = self.request.GET.get('version')
286 286 from_version = self.request.GET.get('from_version') or version
287 287 merge_checks = self.request.GET.get('merge_checks')
288 288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
289 289
290 290 # fetch global flags of ignore ws or context lines
291 291 diff_context = diffs.get_diff_context(self.request)
292 292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
293 293
294 294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
295 295
296 296 (pull_request_latest,
297 297 pull_request_at_ver,
298 298 pull_request_display_obj,
299 299 at_version) = PullRequestModel().get_pr_version(
300 300 pull_request_id, version=version)
301 301 pr_closed = pull_request_latest.is_closed()
302 302
303 303 if pr_closed and (version or from_version):
304 304 # not allow to browse versions
305 305 raise HTTPFound(h.route_path(
306 306 'pullrequest_show', repo_name=self.db_repo_name,
307 307 pull_request_id=pull_request_id))
308 308
309 309 versions = pull_request_display_obj.versions()
310 310 # used to store per-commit range diffs
311 311 c.changes = collections.OrderedDict()
312 312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
313 313
314 314 c.at_version = at_version
315 315 c.at_version_num = (at_version
316 316 if at_version and at_version != 'latest'
317 317 else None)
318 318 c.at_version_pos = ChangesetComment.get_index_from_version(
319 319 c.at_version_num, versions)
320 320
321 321 (prev_pull_request_latest,
322 322 prev_pull_request_at_ver,
323 323 prev_pull_request_display_obj,
324 324 prev_at_version) = PullRequestModel().get_pr_version(
325 325 pull_request_id, version=from_version)
326 326
327 327 c.from_version = prev_at_version
328 328 c.from_version_num = (prev_at_version
329 329 if prev_at_version and prev_at_version != 'latest'
330 330 else None)
331 331 c.from_version_pos = ChangesetComment.get_index_from_version(
332 332 c.from_version_num, versions)
333 333
334 334 # define if we're in COMPARE mode or VIEW at version mode
335 335 compare = at_version != prev_at_version
336 336
337 337 # pull_requests repo_name we opened it against
338 338 # ie. target_repo must match
339 339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
340 340 raise HTTPNotFound()
341 341
342 342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
343 343 pull_request_at_ver)
344 344
345 345 c.pull_request = pull_request_display_obj
346 346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
347 347 c.pull_request_latest = pull_request_latest
348 348
349 349 if compare or (at_version and not at_version == 'latest'):
350 350 c.allowed_to_change_status = False
351 351 c.allowed_to_update = False
352 352 c.allowed_to_merge = False
353 353 c.allowed_to_delete = False
354 354 c.allowed_to_comment = False
355 355 c.allowed_to_close = False
356 356 else:
357 357 can_change_status = PullRequestModel().check_user_change_status(
358 358 pull_request_at_ver, self._rhodecode_user)
359 359 c.allowed_to_change_status = can_change_status and not pr_closed
360 360
361 361 c.allowed_to_update = PullRequestModel().check_user_update(
362 362 pull_request_latest, self._rhodecode_user) and not pr_closed
363 363 c.allowed_to_merge = PullRequestModel().check_user_merge(
364 364 pull_request_latest, self._rhodecode_user) and not pr_closed
365 365 c.allowed_to_delete = PullRequestModel().check_user_delete(
366 366 pull_request_latest, self._rhodecode_user) and not pr_closed
367 367 c.allowed_to_comment = not pr_closed
368 368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
369 369
370 370 c.forbid_adding_reviewers = False
371 371 c.forbid_author_to_review = False
372 372 c.forbid_commit_author_to_review = False
373 373
374 374 if pull_request_latest.reviewer_data and \
375 375 'rules' in pull_request_latest.reviewer_data:
376 376 rules = pull_request_latest.reviewer_data['rules'] or {}
377 377 try:
378 378 c.forbid_adding_reviewers = rules.get(
379 379 'forbid_adding_reviewers')
380 380 c.forbid_author_to_review = rules.get(
381 381 'forbid_author_to_review')
382 382 c.forbid_commit_author_to_review = rules.get(
383 383 'forbid_commit_author_to_review')
384 384 except Exception:
385 385 pass
386 386
387 387 # check merge capabilities
388 388 _merge_check = MergeCheck.validate(
389 389 pull_request_latest, auth_user=self._rhodecode_user,
390 390 translator=self.request.translate,
391 391 force_shadow_repo_refresh=force_refresh)
392 392 c.pr_merge_errors = _merge_check.error_details
393 393 c.pr_merge_possible = not _merge_check.failed
394 394 c.pr_merge_message = _merge_check.merge_msg
395 395
396 396 c.pr_merge_info = MergeCheck.get_merge_conditions(
397 397 pull_request_latest, translator=self.request.translate)
398 398
399 399 c.pull_request_review_status = _merge_check.review_status
400 400 if merge_checks:
401 401 self.request.override_renderer = \
402 402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
403 403 return self._get_template_context(c)
404 404
405 405 comments_model = CommentsModel()
406 406
407 407 # reviewers and statuses
408 408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
409 409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
410 410
411 411 # GENERAL COMMENTS with versions #
412 412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
413 413 q = q.order_by(ChangesetComment.comment_id.asc())
414 414 general_comments = q
415 415
416 416 # pick comments we want to render at current version
417 417 c.comment_versions = comments_model.aggregate_comments(
418 418 general_comments, versions, c.at_version_num)
419 419 c.comments = c.comment_versions[c.at_version_num]['until']
420 420
421 421 # INLINE COMMENTS with versions #
422 422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
423 423 q = q.order_by(ChangesetComment.comment_id.asc())
424 424 inline_comments = q
425 425
426 426 c.inline_versions = comments_model.aggregate_comments(
427 427 inline_comments, versions, c.at_version_num, inline=True)
428 428
429 # TODOs
430 c.unresolved_comments = CommentsModel() \
431 .get_pull_request_unresolved_todos(pull_request)
432 c.resolved_comments = CommentsModel() \
433 .get_pull_request_resolved_todos(pull_request)
434
429 435 # inject latest version
430 436 latest_ver = PullRequest.get_pr_display_object(
431 437 pull_request_latest, pull_request_latest)
432 438
433 439 c.versions = versions + [latest_ver]
434 440
435 441 # if we use version, then do not show later comments
436 442 # than current version
437 443 display_inline_comments = collections.defaultdict(
438 444 lambda: collections.defaultdict(list))
439 445 for co in inline_comments:
440 446 if c.at_version_num:
441 447 # pick comments that are at least UPTO given version, so we
442 448 # don't render comments for higher version
443 449 should_render = co.pull_request_version_id and \
444 450 co.pull_request_version_id <= c.at_version_num
445 451 else:
446 452 # showing all, for 'latest'
447 453 should_render = True
448 454
449 455 if should_render:
450 456 display_inline_comments[co.f_path][co.line_no].append(co)
451 457
452 458 # load diff data into template context, if we use compare mode then
453 459 # diff is calculated based on changes between versions of PR
454 460
455 461 source_repo = pull_request_at_ver.source_repo
456 462 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
457 463
458 464 target_repo = pull_request_at_ver.target_repo
459 465 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
460 466
461 467 if compare:
462 468 # in compare switch the diff base to latest commit from prev version
463 469 target_ref_id = prev_pull_request_display_obj.revisions[0]
464 470
465 471 # despite opening commits for bookmarks/branches/tags, we always
466 472 # convert this to rev to prevent changes after bookmark or branch change
467 473 c.source_ref_type = 'rev'
468 474 c.source_ref = source_ref_id
469 475
470 476 c.target_ref_type = 'rev'
471 477 c.target_ref = target_ref_id
472 478
473 479 c.source_repo = source_repo
474 480 c.target_repo = target_repo
475 481
476 482 c.commit_ranges = []
477 483 source_commit = EmptyCommit()
478 484 target_commit = EmptyCommit()
479 485 c.missing_requirements = False
480 486
481 487 source_scm = source_repo.scm_instance()
482 488 target_scm = target_repo.scm_instance()
483 489
484 490 shadow_scm = None
485 491 try:
486 492 shadow_scm = pull_request_latest.get_shadow_repo()
487 493 except Exception:
488 494 log.debug('Failed to get shadow repo', exc_info=True)
489 495 # try first the existing source_repo, and then shadow
490 496 # repo if we can obtain one
491 497 commits_source_repo = source_scm or shadow_scm
492 498
493 499 c.commits_source_repo = commits_source_repo
494 500 c.ancestor = None # set it to None, to hide it from PR view
495 501
496 502 # empty version means latest, so we keep this to prevent
497 503 # double caching
498 504 version_normalized = version or 'latest'
499 505 from_version_normalized = from_version or 'latest'
500 506
501 507 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
502 508 cache_file_path = diff_cache_exist(
503 509 cache_path, 'pull_request', pull_request_id, version_normalized,
504 510 from_version_normalized, source_ref_id, target_ref_id,
505 511 hide_whitespace_changes, diff_context, c.fulldiff)
506 512
507 513 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
508 514 force_recache = self.get_recache_flag()
509 515
510 516 cached_diff = None
511 517 if caching_enabled:
512 518 cached_diff = load_cached_diff(cache_file_path)
513 519
514 520 has_proper_commit_cache = (
515 521 cached_diff and cached_diff.get('commits')
516 522 and len(cached_diff.get('commits', [])) == 5
517 523 and cached_diff.get('commits')[0]
518 524 and cached_diff.get('commits')[3])
519 525
520 526 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
521 527 diff_commit_cache = \
522 528 (ancestor_commit, commit_cache, missing_requirements,
523 529 source_commit, target_commit) = cached_diff['commits']
524 530 else:
525 531 diff_commit_cache = \
526 532 (ancestor_commit, commit_cache, missing_requirements,
527 533 source_commit, target_commit) = self.get_commits(
528 534 commits_source_repo,
529 535 pull_request_at_ver,
530 536 source_commit,
531 537 source_ref_id,
532 538 source_scm,
533 539 target_commit,
534 540 target_ref_id,
535 541 target_scm)
536 542
537 543 # register our commit range
538 544 for comm in commit_cache.values():
539 545 c.commit_ranges.append(comm)
540 546
541 547 c.missing_requirements = missing_requirements
542 548 c.ancestor_commit = ancestor_commit
543 549 c.statuses = source_repo.statuses(
544 550 [x.raw_id for x in c.commit_ranges])
545 551
546 552 # auto collapse if we have more than limit
547 553 collapse_limit = diffs.DiffProcessor._collapse_commits_over
548 554 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
549 555 c.compare_mode = compare
550 556
551 557 # diff_limit is the old behavior, will cut off the whole diff
552 558 # if the limit is applied otherwise will just hide the
553 559 # big files from the front-end
554 560 diff_limit = c.visual.cut_off_limit_diff
555 561 file_limit = c.visual.cut_off_limit_file
556 562
557 563 c.missing_commits = False
558 564 if (c.missing_requirements
559 565 or isinstance(source_commit, EmptyCommit)
560 566 or source_commit == target_commit):
561 567
562 568 c.missing_commits = True
563 569 else:
564 570 c.inline_comments = display_inline_comments
565 571
566 572 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
567 573 if not force_recache and has_proper_diff_cache:
568 574 c.diffset = cached_diff['diff']
569 575 (ancestor_commit, commit_cache, missing_requirements,
570 576 source_commit, target_commit) = cached_diff['commits']
571 577 else:
572 578 c.diffset = self._get_diffset(
573 579 c.source_repo.repo_name, commits_source_repo,
574 580 source_ref_id, target_ref_id,
575 581 target_commit, source_commit,
576 582 diff_limit, file_limit, c.fulldiff,
577 583 hide_whitespace_changes, diff_context)
578 584
579 585 # save cached diff
580 586 if caching_enabled:
581 587 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
582 588
583 589 c.limited_diff = c.diffset.limited_diff
584 590
585 591 # calculate removed files that are bound to comments
586 592 comment_deleted_files = [
587 593 fname for fname in display_inline_comments
588 594 if fname not in c.diffset.file_stats]
589 595
590 596 c.deleted_files_comments = collections.defaultdict(dict)
591 597 for fname, per_line_comments in display_inline_comments.items():
592 598 if fname in comment_deleted_files:
593 599 c.deleted_files_comments[fname]['stats'] = 0
594 600 c.deleted_files_comments[fname]['comments'] = list()
595 601 for lno, comments in per_line_comments.items():
596 602 c.deleted_files_comments[fname]['comments'].extend(comments)
597 603
598 604 # maybe calculate the range diff
599 605 if c.range_diff_on:
600 606 # TODO(marcink): set whitespace/context
601 607 context_lcl = 3
602 608 ign_whitespace_lcl = False
603 609
604 610 for commit in c.commit_ranges:
605 611 commit2 = commit
606 612 commit1 = commit.first_parent
607 613
608 614 range_diff_cache_file_path = diff_cache_exist(
609 615 cache_path, 'diff', commit.raw_id,
610 616 ign_whitespace_lcl, context_lcl, c.fulldiff)
611 617
612 618 cached_diff = None
613 619 if caching_enabled:
614 620 cached_diff = load_cached_diff(range_diff_cache_file_path)
615 621
616 622 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
617 623 if not force_recache and has_proper_diff_cache:
618 624 diffset = cached_diff['diff']
619 625 else:
620 626 diffset = self._get_range_diffset(
621 627 source_scm, source_repo,
622 628 commit1, commit2, diff_limit, file_limit,
623 629 c.fulldiff, ign_whitespace_lcl, context_lcl
624 630 )
625 631
626 632 # save cached diff
627 633 if caching_enabled:
628 634 cache_diff(range_diff_cache_file_path, diffset, None)
629 635
630 636 c.changes[commit.raw_id] = diffset
631 637
632 638 # this is a hack to properly display links, when creating PR, the
633 639 # compare view and others uses different notation, and
634 640 # compare_commits.mako renders links based on the target_repo.
635 641 # We need to swap that here to generate it properly on the html side
636 642 c.target_repo = c.source_repo
637 643
638 644 c.commit_statuses = ChangesetStatus.STATUSES
639 645
640 646 c.show_version_changes = not pr_closed
641 647 if c.show_version_changes:
642 648 cur_obj = pull_request_at_ver
643 649 prev_obj = prev_pull_request_at_ver
644 650
645 651 old_commit_ids = prev_obj.revisions
646 652 new_commit_ids = cur_obj.revisions
647 653 commit_changes = PullRequestModel()._calculate_commit_id_changes(
648 654 old_commit_ids, new_commit_ids)
649 655 c.commit_changes_summary = commit_changes
650 656
651 657 # calculate the diff for commits between versions
652 658 c.commit_changes = []
653 659 mark = lambda cs, fw: list(
654 660 h.itertools.izip_longest([], cs, fillvalue=fw))
655 661 for c_type, raw_id in mark(commit_changes.added, 'a') \
656 662 + mark(commit_changes.removed, 'r') \
657 663 + mark(commit_changes.common, 'c'):
658 664
659 665 if raw_id in commit_cache:
660 666 commit = commit_cache[raw_id]
661 667 else:
662 668 try:
663 669 commit = commits_source_repo.get_commit(raw_id)
664 670 except CommitDoesNotExistError:
665 671 # in case we fail extracting still use "dummy" commit
666 672 # for display in commit diff
667 673 commit = h.AttributeDict(
668 674 {'raw_id': raw_id,
669 675 'message': 'EMPTY or MISSING COMMIT'})
670 676 c.commit_changes.append([c_type, commit])
671 677
672 678 # current user review statuses for each version
673 679 c.review_versions = {}
674 680 if self._rhodecode_user.user_id in allowed_reviewers:
675 681 for co in general_comments:
676 682 if co.author.user_id == self._rhodecode_user.user_id:
677 683 status = co.status_change
678 684 if status:
679 685 _ver_pr = status[0].comment.pull_request_version_id
680 686 c.review_versions[_ver_pr] = status[0]
681 687
682 688 return self._get_template_context(c)
683 689
684 690 def get_commits(
685 691 self, commits_source_repo, pull_request_at_ver, source_commit,
686 692 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
687 693 commit_cache = collections.OrderedDict()
688 694 missing_requirements = False
689 695 try:
690 696 pre_load = ["author", "date", "message", "branch", "parents"]
691 697 show_revs = pull_request_at_ver.revisions
692 698 for rev in show_revs:
693 699 comm = commits_source_repo.get_commit(
694 700 commit_id=rev, pre_load=pre_load)
695 701 commit_cache[comm.raw_id] = comm
696 702
697 703 # Order here matters, we first need to get target, and then
698 704 # the source
699 705 target_commit = commits_source_repo.get_commit(
700 706 commit_id=safe_str(target_ref_id))
701 707
702 708 source_commit = commits_source_repo.get_commit(
703 709 commit_id=safe_str(source_ref_id))
704 710 except CommitDoesNotExistError:
705 711 log.warning(
706 712 'Failed to get commit from `{}` repo'.format(
707 713 commits_source_repo), exc_info=True)
708 714 except RepositoryRequirementError:
709 715 log.warning(
710 716 'Failed to get all required data from repo', exc_info=True)
711 717 missing_requirements = True
712 718 ancestor_commit = None
713 719 try:
714 720 ancestor_id = source_scm.get_common_ancestor(
715 721 source_commit.raw_id, target_commit.raw_id, target_scm)
716 722 ancestor_commit = source_scm.get_commit(ancestor_id)
717 723 except Exception:
718 724 ancestor_commit = None
719 725 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
720 726
721 727 def assure_not_empty_repo(self):
722 728 _ = self.request.translate
723 729
724 730 try:
725 731 self.db_repo.scm_instance().get_commit()
726 732 except EmptyRepositoryError:
727 733 h.flash(h.literal(_('There are no commits yet')),
728 734 category='warning')
729 735 raise HTTPFound(
730 736 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
731 737
732 738 @LoginRequired()
733 739 @NotAnonymous()
734 740 @HasRepoPermissionAnyDecorator(
735 741 'repository.read', 'repository.write', 'repository.admin')
736 742 @view_config(
737 743 route_name='pullrequest_new', request_method='GET',
738 744 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
739 745 def pull_request_new(self):
740 746 _ = self.request.translate
741 747 c = self.load_default_context()
742 748
743 749 self.assure_not_empty_repo()
744 750 source_repo = self.db_repo
745 751
746 752 commit_id = self.request.GET.get('commit')
747 753 branch_ref = self.request.GET.get('branch')
748 754 bookmark_ref = self.request.GET.get('bookmark')
749 755
750 756 try:
751 757 source_repo_data = PullRequestModel().generate_repo_data(
752 758 source_repo, commit_id=commit_id,
753 759 branch=branch_ref, bookmark=bookmark_ref,
754 760 translator=self.request.translate)
755 761 except CommitDoesNotExistError as e:
756 762 log.exception(e)
757 763 h.flash(_('Commit does not exist'), 'error')
758 764 raise HTTPFound(
759 765 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
760 766
761 767 default_target_repo = source_repo
762 768
763 769 if source_repo.parent and c.has_origin_repo_read_perm:
764 770 parent_vcs_obj = source_repo.parent.scm_instance()
765 771 if parent_vcs_obj and not parent_vcs_obj.is_empty():
766 772 # change default if we have a parent repo
767 773 default_target_repo = source_repo.parent
768 774
769 775 target_repo_data = PullRequestModel().generate_repo_data(
770 776 default_target_repo, translator=self.request.translate)
771 777
772 778 selected_source_ref = source_repo_data['refs']['selected_ref']
773 779 title_source_ref = ''
774 780 if selected_source_ref:
775 781 title_source_ref = selected_source_ref.split(':', 2)[1]
776 782 c.default_title = PullRequestModel().generate_pullrequest_title(
777 783 source=source_repo.repo_name,
778 784 source_ref=title_source_ref,
779 785 target=default_target_repo.repo_name
780 786 )
781 787
782 788 c.default_repo_data = {
783 789 'source_repo_name': source_repo.repo_name,
784 790 'source_refs_json': json.dumps(source_repo_data),
785 791 'target_repo_name': default_target_repo.repo_name,
786 792 'target_refs_json': json.dumps(target_repo_data),
787 793 }
788 794 c.default_source_ref = selected_source_ref
789 795
790 796 return self._get_template_context(c)
791 797
792 798 @LoginRequired()
793 799 @NotAnonymous()
794 800 @HasRepoPermissionAnyDecorator(
795 801 'repository.read', 'repository.write', 'repository.admin')
796 802 @view_config(
797 803 route_name='pullrequest_repo_refs', request_method='GET',
798 804 renderer='json_ext', xhr=True)
799 805 def pull_request_repo_refs(self):
800 806 self.load_default_context()
801 807 target_repo_name = self.request.matchdict['target_repo_name']
802 808 repo = Repository.get_by_repo_name(target_repo_name)
803 809 if not repo:
804 810 raise HTTPNotFound()
805 811
806 812 target_perm = HasRepoPermissionAny(
807 813 'repository.read', 'repository.write', 'repository.admin')(
808 814 target_repo_name)
809 815 if not target_perm:
810 816 raise HTTPNotFound()
811 817
812 818 return PullRequestModel().generate_repo_data(
813 819 repo, translator=self.request.translate)
814 820
815 821 @LoginRequired()
816 822 @NotAnonymous()
817 823 @HasRepoPermissionAnyDecorator(
818 824 'repository.read', 'repository.write', 'repository.admin')
819 825 @view_config(
820 826 route_name='pullrequest_repo_targets', request_method='GET',
821 827 renderer='json_ext', xhr=True)
822 828 def pullrequest_repo_targets(self):
823 829 _ = self.request.translate
824 830 filter_query = self.request.GET.get('query')
825 831
826 832 # get the parents
827 833 parent_target_repos = []
828 834 if self.db_repo.parent:
829 835 parents_query = Repository.query() \
830 836 .order_by(func.length(Repository.repo_name)) \
831 837 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
832 838
833 839 if filter_query:
834 840 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
835 841 parents_query = parents_query.filter(
836 842 Repository.repo_name.ilike(ilike_expression))
837 843 parents = parents_query.limit(20).all()
838 844
839 845 for parent in parents:
840 846 parent_vcs_obj = parent.scm_instance()
841 847 if parent_vcs_obj and not parent_vcs_obj.is_empty():
842 848 parent_target_repos.append(parent)
843 849
844 850 # get other forks, and repo itself
845 851 query = Repository.query() \
846 852 .order_by(func.length(Repository.repo_name)) \
847 853 .filter(
848 854 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
849 855 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
850 856 ) \
851 857 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
852 858
853 859 if filter_query:
854 860 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
855 861 query = query.filter(Repository.repo_name.ilike(ilike_expression))
856 862
857 863 limit = max(20 - len(parent_target_repos), 5) # not less then 5
858 864 target_repos = query.limit(limit).all()
859 865
860 866 all_target_repos = target_repos + parent_target_repos
861 867
862 868 repos = []
863 869 # This checks permissions to the repositories
864 870 for obj in ScmModel().get_repos(all_target_repos):
865 871 repos.append({
866 872 'id': obj['name'],
867 873 'text': obj['name'],
868 874 'type': 'repo',
869 875 'repo_id': obj['dbrepo']['repo_id'],
870 876 'repo_type': obj['dbrepo']['repo_type'],
871 877 'private': obj['dbrepo']['private'],
872 878
873 879 })
874 880
875 881 data = {
876 882 'more': False,
877 883 'results': [{
878 884 'text': _('Repositories'),
879 885 'children': repos
880 886 }] if repos else []
881 887 }
882 888 return data
883 889
884 890 @LoginRequired()
885 891 @NotAnonymous()
886 892 @HasRepoPermissionAnyDecorator(
887 893 'repository.read', 'repository.write', 'repository.admin')
888 894 @CSRFRequired()
889 895 @view_config(
890 896 route_name='pullrequest_create', request_method='POST',
891 897 renderer=None)
892 898 def pull_request_create(self):
893 899 _ = self.request.translate
894 900 self.assure_not_empty_repo()
895 901 self.load_default_context()
896 902
897 903 controls = peppercorn.parse(self.request.POST.items())
898 904
899 905 try:
900 906 form = PullRequestForm(
901 907 self.request.translate, self.db_repo.repo_id)()
902 908 _form = form.to_python(controls)
903 909 except formencode.Invalid as errors:
904 910 if errors.error_dict.get('revisions'):
905 911 msg = 'Revisions: %s' % errors.error_dict['revisions']
906 912 elif errors.error_dict.get('pullrequest_title'):
907 913 msg = errors.error_dict.get('pullrequest_title')
908 914 else:
909 915 msg = _('Error creating pull request: {}').format(errors)
910 916 log.exception(msg)
911 917 h.flash(msg, 'error')
912 918
913 919 # would rather just go back to form ...
914 920 raise HTTPFound(
915 921 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
916 922
917 923 source_repo = _form['source_repo']
918 924 source_ref = _form['source_ref']
919 925 target_repo = _form['target_repo']
920 926 target_ref = _form['target_ref']
921 927 commit_ids = _form['revisions'][::-1]
922 928
923 929 # find the ancestor for this pr
924 930 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
925 931 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
926 932
927 933 if not (source_db_repo or target_db_repo):
928 934 h.flash(_('source_repo or target repo not found'), category='error')
929 935 raise HTTPFound(
930 936 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
931 937
932 938 # re-check permissions again here
933 939 # source_repo we must have read permissions
934 940
935 941 source_perm = HasRepoPermissionAny(
936 942 'repository.read', 'repository.write', 'repository.admin')(
937 943 source_db_repo.repo_name)
938 944 if not source_perm:
939 945 msg = _('Not Enough permissions to source repo `{}`.'.format(
940 946 source_db_repo.repo_name))
941 947 h.flash(msg, category='error')
942 948 # copy the args back to redirect
943 949 org_query = self.request.GET.mixed()
944 950 raise HTTPFound(
945 951 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
946 952 _query=org_query))
947 953
948 954 # target repo we must have read permissions, and also later on
949 955 # we want to check branch permissions here
950 956 target_perm = HasRepoPermissionAny(
951 957 'repository.read', 'repository.write', 'repository.admin')(
952 958 target_db_repo.repo_name)
953 959 if not target_perm:
954 960 msg = _('Not Enough permissions to target repo `{}`.'.format(
955 961 target_db_repo.repo_name))
956 962 h.flash(msg, category='error')
957 963 # copy the args back to redirect
958 964 org_query = self.request.GET.mixed()
959 965 raise HTTPFound(
960 966 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
961 967 _query=org_query))
962 968
963 969 source_scm = source_db_repo.scm_instance()
964 970 target_scm = target_db_repo.scm_instance()
965 971
966 972 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
967 973 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
968 974
969 975 ancestor = source_scm.get_common_ancestor(
970 976 source_commit.raw_id, target_commit.raw_id, target_scm)
971 977
972 978 # recalculate target ref based on ancestor
973 979 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
974 980 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
975 981
976 982 get_default_reviewers_data, validate_default_reviewers = \
977 983 PullRequestModel().get_reviewer_functions()
978 984
979 985 # recalculate reviewers logic, to make sure we can validate this
980 986 reviewer_rules = get_default_reviewers_data(
981 987 self._rhodecode_db_user, source_db_repo,
982 988 source_commit, target_db_repo, target_commit)
983 989
984 990 given_reviewers = _form['review_members']
985 991 reviewers = validate_default_reviewers(
986 992 given_reviewers, reviewer_rules)
987 993
988 994 pullrequest_title = _form['pullrequest_title']
989 995 title_source_ref = source_ref.split(':', 2)[1]
990 996 if not pullrequest_title:
991 997 pullrequest_title = PullRequestModel().generate_pullrequest_title(
992 998 source=source_repo,
993 999 source_ref=title_source_ref,
994 1000 target=target_repo
995 1001 )
996 1002
997 1003 description = _form['pullrequest_desc']
998 1004 description_renderer = _form['description_renderer']
999 1005
1000 1006 try:
1001 1007 pull_request = PullRequestModel().create(
1002 1008 created_by=self._rhodecode_user.user_id,
1003 1009 source_repo=source_repo,
1004 1010 source_ref=source_ref,
1005 1011 target_repo=target_repo,
1006 1012 target_ref=target_ref,
1007 1013 revisions=commit_ids,
1008 1014 reviewers=reviewers,
1009 1015 title=pullrequest_title,
1010 1016 description=description,
1011 1017 description_renderer=description_renderer,
1012 1018 reviewer_data=reviewer_rules,
1013 1019 auth_user=self._rhodecode_user
1014 1020 )
1015 1021 Session().commit()
1016 1022
1017 1023 h.flash(_('Successfully opened new pull request'),
1018 1024 category='success')
1019 1025 except Exception:
1020 1026 msg = _('Error occurred during creation of this pull request.')
1021 1027 log.exception(msg)
1022 1028 h.flash(msg, category='error')
1023 1029
1024 1030 # copy the args back to redirect
1025 1031 org_query = self.request.GET.mixed()
1026 1032 raise HTTPFound(
1027 1033 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1028 1034 _query=org_query))
1029 1035
1030 1036 raise HTTPFound(
1031 1037 h.route_path('pullrequest_show', repo_name=target_repo,
1032 1038 pull_request_id=pull_request.pull_request_id))
1033 1039
1034 1040 @LoginRequired()
1035 1041 @NotAnonymous()
1036 1042 @HasRepoPermissionAnyDecorator(
1037 1043 'repository.read', 'repository.write', 'repository.admin')
1038 1044 @CSRFRequired()
1039 1045 @view_config(
1040 1046 route_name='pullrequest_update', request_method='POST',
1041 1047 renderer='json_ext')
1042 1048 def pull_request_update(self):
1043 1049 pull_request = PullRequest.get_or_404(
1044 1050 self.request.matchdict['pull_request_id'])
1045 1051 _ = self.request.translate
1046 1052
1047 1053 self.load_default_context()
1048 1054
1049 1055 if pull_request.is_closed():
1050 1056 log.debug('update: forbidden because pull request is closed')
1051 1057 msg = _(u'Cannot update closed pull requests.')
1052 1058 h.flash(msg, category='error')
1053 1059 return True
1054 1060
1055 1061 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1056 1062 log.debug('update: forbidden because pull request is in state %s',
1057 1063 pull_request.pull_request_state)
1058 1064 msg = _(u'Cannot update pull requests in state other than `{}`. '
1059 1065 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1060 1066 pull_request.pull_request_state)
1061 1067 h.flash(msg, category='error')
1062 1068 return True
1063 1069
1064 1070 # only owner or admin can update it
1065 1071 allowed_to_update = PullRequestModel().check_user_update(
1066 1072 pull_request, self._rhodecode_user)
1067 1073 if allowed_to_update:
1068 1074 controls = peppercorn.parse(self.request.POST.items())
1069 1075
1070 1076 if 'review_members' in controls:
1071 1077 self._update_reviewers(
1072 1078 pull_request, controls['review_members'],
1073 1079 pull_request.reviewer_data)
1074 1080 elif str2bool(self.request.POST.get('update_commits', 'false')):
1075 1081 self._update_commits(pull_request)
1076 1082 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1077 1083 self._edit_pull_request(pull_request)
1078 1084 else:
1079 1085 raise HTTPBadRequest()
1080 1086 return True
1081 1087 raise HTTPForbidden()
1082 1088
1083 1089 def _edit_pull_request(self, pull_request):
1084 1090 _ = self.request.translate
1085 1091
1086 1092 try:
1087 1093 PullRequestModel().edit(
1088 1094 pull_request,
1089 1095 self.request.POST.get('title'),
1090 1096 self.request.POST.get('description'),
1091 1097 self.request.POST.get('description_renderer'),
1092 1098 self._rhodecode_user)
1093 1099 except ValueError:
1094 1100 msg = _(u'Cannot update closed pull requests.')
1095 1101 h.flash(msg, category='error')
1096 1102 return
1097 1103 else:
1098 1104 Session().commit()
1099 1105
1100 1106 msg = _(u'Pull request title & description updated.')
1101 1107 h.flash(msg, category='success')
1102 1108 return
1103 1109
1104 1110 def _update_commits(self, pull_request):
1105 1111 _ = self.request.translate
1106 1112
1107 1113 with pull_request.set_state(PullRequest.STATE_UPDATING):
1108 1114 resp = PullRequestModel().update_commits(pull_request)
1109 1115
1110 1116 if resp.executed:
1111 1117
1112 1118 if resp.target_changed and resp.source_changed:
1113 1119 changed = 'target and source repositories'
1114 1120 elif resp.target_changed and not resp.source_changed:
1115 1121 changed = 'target repository'
1116 1122 elif not resp.target_changed and resp.source_changed:
1117 1123 changed = 'source repository'
1118 1124 else:
1119 1125 changed = 'nothing'
1120 1126
1121 1127 msg = _(u'Pull request updated to "{source_commit_id}" with '
1122 1128 u'{count_added} added, {count_removed} removed commits. '
1123 1129 u'Source of changes: {change_source}')
1124 1130 msg = msg.format(
1125 1131 source_commit_id=pull_request.source_ref_parts.commit_id,
1126 1132 count_added=len(resp.changes.added),
1127 1133 count_removed=len(resp.changes.removed),
1128 1134 change_source=changed)
1129 1135 h.flash(msg, category='success')
1130 1136
1131 1137 channel = '/repo${}$/pr/{}'.format(
1132 1138 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1133 1139 message = msg + (
1134 1140 ' - <a onclick="window.location.reload()">'
1135 1141 '<strong>{}</strong></a>'.format(_('Reload page')))
1136 1142 channelstream.post_message(
1137 1143 channel, message, self._rhodecode_user.username,
1138 1144 registry=self.request.registry)
1139 1145 else:
1140 1146 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1141 1147 warning_reasons = [
1142 1148 UpdateFailureReason.NO_CHANGE,
1143 1149 UpdateFailureReason.WRONG_REF_TYPE,
1144 1150 ]
1145 1151 category = 'warning' if resp.reason in warning_reasons else 'error'
1146 1152 h.flash(msg, category=category)
1147 1153
1148 1154 @LoginRequired()
1149 1155 @NotAnonymous()
1150 1156 @HasRepoPermissionAnyDecorator(
1151 1157 'repository.read', 'repository.write', 'repository.admin')
1152 1158 @CSRFRequired()
1153 1159 @view_config(
1154 1160 route_name='pullrequest_merge', request_method='POST',
1155 1161 renderer='json_ext')
1156 1162 def pull_request_merge(self):
1157 1163 """
1158 1164 Merge will perform a server-side merge of the specified
1159 1165 pull request, if the pull request is approved and mergeable.
1160 1166 After successful merging, the pull request is automatically
1161 1167 closed, with a relevant comment.
1162 1168 """
1163 1169 pull_request = PullRequest.get_or_404(
1164 1170 self.request.matchdict['pull_request_id'])
1165 1171 _ = self.request.translate
1166 1172
1167 1173 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1168 1174 log.debug('show: forbidden because pull request is in state %s',
1169 1175 pull_request.pull_request_state)
1170 1176 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1171 1177 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1172 1178 pull_request.pull_request_state)
1173 1179 h.flash(msg, category='error')
1174 1180 raise HTTPFound(
1175 1181 h.route_path('pullrequest_show',
1176 1182 repo_name=pull_request.target_repo.repo_name,
1177 1183 pull_request_id=pull_request.pull_request_id))
1178 1184
1179 1185 self.load_default_context()
1180 1186
1181 1187 with pull_request.set_state(PullRequest.STATE_UPDATING):
1182 1188 check = MergeCheck.validate(
1183 1189 pull_request, auth_user=self._rhodecode_user,
1184 1190 translator=self.request.translate)
1185 1191 merge_possible = not check.failed
1186 1192
1187 1193 for err_type, error_msg in check.errors:
1188 1194 h.flash(error_msg, category=err_type)
1189 1195
1190 1196 if merge_possible:
1191 1197 log.debug("Pre-conditions checked, trying to merge.")
1192 1198 extras = vcs_operation_context(
1193 1199 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1194 1200 username=self._rhodecode_db_user.username, action='push',
1195 1201 scm=pull_request.target_repo.repo_type)
1196 1202 with pull_request.set_state(PullRequest.STATE_UPDATING):
1197 1203 self._merge_pull_request(
1198 1204 pull_request, self._rhodecode_db_user, extras)
1199 1205 else:
1200 1206 log.debug("Pre-conditions failed, NOT merging.")
1201 1207
1202 1208 raise HTTPFound(
1203 1209 h.route_path('pullrequest_show',
1204 1210 repo_name=pull_request.target_repo.repo_name,
1205 1211 pull_request_id=pull_request.pull_request_id))
1206 1212
1207 1213 def _merge_pull_request(self, pull_request, user, extras):
1208 1214 _ = self.request.translate
1209 1215 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1210 1216
1211 1217 if merge_resp.executed:
1212 1218 log.debug("The merge was successful, closing the pull request.")
1213 1219 PullRequestModel().close_pull_request(
1214 1220 pull_request.pull_request_id, user)
1215 1221 Session().commit()
1216 1222 msg = _('Pull request was successfully merged and closed.')
1217 1223 h.flash(msg, category='success')
1218 1224 else:
1219 1225 log.debug(
1220 1226 "The merge was not successful. Merge response: %s", merge_resp)
1221 1227 msg = merge_resp.merge_status_message
1222 1228 h.flash(msg, category='error')
1223 1229
1224 1230 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1225 1231 _ = self.request.translate
1226 1232
1227 1233 get_default_reviewers_data, validate_default_reviewers = \
1228 1234 PullRequestModel().get_reviewer_functions()
1229 1235
1230 1236 try:
1231 1237 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1232 1238 except ValueError as e:
1233 1239 log.error('Reviewers Validation: {}'.format(e))
1234 1240 h.flash(e, category='error')
1235 1241 return
1236 1242
1237 1243 old_calculated_status = pull_request.calculated_review_status()
1238 1244 PullRequestModel().update_reviewers(
1239 1245 pull_request, reviewers, self._rhodecode_user)
1240 1246 h.flash(_('Pull request reviewers updated.'), category='success')
1241 1247 Session().commit()
1242 1248
1243 1249 # trigger status changed if change in reviewers changes the status
1244 1250 calculated_status = pull_request.calculated_review_status()
1245 1251 if old_calculated_status != calculated_status:
1246 1252 PullRequestModel().trigger_pull_request_hook(
1247 1253 pull_request, self._rhodecode_user, 'review_status_change',
1248 1254 data={'status': calculated_status})
1249 1255
1250 1256 @LoginRequired()
1251 1257 @NotAnonymous()
1252 1258 @HasRepoPermissionAnyDecorator(
1253 1259 'repository.read', 'repository.write', 'repository.admin')
1254 1260 @CSRFRequired()
1255 1261 @view_config(
1256 1262 route_name='pullrequest_delete', request_method='POST',
1257 1263 renderer='json_ext')
1258 1264 def pull_request_delete(self):
1259 1265 _ = self.request.translate
1260 1266
1261 1267 pull_request = PullRequest.get_or_404(
1262 1268 self.request.matchdict['pull_request_id'])
1263 1269 self.load_default_context()
1264 1270
1265 1271 pr_closed = pull_request.is_closed()
1266 1272 allowed_to_delete = PullRequestModel().check_user_delete(
1267 1273 pull_request, self._rhodecode_user) and not pr_closed
1268 1274
1269 1275 # only owner can delete it !
1270 1276 if allowed_to_delete:
1271 1277 PullRequestModel().delete(pull_request, self._rhodecode_user)
1272 1278 Session().commit()
1273 1279 h.flash(_('Successfully deleted pull request'),
1274 1280 category='success')
1275 1281 raise HTTPFound(h.route_path('pullrequest_show_all',
1276 1282 repo_name=self.db_repo_name))
1277 1283
1278 1284 log.warning('user %s tried to delete pull request without access',
1279 1285 self._rhodecode_user)
1280 1286 raise HTTPNotFound()
1281 1287
1282 1288 @LoginRequired()
1283 1289 @NotAnonymous()
1284 1290 @HasRepoPermissionAnyDecorator(
1285 1291 'repository.read', 'repository.write', 'repository.admin')
1286 1292 @CSRFRequired()
1287 1293 @view_config(
1288 1294 route_name='pullrequest_comment_create', request_method='POST',
1289 1295 renderer='json_ext')
1290 1296 def pull_request_comment_create(self):
1291 1297 _ = self.request.translate
1292 1298
1293 1299 pull_request = PullRequest.get_or_404(
1294 1300 self.request.matchdict['pull_request_id'])
1295 1301 pull_request_id = pull_request.pull_request_id
1296 1302
1297 1303 if pull_request.is_closed():
1298 1304 log.debug('comment: forbidden because pull request is closed')
1299 1305 raise HTTPForbidden()
1300 1306
1301 1307 allowed_to_comment = PullRequestModel().check_user_comment(
1302 1308 pull_request, self._rhodecode_user)
1303 1309 if not allowed_to_comment:
1304 1310 log.debug(
1305 1311 'comment: forbidden because pull request is from forbidden repo')
1306 1312 raise HTTPForbidden()
1307 1313
1308 1314 c = self.load_default_context()
1309 1315
1310 1316 status = self.request.POST.get('changeset_status', None)
1311 1317 text = self.request.POST.get('text')
1312 1318 comment_type = self.request.POST.get('comment_type')
1313 1319 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1314 1320 close_pull_request = self.request.POST.get('close_pull_request')
1315 1321
1316 1322 # the logic here should work like following, if we submit close
1317 1323 # pr comment, use `close_pull_request_with_comment` function
1318 1324 # else handle regular comment logic
1319 1325
1320 1326 if close_pull_request:
1321 1327 # only owner or admin or person with write permissions
1322 1328 allowed_to_close = PullRequestModel().check_user_update(
1323 1329 pull_request, self._rhodecode_user)
1324 1330 if not allowed_to_close:
1325 1331 log.debug('comment: forbidden because not allowed to close '
1326 1332 'pull request %s', pull_request_id)
1327 1333 raise HTTPForbidden()
1328 1334
1329 1335 # This also triggers `review_status_change`
1330 1336 comment, status = PullRequestModel().close_pull_request_with_comment(
1331 1337 pull_request, self._rhodecode_user, self.db_repo, message=text,
1332 1338 auth_user=self._rhodecode_user)
1333 1339 Session().flush()
1334 1340
1335 1341 PullRequestModel().trigger_pull_request_hook(
1336 1342 pull_request, self._rhodecode_user, 'comment',
1337 1343 data={'comment': comment})
1338 1344
1339 1345 else:
1340 1346 # regular comment case, could be inline, or one with status.
1341 1347 # for that one we check also permissions
1342 1348
1343 1349 allowed_to_change_status = PullRequestModel().check_user_change_status(
1344 1350 pull_request, self._rhodecode_user)
1345 1351
1346 1352 if status and allowed_to_change_status:
1347 1353 message = (_('Status change %(transition_icon)s %(status)s')
1348 1354 % {'transition_icon': '>',
1349 1355 'status': ChangesetStatus.get_status_lbl(status)})
1350 1356 text = text or message
1351 1357
1352 1358 comment = CommentsModel().create(
1353 1359 text=text,
1354 1360 repo=self.db_repo.repo_id,
1355 1361 user=self._rhodecode_user.user_id,
1356 1362 pull_request=pull_request,
1357 1363 f_path=self.request.POST.get('f_path'),
1358 1364 line_no=self.request.POST.get('line'),
1359 1365 status_change=(ChangesetStatus.get_status_lbl(status)
1360 1366 if status and allowed_to_change_status else None),
1361 1367 status_change_type=(status
1362 1368 if status and allowed_to_change_status else None),
1363 1369 comment_type=comment_type,
1364 1370 resolves_comment_id=resolves_comment_id,
1365 1371 auth_user=self._rhodecode_user
1366 1372 )
1367 1373
1368 1374 if allowed_to_change_status:
1369 1375 # calculate old status before we change it
1370 1376 old_calculated_status = pull_request.calculated_review_status()
1371 1377
1372 1378 # get status if set !
1373 1379 if status:
1374 1380 ChangesetStatusModel().set_status(
1375 1381 self.db_repo.repo_id,
1376 1382 status,
1377 1383 self._rhodecode_user.user_id,
1378 1384 comment,
1379 1385 pull_request=pull_request
1380 1386 )
1381 1387
1382 1388 Session().flush()
1383 1389 # this is somehow required to get access to some relationship
1384 1390 # loaded on comment
1385 1391 Session().refresh(comment)
1386 1392
1387 1393 PullRequestModel().trigger_pull_request_hook(
1388 1394 pull_request, self._rhodecode_user, 'comment',
1389 1395 data={'comment': comment})
1390 1396
1391 1397 # we now calculate the status of pull request, and based on that
1392 1398 # calculation we set the commits status
1393 1399 calculated_status = pull_request.calculated_review_status()
1394 1400 if old_calculated_status != calculated_status:
1395 1401 PullRequestModel().trigger_pull_request_hook(
1396 1402 pull_request, self._rhodecode_user, 'review_status_change',
1397 1403 data={'status': calculated_status})
1398 1404
1399 1405 Session().commit()
1400 1406
1401 1407 data = {
1402 1408 'target_id': h.safeid(h.safe_unicode(
1403 1409 self.request.POST.get('f_path'))),
1404 1410 }
1405 1411 if comment:
1406 1412 c.co = comment
1407 1413 rendered_comment = render(
1408 1414 'rhodecode:templates/changeset/changeset_comment_block.mako',
1409 1415 self._get_template_context(c), self.request)
1410 1416
1411 1417 data.update(comment.get_dict())
1412 1418 data.update({'rendered_text': rendered_comment})
1413 1419
1414 1420 return data
1415 1421
1416 1422 @LoginRequired()
1417 1423 @NotAnonymous()
1418 1424 @HasRepoPermissionAnyDecorator(
1419 1425 'repository.read', 'repository.write', 'repository.admin')
1420 1426 @CSRFRequired()
1421 1427 @view_config(
1422 1428 route_name='pullrequest_comment_delete', request_method='POST',
1423 1429 renderer='json_ext')
1424 1430 def pull_request_comment_delete(self):
1425 1431 pull_request = PullRequest.get_or_404(
1426 1432 self.request.matchdict['pull_request_id'])
1427 1433
1428 1434 comment = ChangesetComment.get_or_404(
1429 1435 self.request.matchdict['comment_id'])
1430 1436 comment_id = comment.comment_id
1431 1437
1432 1438 if pull_request.is_closed():
1433 1439 log.debug('comment: forbidden because pull request is closed')
1434 1440 raise HTTPForbidden()
1435 1441
1436 1442 if not comment:
1437 1443 log.debug('Comment with id:%s not found, skipping', comment_id)
1438 1444 # comment already deleted in another call probably
1439 1445 return True
1440 1446
1441 1447 if comment.pull_request.is_closed():
1442 1448 # don't allow deleting comments on closed pull request
1443 1449 raise HTTPForbidden()
1444 1450
1445 1451 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1446 1452 super_admin = h.HasPermissionAny('hg.admin')()
1447 1453 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1448 1454 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1449 1455 comment_repo_admin = is_repo_admin and is_repo_comment
1450 1456
1451 1457 if super_admin or comment_owner or comment_repo_admin:
1452 1458 old_calculated_status = comment.pull_request.calculated_review_status()
1453 1459 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1454 1460 Session().commit()
1455 1461 calculated_status = comment.pull_request.calculated_review_status()
1456 1462 if old_calculated_status != calculated_status:
1457 1463 PullRequestModel().trigger_pull_request_hook(
1458 1464 comment.pull_request, self._rhodecode_user, 'review_status_change',
1459 1465 data={'status': calculated_status})
1460 1466 return True
1461 1467 else:
1462 1468 log.warning('No permissions for user %s to delete comment_id: %s',
1463 1469 self._rhodecode_db_user, comment_id)
1464 1470 raise HTTPNotFound()
@@ -1,717 +1,734 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 pyramid.threadlocal import get_current_registry, get_current_request
30 30 from sqlalchemy.sql.expression import null
31 31 from sqlalchemy.sql.functions import coalesce
32 32
33 33 from rhodecode.lib import helpers as h, diffs, channelstream
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 39 from rhodecode.model.notification import NotificationModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.model.notification import EmailNotificationModel
43 43 from rhodecode.model.validation_schema.schemas import comment_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CommentsModel(BaseModel):
50 50
51 51 cls = ChangesetComment
52 52
53 53 DIFF_CONTEXT_BEFORE = 3
54 54 DIFF_CONTEXT_AFTER = 3
55 55
56 56 def __get_commit_comment(self, changeset_comment):
57 57 return self._get_instance(ChangesetComment, changeset_comment)
58 58
59 59 def __get_pull_request(self, pull_request):
60 60 return self._get_instance(PullRequest, pull_request)
61 61
62 62 def _extract_mentions(self, s):
63 63 user_objects = []
64 64 for username in extract_mentioned_users(s):
65 65 user_obj = User.get_by_username(username, case_insensitive=True)
66 66 if user_obj:
67 67 user_objects.append(user_obj)
68 68 return user_objects
69 69
70 70 def _get_renderer(self, global_renderer='rst', request=None):
71 71 request = request or get_current_request()
72 72
73 73 try:
74 74 global_renderer = request.call_context.visual.default_renderer
75 75 except AttributeError:
76 76 log.debug("Renderer not set, falling back "
77 77 "to default renderer '%s'", global_renderer)
78 78 except Exception:
79 79 log.error(traceback.format_exc())
80 80 return global_renderer
81 81
82 82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 83 # group by versions, and count until, and display objects
84 84
85 85 comment_groups = collections.defaultdict(list)
86 86 [comment_groups[
87 87 _co.pull_request_version_id].append(_co) for _co in comments]
88 88
89 89 def yield_comments(pos):
90 90 for co in comment_groups[pos]:
91 91 yield co
92 92
93 93 comment_versions = collections.defaultdict(
94 94 lambda: collections.defaultdict(list))
95 95 prev_prvid = -1
96 96 # fake last entry with None, to aggregate on "latest" version which
97 97 # doesn't have an pull_request_version_id
98 98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 99 prvid = ver.pull_request_version_id
100 100 if prev_prvid == -1:
101 101 prev_prvid = prvid
102 102
103 103 for co in yield_comments(prvid):
104 104 comment_versions[prvid]['at'].append(co)
105 105
106 106 # save until
107 107 current = comment_versions[prvid]['at']
108 108 prev_until = comment_versions[prev_prvid]['until']
109 109 cur_until = prev_until + current
110 110 comment_versions[prvid]['until'].extend(cur_until)
111 111
112 112 # save outdated
113 113 if inline:
114 114 outdated = [x for x in cur_until
115 115 if x.outdated_at_version(show_version)]
116 116 else:
117 117 outdated = [x for x in cur_until
118 118 if x.older_than_version(show_version)]
119 119 display = [x for x in cur_until if x not in outdated]
120 120
121 121 comment_versions[prvid]['outdated'] = outdated
122 122 comment_versions[prvid]['display'] = display
123 123
124 124 prev_prvid = prvid
125 125
126 126 return comment_versions
127 127
128 128 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
129 129 qry = Session().query(ChangesetComment) \
130 130 .filter(ChangesetComment.repo == repo)
131 131
132 132 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
133 133 qry = qry.filter(ChangesetComment.comment_type == comment_type)
134 134
135 135 if user:
136 136 user = self._get_user(user)
137 137 if user:
138 138 qry = qry.filter(ChangesetComment.user_id == user.user_id)
139 139
140 140 if commit_id:
141 141 qry = qry.filter(ChangesetComment.revision == commit_id)
142 142
143 143 qry = qry.order_by(ChangesetComment.created_on)
144 144 return qry.all()
145 145
146 146 def get_repository_unresolved_todos(self, repo):
147 147 todos = Session().query(ChangesetComment) \
148 148 .filter(ChangesetComment.repo == repo) \
149 149 .filter(ChangesetComment.resolved_by == None) \
150 150 .filter(ChangesetComment.comment_type
151 151 == ChangesetComment.COMMENT_TYPE_TODO)
152 152 todos = todos.all()
153 153
154 154 return todos
155 155
156 156 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
157 157
158 158 todos = Session().query(ChangesetComment) \
159 159 .filter(ChangesetComment.pull_request == pull_request) \
160 160 .filter(ChangesetComment.resolved_by == None) \
161 161 .filter(ChangesetComment.comment_type
162 162 == ChangesetComment.COMMENT_TYPE_TODO)
163 163
164 164 if not show_outdated:
165 165 todos = todos.filter(
166 166 coalesce(ChangesetComment.display_state, '') !=
167 167 ChangesetComment.COMMENT_OUTDATED)
168 168
169 169 todos = todos.all()
170 170
171 171 return todos
172 172
173 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
174
175 todos = Session().query(ChangesetComment) \
176 .filter(ChangesetComment.pull_request == pull_request) \
177 .filter(ChangesetComment.resolved_by != None) \
178 .filter(ChangesetComment.comment_type
179 == ChangesetComment.COMMENT_TYPE_TODO)
180
181 if not show_outdated:
182 todos = todos.filter(
183 coalesce(ChangesetComment.display_state, '') !=
184 ChangesetComment.COMMENT_OUTDATED)
185
186 todos = todos.all()
187
188 return todos
189
173 190 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
174 191
175 192 todos = Session().query(ChangesetComment) \
176 193 .filter(ChangesetComment.revision == commit_id) \
177 194 .filter(ChangesetComment.resolved_by == None) \
178 195 .filter(ChangesetComment.comment_type
179 196 == ChangesetComment.COMMENT_TYPE_TODO)
180 197
181 198 if not show_outdated:
182 199 todos = todos.filter(
183 200 coalesce(ChangesetComment.display_state, '') !=
184 201 ChangesetComment.COMMENT_OUTDATED)
185 202
186 203 todos = todos.all()
187 204
188 205 return todos
189 206
190 207 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
191 208
192 209 todos = Session().query(ChangesetComment) \
193 210 .filter(ChangesetComment.revision == commit_id) \
194 211 .filter(ChangesetComment.resolved_by != None) \
195 212 .filter(ChangesetComment.comment_type
196 213 == ChangesetComment.COMMENT_TYPE_TODO)
197 214
198 215 if not show_outdated:
199 216 todos = todos.filter(
200 217 coalesce(ChangesetComment.display_state, '') !=
201 218 ChangesetComment.COMMENT_OUTDATED)
202 219
203 220 todos = todos.all()
204 221
205 222 return todos
206 223
207 224 def _log_audit_action(self, action, action_data, auth_user, comment):
208 225 audit_logger.store(
209 226 action=action,
210 227 action_data=action_data,
211 228 user=auth_user,
212 229 repo=comment.repo)
213 230
214 231 def create(self, text, repo, user, commit_id=None, pull_request=None,
215 232 f_path=None, line_no=None, status_change=None,
216 233 status_change_type=None, comment_type=None,
217 234 resolves_comment_id=None, closing_pr=False, send_email=True,
218 235 renderer=None, auth_user=None):
219 236 """
220 237 Creates new comment for commit or pull request.
221 238 IF status_change is not none this comment is associated with a
222 239 status change of commit or commit associated with pull request
223 240
224 241 :param text:
225 242 :param repo:
226 243 :param user:
227 244 :param commit_id:
228 245 :param pull_request:
229 246 :param f_path:
230 247 :param line_no:
231 248 :param status_change: Label for status change
232 249 :param comment_type: Type of comment
233 250 :param status_change_type: type of status change
234 251 :param closing_pr:
235 252 :param send_email:
236 253 :param renderer: pick renderer for this comment
237 254 """
238 255
239 256 if not text:
240 257 log.warning('Missing text for comment, skipping...')
241 258 return
242 259 request = get_current_request()
243 260 _ = request.translate
244 261
245 262 if not renderer:
246 263 renderer = self._get_renderer(request=request)
247 264
248 265 repo = self._get_repo(repo)
249 266 user = self._get_user(user)
250 267 auth_user = auth_user or user
251 268
252 269 schema = comment_schema.CommentSchema()
253 270 validated_kwargs = schema.deserialize(dict(
254 271 comment_body=text,
255 272 comment_type=comment_type,
256 273 comment_file=f_path,
257 274 comment_line=line_no,
258 275 renderer_type=renderer,
259 276 status_change=status_change_type,
260 277 resolves_comment_id=resolves_comment_id,
261 278 repo=repo.repo_id,
262 279 user=user.user_id,
263 280 ))
264 281
265 282 comment = ChangesetComment()
266 283 comment.renderer = validated_kwargs['renderer_type']
267 284 comment.text = validated_kwargs['comment_body']
268 285 comment.f_path = validated_kwargs['comment_file']
269 286 comment.line_no = validated_kwargs['comment_line']
270 287 comment.comment_type = validated_kwargs['comment_type']
271 288
272 289 comment.repo = repo
273 290 comment.author = user
274 291 resolved_comment = self.__get_commit_comment(
275 292 validated_kwargs['resolves_comment_id'])
276 293 # check if the comment actually belongs to this PR
277 294 if resolved_comment and resolved_comment.pull_request and \
278 295 resolved_comment.pull_request != pull_request:
279 296 log.warning('Comment tried to resolved unrelated todo comment: %s',
280 297 resolved_comment)
281 298 # comment not bound to this pull request, forbid
282 299 resolved_comment = None
283 300
284 301 elif resolved_comment and resolved_comment.repo and \
285 302 resolved_comment.repo != repo:
286 303 log.warning('Comment tried to resolved unrelated todo comment: %s',
287 304 resolved_comment)
288 305 # comment not bound to this repo, forbid
289 306 resolved_comment = None
290 307
291 308 comment.resolved_comment = resolved_comment
292 309
293 310 pull_request_id = pull_request
294 311
295 312 commit_obj = None
296 313 pull_request_obj = None
297 314
298 315 if commit_id:
299 316 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
300 317 # do a lookup, so we don't pass something bad here
301 318 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
302 319 comment.revision = commit_obj.raw_id
303 320
304 321 elif pull_request_id:
305 322 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
306 323 pull_request_obj = self.__get_pull_request(pull_request_id)
307 324 comment.pull_request = pull_request_obj
308 325 else:
309 326 raise Exception('Please specify commit or pull_request_id')
310 327
311 328 Session().add(comment)
312 329 Session().flush()
313 330 kwargs = {
314 331 'user': user,
315 332 'renderer_type': renderer,
316 333 'repo_name': repo.repo_name,
317 334 'status_change': status_change,
318 335 'status_change_type': status_change_type,
319 336 'comment_body': text,
320 337 'comment_file': f_path,
321 338 'comment_line': line_no,
322 339 'comment_type': comment_type or 'note'
323 340 }
324 341
325 342 if commit_obj:
326 343 recipients = ChangesetComment.get_users(
327 344 revision=commit_obj.raw_id)
328 345 # add commit author if it's in RhodeCode system
329 346 cs_author = User.get_from_cs_author(commit_obj.author)
330 347 if not cs_author:
331 348 # use repo owner if we cannot extract the author correctly
332 349 cs_author = repo.user
333 350 recipients += [cs_author]
334 351
335 352 commit_comment_url = self.get_url(comment, request=request)
336 353
337 354 target_repo_url = h.link_to(
338 355 repo.repo_name,
339 356 h.route_url('repo_summary', repo_name=repo.repo_name))
340 357
341 358 # commit specifics
342 359 kwargs.update({
343 360 'commit': commit_obj,
344 361 'commit_message': commit_obj.message,
345 362 'commit_target_repo': target_repo_url,
346 363 'commit_comment_url': commit_comment_url,
347 364 })
348 365
349 366 elif pull_request_obj:
350 367 # get the current participants of this pull request
351 368 recipients = ChangesetComment.get_users(
352 369 pull_request_id=pull_request_obj.pull_request_id)
353 370 # add pull request author
354 371 recipients += [pull_request_obj.author]
355 372
356 373 # add the reviewers to notification
357 374 recipients += [x.user for x in pull_request_obj.reviewers]
358 375
359 376 pr_target_repo = pull_request_obj.target_repo
360 377 pr_source_repo = pull_request_obj.source_repo
361 378
362 379 pr_comment_url = h.route_url(
363 380 'pullrequest_show',
364 381 repo_name=pr_target_repo.repo_name,
365 382 pull_request_id=pull_request_obj.pull_request_id,
366 383 _anchor='comment-%s' % comment.comment_id)
367 384
368 385 # set some variables for email notification
369 386 pr_target_repo_url = h.route_url(
370 387 'repo_summary', repo_name=pr_target_repo.repo_name)
371 388
372 389 pr_source_repo_url = h.route_url(
373 390 'repo_summary', repo_name=pr_source_repo.repo_name)
374 391
375 392 # pull request specifics
376 393 kwargs.update({
377 394 'pull_request': pull_request_obj,
378 395 'pr_id': pull_request_obj.pull_request_id,
379 396 'pr_target_repo': pr_target_repo,
380 397 'pr_target_repo_url': pr_target_repo_url,
381 398 'pr_source_repo': pr_source_repo,
382 399 'pr_source_repo_url': pr_source_repo_url,
383 400 'pr_comment_url': pr_comment_url,
384 401 'pr_closing': closing_pr,
385 402 })
386 403 if send_email:
387 404 # pre-generate the subject for notification itself
388 405 (subject,
389 406 _h, _e, # we don't care about those
390 407 body_plaintext) = EmailNotificationModel().render_email(
391 408 notification_type, **kwargs)
392 409
393 410 mention_recipients = set(
394 411 self._extract_mentions(text)).difference(recipients)
395 412
396 413 # create notification objects, and emails
397 414 NotificationModel().create(
398 415 created_by=user,
399 416 notification_subject=subject,
400 417 notification_body=body_plaintext,
401 418 notification_type=notification_type,
402 419 recipients=recipients,
403 420 mention_recipients=mention_recipients,
404 421 email_kwargs=kwargs,
405 422 )
406 423
407 424 Session().flush()
408 425 if comment.pull_request:
409 426 action = 'repo.pull_request.comment.create'
410 427 else:
411 428 action = 'repo.commit.comment.create'
412 429
413 430 comment_data = comment.get_api_data()
414 431 self._log_audit_action(
415 432 action, {'data': comment_data}, auth_user, comment)
416 433
417 434 msg_url = ''
418 435 channel = None
419 436 if commit_obj:
420 437 msg_url = commit_comment_url
421 438 repo_name = repo.repo_name
422 439 channel = u'/repo${}$/commit/{}'.format(
423 440 repo_name,
424 441 commit_obj.raw_id
425 442 )
426 443 elif pull_request_obj:
427 444 msg_url = pr_comment_url
428 445 repo_name = pr_target_repo.repo_name
429 446 channel = u'/repo${}$/pr/{}'.format(
430 447 repo_name,
431 448 pull_request_id
432 449 )
433 450
434 451 message = '<strong>{}</strong> {} - ' \
435 452 '<a onclick="window.location=\'{}\';' \
436 453 'window.location.reload()">' \
437 454 '<strong>{}</strong></a>'
438 455 message = message.format(
439 456 user.username, _('made a comment'), msg_url,
440 457 _('Show it now'))
441 458
442 459 channelstream.post_message(
443 460 channel, message, user.username,
444 461 registry=get_current_registry())
445 462
446 463 return comment
447 464
448 465 def delete(self, comment, auth_user):
449 466 """
450 467 Deletes given comment
451 468 """
452 469 comment = self.__get_commit_comment(comment)
453 470 old_data = comment.get_api_data()
454 471 Session().delete(comment)
455 472
456 473 if comment.pull_request:
457 474 action = 'repo.pull_request.comment.delete'
458 475 else:
459 476 action = 'repo.commit.comment.delete'
460 477
461 478 self._log_audit_action(
462 479 action, {'old_data': old_data}, auth_user, comment)
463 480
464 481 return comment
465 482
466 483 def get_all_comments(self, repo_id, revision=None, pull_request=None):
467 484 q = ChangesetComment.query()\
468 485 .filter(ChangesetComment.repo_id == repo_id)
469 486 if revision:
470 487 q = q.filter(ChangesetComment.revision == revision)
471 488 elif pull_request:
472 489 pull_request = self.__get_pull_request(pull_request)
473 490 q = q.filter(ChangesetComment.pull_request == pull_request)
474 491 else:
475 492 raise Exception('Please specify commit or pull_request')
476 493 q = q.order_by(ChangesetComment.created_on)
477 494 return q.all()
478 495
479 496 def get_url(self, comment, request=None, permalink=False):
480 497 if not request:
481 498 request = get_current_request()
482 499
483 500 comment = self.__get_commit_comment(comment)
484 501 if comment.pull_request:
485 502 pull_request = comment.pull_request
486 503 if permalink:
487 504 return request.route_url(
488 505 'pull_requests_global',
489 506 pull_request_id=pull_request.pull_request_id,
490 507 _anchor='comment-%s' % comment.comment_id)
491 508 else:
492 509 return request.route_url(
493 510 'pullrequest_show',
494 511 repo_name=safe_str(pull_request.target_repo.repo_name),
495 512 pull_request_id=pull_request.pull_request_id,
496 513 _anchor='comment-%s' % comment.comment_id)
497 514
498 515 else:
499 516 repo = comment.repo
500 517 commit_id = comment.revision
501 518
502 519 if permalink:
503 520 return request.route_url(
504 521 'repo_commit', repo_name=safe_str(repo.repo_id),
505 522 commit_id=commit_id,
506 523 _anchor='comment-%s' % comment.comment_id)
507 524
508 525 else:
509 526 return request.route_url(
510 527 'repo_commit', repo_name=safe_str(repo.repo_name),
511 528 commit_id=commit_id,
512 529 _anchor='comment-%s' % comment.comment_id)
513 530
514 531 def get_comments(self, repo_id, revision=None, pull_request=None):
515 532 """
516 533 Gets main comments based on revision or pull_request_id
517 534
518 535 :param repo_id:
519 536 :param revision:
520 537 :param pull_request:
521 538 """
522 539
523 540 q = ChangesetComment.query()\
524 541 .filter(ChangesetComment.repo_id == repo_id)\
525 542 .filter(ChangesetComment.line_no == None)\
526 543 .filter(ChangesetComment.f_path == None)
527 544 if revision:
528 545 q = q.filter(ChangesetComment.revision == revision)
529 546 elif pull_request:
530 547 pull_request = self.__get_pull_request(pull_request)
531 548 q = q.filter(ChangesetComment.pull_request == pull_request)
532 549 else:
533 550 raise Exception('Please specify commit or pull_request')
534 551 q = q.order_by(ChangesetComment.created_on)
535 552 return q.all()
536 553
537 554 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
538 555 q = self._get_inline_comments_query(repo_id, revision, pull_request)
539 556 return self._group_comments_by_path_and_line_number(q)
540 557
541 558 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
542 559 version=None):
543 560 inline_cnt = 0
544 561 for fname, per_line_comments in inline_comments.iteritems():
545 562 for lno, comments in per_line_comments.iteritems():
546 563 for comm in comments:
547 564 if not comm.outdated_at_version(version) and skip_outdated:
548 565 inline_cnt += 1
549 566
550 567 return inline_cnt
551 568
552 569 def get_outdated_comments(self, repo_id, pull_request):
553 570 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
554 571 # of a pull request.
555 572 q = self._all_inline_comments_of_pull_request(pull_request)
556 573 q = q.filter(
557 574 ChangesetComment.display_state ==
558 575 ChangesetComment.COMMENT_OUTDATED
559 576 ).order_by(ChangesetComment.comment_id.asc())
560 577
561 578 return self._group_comments_by_path_and_line_number(q)
562 579
563 580 def _get_inline_comments_query(self, repo_id, revision, pull_request):
564 581 # TODO: johbo: Split this into two methods: One for PR and one for
565 582 # commit.
566 583 if revision:
567 584 q = Session().query(ChangesetComment).filter(
568 585 ChangesetComment.repo_id == repo_id,
569 586 ChangesetComment.line_no != null(),
570 587 ChangesetComment.f_path != null(),
571 588 ChangesetComment.revision == revision)
572 589
573 590 elif pull_request:
574 591 pull_request = self.__get_pull_request(pull_request)
575 592 if not CommentsModel.use_outdated_comments(pull_request):
576 593 q = self._visible_inline_comments_of_pull_request(pull_request)
577 594 else:
578 595 q = self._all_inline_comments_of_pull_request(pull_request)
579 596
580 597 else:
581 598 raise Exception('Please specify commit or pull_request_id')
582 599 q = q.order_by(ChangesetComment.comment_id.asc())
583 600 return q
584 601
585 602 def _group_comments_by_path_and_line_number(self, q):
586 603 comments = q.all()
587 604 paths = collections.defaultdict(lambda: collections.defaultdict(list))
588 605 for co in comments:
589 606 paths[co.f_path][co.line_no].append(co)
590 607 return paths
591 608
592 609 @classmethod
593 610 def needed_extra_diff_context(cls):
594 611 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
595 612
596 613 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
597 614 if not CommentsModel.use_outdated_comments(pull_request):
598 615 return
599 616
600 617 comments = self._visible_inline_comments_of_pull_request(pull_request)
601 618 comments_to_outdate = comments.all()
602 619
603 620 for comment in comments_to_outdate:
604 621 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
605 622
606 623 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
607 624 diff_line = _parse_comment_line_number(comment.line_no)
608 625
609 626 try:
610 627 old_context = old_diff_proc.get_context_of_line(
611 628 path=comment.f_path, diff_line=diff_line)
612 629 new_context = new_diff_proc.get_context_of_line(
613 630 path=comment.f_path, diff_line=diff_line)
614 631 except (diffs.LineNotInDiffException,
615 632 diffs.FileNotInDiffException):
616 633 comment.display_state = ChangesetComment.COMMENT_OUTDATED
617 634 return
618 635
619 636 if old_context == new_context:
620 637 return
621 638
622 639 if self._should_relocate_diff_line(diff_line):
623 640 new_diff_lines = new_diff_proc.find_context(
624 641 path=comment.f_path, context=old_context,
625 642 offset=self.DIFF_CONTEXT_BEFORE)
626 643 if not new_diff_lines:
627 644 comment.display_state = ChangesetComment.COMMENT_OUTDATED
628 645 else:
629 646 new_diff_line = self._choose_closest_diff_line(
630 647 diff_line, new_diff_lines)
631 648 comment.line_no = _diff_to_comment_line_number(new_diff_line)
632 649 else:
633 650 comment.display_state = ChangesetComment.COMMENT_OUTDATED
634 651
635 652 def _should_relocate_diff_line(self, diff_line):
636 653 """
637 654 Checks if relocation shall be tried for the given `diff_line`.
638 655
639 656 If a comment points into the first lines, then we can have a situation
640 657 that after an update another line has been added on top. In this case
641 658 we would find the context still and move the comment around. This
642 659 would be wrong.
643 660 """
644 661 should_relocate = (
645 662 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
646 663 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
647 664 return should_relocate
648 665
649 666 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
650 667 candidate = new_diff_lines[0]
651 668 best_delta = _diff_line_delta(diff_line, candidate)
652 669 for new_diff_line in new_diff_lines[1:]:
653 670 delta = _diff_line_delta(diff_line, new_diff_line)
654 671 if delta < best_delta:
655 672 candidate = new_diff_line
656 673 best_delta = delta
657 674 return candidate
658 675
659 676 def _visible_inline_comments_of_pull_request(self, pull_request):
660 677 comments = self._all_inline_comments_of_pull_request(pull_request)
661 678 comments = comments.filter(
662 679 coalesce(ChangesetComment.display_state, '') !=
663 680 ChangesetComment.COMMENT_OUTDATED)
664 681 return comments
665 682
666 683 def _all_inline_comments_of_pull_request(self, pull_request):
667 684 comments = Session().query(ChangesetComment)\
668 685 .filter(ChangesetComment.line_no != None)\
669 686 .filter(ChangesetComment.f_path != None)\
670 687 .filter(ChangesetComment.pull_request == pull_request)
671 688 return comments
672 689
673 690 def _all_general_comments_of_pull_request(self, pull_request):
674 691 comments = Session().query(ChangesetComment)\
675 692 .filter(ChangesetComment.line_no == None)\
676 693 .filter(ChangesetComment.f_path == None)\
677 694 .filter(ChangesetComment.pull_request == pull_request)
678 695 return comments
679 696
680 697 @staticmethod
681 698 def use_outdated_comments(pull_request):
682 699 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
683 700 settings = settings_model.get_general_settings()
684 701 return settings.get('rhodecode_use_outdated_comments', False)
685 702
686 703
687 704 def _parse_comment_line_number(line_no):
688 705 """
689 706 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
690 707 """
691 708 old_line = None
692 709 new_line = None
693 710 if line_no.startswith('o'):
694 711 old_line = int(line_no[1:])
695 712 elif line_no.startswith('n'):
696 713 new_line = int(line_no[1:])
697 714 else:
698 715 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
699 716 return diffs.DiffLineNumber(old_line, new_line)
700 717
701 718
702 719 def _diff_to_comment_line_number(diff_line):
703 720 if diff_line.new is not None:
704 721 return u'n{}'.format(diff_line.new)
705 722 elif diff_line.old is not None:
706 723 return u'o{}'.format(diff_line.old)
707 724 return u''
708 725
709 726
710 727 def _diff_line_delta(a, b):
711 728 if None not in (a.new, b.new):
712 729 return abs(a.new - b.new)
713 730 elif None not in (a.old, b.old):
714 731 return abs(a.old - b.old)
715 732 else:
716 733 raise ValueError(
717 734 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,581 +1,585 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 @comment-outdated-opacity: 0.6;
8 8
9 9 .comments {
10 10 width: 100%;
11 11 }
12 12
13 13 .comments-heading {
14 14 margin-bottom: -1px;
15 15 background: @grey6;
16 16 display: block;
17 17 padding: 10px 0px;
18 18 font-size: 18px
19 19 }
20 20
21 #comment-tr-show {
22 padding: 5px 0;
23 }
24
21 25 tr.inline-comments div {
22 26 max-width: 100%;
23 27
24 28 p {
25 29 white-space: normal;
26 30 }
27 31
28 32 code, pre, .code, dd {
29 33 overflow-x: auto;
30 34 width: 1062px;
31 35 }
32 36
33 37 dd {
34 38 width: auto;
35 39 }
36 40 }
37 41
38 42 #injected_page_comments {
39 43 .comment-previous-link,
40 44 .comment-next-link,
41 45 .comment-links-divider {
42 46 display: none;
43 47 }
44 48 }
45 49
46 50 .add-comment {
47 51 margin-bottom: 10px;
48 52 }
49 53 .hide-comment-button .add-comment {
50 54 display: none;
51 55 }
52 56
53 57 .comment-bubble {
54 58 color: @grey4;
55 59 margin-top: 4px;
56 60 margin-right: 30px;
57 61 visibility: hidden;
58 62 }
59 63
60 64 .comment-label {
61 65 float: left;
62 66
63 67 padding: 0.4em 0.4em;
64 68 margin: 3px 5px 0px -10px;
65 69 display: inline-block;
66 70 min-height: 0;
67 71
68 72 text-align: center;
69 73 font-size: 10px;
70 74 line-height: .8em;
71 75
72 76 font-family: @text-italic;
73 77 font-style: italic;
74 78 background: #fff none;
75 79 color: @grey4;
76 80 border: 1px solid @grey4;
77 81 white-space: nowrap;
78 82
79 83 text-transform: uppercase;
80 84 min-width: 40px;
81 85
82 86 &.todo {
83 87 color: @color5;
84 88 font-style: italic;
85 89 font-weight: @text-bold-italic-weight;
86 90 font-family: @text-bold-italic;
87 91 }
88 92
89 93 .resolve {
90 94 cursor: pointer;
91 95 text-decoration: underline;
92 96 }
93 97
94 98 .resolved {
95 99 text-decoration: line-through;
96 100 color: @color1;
97 101 }
98 102 .resolved a {
99 103 text-decoration: line-through;
100 104 color: @color1;
101 105 }
102 106 .resolve-text {
103 107 color: @color1;
104 108 margin: 2px 8px;
105 109 font-family: @text-italic;
106 110 font-style: italic;
107 111 }
108 112 }
109 113
110 114 .has-spacer-after {
111 115 &:after {
112 116 content: ' | ';
113 117 color: @grey5;
114 118 }
115 119 }
116 120
117 121 .has-spacer-before {
118 122 &:before {
119 123 content: ' | ';
120 124 color: @grey5;
121 125 }
122 126 }
123 127
124 128 .comment {
125 129
126 130 &.comment-general {
127 131 border: 1px solid @grey5;
128 132 padding: 5px 5px 5px 5px;
129 133 }
130 134
131 135 margin: @padding 0;
132 136 padding: 4px 0 0 0;
133 137 line-height: 1em;
134 138
135 139 .rc-user {
136 140 min-width: 0;
137 141 margin: 0px .5em 0 0;
138 142
139 143 .user {
140 144 display: inline;
141 145 }
142 146 }
143 147
144 148 .meta {
145 149 position: relative;
146 150 width: 100%;
147 151 border-bottom: 1px solid @grey5;
148 152 margin: -5px 0px;
149 153 line-height: 24px;
150 154
151 155 &:hover .permalink {
152 156 visibility: visible;
153 157 color: @rcblue;
154 158 }
155 159 }
156 160
157 161 .author,
158 162 .date {
159 163 display: inline;
160 164
161 165 &:after {
162 166 content: ' | ';
163 167 color: @grey5;
164 168 }
165 169 }
166 170
167 171 .author-general img {
168 172 top: 3px;
169 173 }
170 174 .author-inline img {
171 175 top: 3px;
172 176 }
173 177
174 178 .status-change,
175 179 .permalink,
176 180 .changeset-status-lbl {
177 181 display: inline;
178 182 }
179 183
180 184 .permalink {
181 185 visibility: hidden;
182 186 }
183 187
184 188 .comment-links-divider {
185 189 display: inline;
186 190 }
187 191
188 192 .comment-links-block {
189 193 float:right;
190 194 text-align: right;
191 195 min-width: 85px;
192 196
193 197 [class^="icon-"]:before,
194 198 [class*=" icon-"]:before {
195 199 margin-left: 0;
196 200 margin-right: 0;
197 201 }
198 202 }
199 203
200 204 .comment-previous-link {
201 205 display: inline-block;
202 206
203 207 .arrow_comment_link{
204 208 cursor: pointer;
205 209 i {
206 210 font-size:10px;
207 211 }
208 212 }
209 213 .arrow_comment_link.disabled {
210 214 cursor: default;
211 215 color: @grey5;
212 216 }
213 217 }
214 218
215 219 .comment-next-link {
216 220 display: inline-block;
217 221
218 222 .arrow_comment_link{
219 223 cursor: pointer;
220 224 i {
221 225 font-size:10px;
222 226 }
223 227 }
224 228 .arrow_comment_link.disabled {
225 229 cursor: default;
226 230 color: @grey5;
227 231 }
228 232 }
229 233
230 234 .delete-comment {
231 235 display: inline-block;
232 236 color: @rcblue;
233 237
234 238 &:hover {
235 239 cursor: pointer;
236 240 }
237 241 }
238 242
239 243 .text {
240 244 clear: both;
241 245 .border-radius(@border-radius);
242 246 .box-sizing(border-box);
243 247
244 248 .markdown-block p,
245 249 .rst-block p {
246 250 margin: .5em 0 !important;
247 251 // TODO: lisa: This is needed because of other rst !important rules :[
248 252 }
249 253 }
250 254
251 255 .pr-version {
252 256 float: left;
253 257 margin: 0px 4px;
254 258 }
255 259 .pr-version-inline {
256 260 float: left;
257 261 margin: 0px 4px;
258 262 }
259 263 .pr-version-num {
260 264 font-size: 10px;
261 265 }
262 266 }
263 267
264 268 @comment-padding: 5px;
265 269
266 270 .general-comments {
267 271 .comment-outdated {
268 272 opacity: @comment-outdated-opacity;
269 273 }
270 274 }
271 275
272 276 .inline-comments {
273 277 border-radius: @border-radius;
274 278 .comment {
275 279 margin: 0;
276 280 border-radius: @border-radius;
277 281 }
278 282 .comment-outdated {
279 283 opacity: @comment-outdated-opacity;
280 284 }
281 285
282 286 .comment-inline {
283 287 background: white;
284 288 padding: @comment-padding @comment-padding;
285 289 border: @comment-padding solid @grey6;
286 290
287 291 .text {
288 292 border: none;
289 293 }
290 294 .meta {
291 295 border-bottom: 1px solid @grey6;
292 296 margin: -5px 0px;
293 297 line-height: 24px;
294 298 }
295 299 }
296 300 .comment-selected {
297 301 border-left: 6px solid @comment-highlight-color;
298 302 }
299 303 .comment-inline-form {
300 304 padding: @comment-padding;
301 305 display: none;
302 306 }
303 307 .cb-comment-add-button {
304 308 margin: @comment-padding;
305 309 }
306 310 /* hide add comment button when form is open */
307 311 .comment-inline-form-open ~ .cb-comment-add-button {
308 312 display: none;
309 313 }
310 314 .comment-inline-form-open {
311 315 display: block;
312 316 }
313 317 /* hide add comment button when form but no comments */
314 318 .comment-inline-form:first-child + .cb-comment-add-button {
315 319 display: none;
316 320 }
317 321 /* hide add comment button when no comments or form */
318 322 .cb-comment-add-button:first-child {
319 323 display: none;
320 324 }
321 325 /* hide add comment button when only comment is being deleted */
322 326 .comment-deleting:first-child + .cb-comment-add-button {
323 327 display: none;
324 328 }
325 329 }
326 330
327 331
328 332 .show-outdated-comments {
329 333 display: inline;
330 334 color: @rcblue;
331 335 }
332 336
333 337 // Comment Form
334 338 div.comment-form {
335 339 margin-top: 20px;
336 340 }
337 341
338 342 .comment-form strong {
339 343 display: block;
340 344 margin-bottom: 15px;
341 345 }
342 346
343 347 .comment-form textarea {
344 348 width: 100%;
345 349 height: 100px;
346 350 font-family: @text-monospace;
347 351 }
348 352
349 353 form.comment-form {
350 354 margin-top: 10px;
351 355 margin-left: 10px;
352 356 }
353 357
354 358 .comment-inline-form .comment-block-ta,
355 359 .comment-form .comment-block-ta,
356 360 .comment-form .preview-box {
357 361 .border-radius(@border-radius);
358 362 .box-sizing(border-box);
359 363 background-color: white;
360 364 }
361 365
362 366 .comment-form-submit {
363 367 margin-top: 5px;
364 368 margin-left: 525px;
365 369 }
366 370
367 371 .file-comments {
368 372 display: none;
369 373 }
370 374
371 375 .comment-form .preview-box.unloaded,
372 376 .comment-inline-form .preview-box.unloaded {
373 377 height: 50px;
374 378 text-align: center;
375 379 padding: 20px;
376 380 background-color: white;
377 381 }
378 382
379 383 .comment-footer {
380 384 position: relative;
381 385 width: 100%;
382 386 min-height: 42px;
383 387
384 388 .status_box,
385 389 .cancel-button {
386 390 float: left;
387 391 display: inline-block;
388 392 }
389 393
390 394 .action-buttons {
391 395 float: right;
392 396 display: inline-block;
393 397 }
394 398
395 399 .action-buttons-extra {
396 400 display: inline-block;
397 401 }
398 402 }
399 403
400 404 .comment-form {
401 405
402 406 .comment {
403 407 margin-left: 10px;
404 408 }
405 409
406 410 .comment-help {
407 411 color: @grey4;
408 412 padding: 5px 0 5px 0;
409 413 }
410 414
411 415 .comment-title {
412 416 padding: 5px 0 5px 0;
413 417 }
414 418
415 419 .comment-button {
416 420 display: inline-block;
417 421 }
418 422
419 423 .comment-button-input {
420 424 margin-right: 0;
421 425 }
422 426
423 427 .comment-footer {
424 428 margin-bottom: 110px;
425 429 margin-top: 10px;
426 430 }
427 431 }
428 432
429 433
430 434 .comment-form-login {
431 435 .comment-help {
432 436 padding: 0.7em; //same as the button
433 437 }
434 438
435 439 div.clearfix {
436 440 clear: both;
437 441 width: 100%;
438 442 display: block;
439 443 }
440 444 }
441 445
442 446 .comment-type {
443 447 margin: 0px;
444 448 border-radius: inherit;
445 449 border-color: @grey6;
446 450 }
447 451
448 452 .preview-box {
449 453 min-height: 105px;
450 454 margin-bottom: 15px;
451 455 background-color: white;
452 456 .border-radius(@border-radius);
453 457 .box-sizing(border-box);
454 458 }
455 459
456 460 .add-another-button {
457 461 margin-left: 10px;
458 462 margin-top: 10px;
459 463 margin-bottom: 10px;
460 464 }
461 465
462 466 .comment .buttons {
463 467 float: right;
464 468 margin: -1px 0px 0px 0px;
465 469 }
466 470
467 471 // Inline Comment Form
468 472 .injected_diff .comment-inline-form,
469 473 .comment-inline-form {
470 474 background-color: white;
471 475 margin-top: 10px;
472 476 margin-bottom: 20px;
473 477 }
474 478
475 479 .inline-form {
476 480 padding: 10px 7px;
477 481 }
478 482
479 483 .inline-form div {
480 484 max-width: 100%;
481 485 }
482 486
483 487 .overlay {
484 488 display: none;
485 489 position: absolute;
486 490 width: 100%;
487 491 text-align: center;
488 492 vertical-align: middle;
489 493 font-size: 16px;
490 494 background: none repeat scroll 0 0 white;
491 495
492 496 &.submitting {
493 497 display: block;
494 498 opacity: 0.5;
495 499 z-index: 100;
496 500 }
497 501 }
498 502 .comment-inline-form .overlay.submitting .overlay-text {
499 503 margin-top: 5%;
500 504 }
501 505
502 506 .comment-inline-form .clearfix,
503 507 .comment-form .clearfix {
504 508 .border-radius(@border-radius);
505 509 margin: 0px;
506 510 }
507 511
508 512 .comment-inline-form .comment-footer {
509 513 margin: 10px 0px 0px 0px;
510 514 }
511 515
512 516 .hide-inline-form-button {
513 517 margin-left: 5px;
514 518 }
515 519 .comment-button .hide-inline-form {
516 520 background: white;
517 521 }
518 522
519 523 .comment-area {
520 524 padding: 8px 12px;
521 525 border: 1px solid @grey5;
522 526 .border-radius(@border-radius);
523 527
524 528 .resolve-action {
525 529 padding: 1px 0px 0px 6px;
526 530 }
527 531
528 532 }
529 533
530 534 .comment-area-header .nav-links {
531 535 display: flex;
532 536 flex-flow: row wrap;
533 537 -webkit-flex-flow: row wrap;
534 538 width: 100%;
535 539 }
536 540
537 541 .comment-area-footer {
538 542 display: flex;
539 543 }
540 544
541 545 .comment-footer .toolbar {
542 546
543 547 }
544 548
545 549 .nav-links {
546 550 padding: 0;
547 551 margin: 0;
548 552 list-style: none;
549 553 height: auto;
550 554 border-bottom: 1px solid @grey5;
551 555 }
552 556 .nav-links li {
553 557 display: inline-block;
554 558 list-style-type: none;
555 559 }
556 560
557 561 .nav-links li a.disabled {
558 562 cursor: not-allowed;
559 563 }
560 564
561 565 .nav-links li.active a {
562 566 border-bottom: 2px solid @rcblue;
563 567 color: #000;
564 568 font-weight: 600;
565 569 }
566 570 .nav-links li a {
567 571 display: inline-block;
568 572 padding: 0px 10px 5px 10px;
569 573 margin-bottom: -1px;
570 574 font-size: 14px;
571 575 line-height: 28px;
572 576 color: #8f8f8f;
573 577 border-bottom: 2px solid transparent;
574 578 }
575 579
576 580 .toolbar-text {
577 581 float: left;
578 582 margin: -5px 0px 0px 0px;
579 583 font-size: 12px;
580 584 }
581 585
@@ -1,2823 +1,2819 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 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
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-size: 120%;
38 38 color: white;
39 39 background-color: @alert2;
40 40 padding: 5px 0 5px 0;
41 41 font-weight: @text-semibold-weight;
42 42 font-family: @text-semibold;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .action-link.disabled {
104 104 color: @grey4;
105 105 cursor: inherit;
106 106 }
107 107
108 108 .clipboard-action {
109 109 cursor: pointer;
110 110 color: @grey4;
111 111 margin-left: 5px;
112 112
113 113 &:hover {
114 114 color: @grey2;
115 115 }
116 116 }
117 117
118 118 ul.simple-list{
119 119 list-style: none;
120 120 margin: 0;
121 121 padding: 0;
122 122 }
123 123
124 124 .main-content {
125 125 padding-bottom: @pagepadding;
126 126 }
127 127
128 128 .wide-mode-wrapper {
129 129 max-width:4000px !important;
130 130 }
131 131
132 132 .wrapper {
133 133 position: relative;
134 134 max-width: @wrapper-maxwidth;
135 135 margin: 0 auto;
136 136 }
137 137
138 138 #content {
139 139 clear: both;
140 140 padding: 0 @contentpadding;
141 141 }
142 142
143 143 .advanced-settings-fields{
144 144 input{
145 145 margin-left: @textmargin;
146 146 margin-right: @padding/2;
147 147 }
148 148 }
149 149
150 150 .cs_files_title {
151 151 margin: @pagepadding 0 0;
152 152 }
153 153
154 154 input.inline[type="file"] {
155 155 display: inline;
156 156 }
157 157
158 158 .error_page {
159 159 margin: 10% auto;
160 160
161 161 h1 {
162 162 color: @grey2;
163 163 }
164 164
165 165 .alert {
166 166 margin: @padding 0;
167 167 }
168 168
169 169 .error-branding {
170 170 color: @grey4;
171 171 font-weight: @text-semibold-weight;
172 172 font-family: @text-semibold;
173 173 }
174 174
175 175 .error_message {
176 176 font-family: @text-regular;
177 177 }
178 178
179 179 .sidebar {
180 180 min-height: 275px;
181 181 margin: 0;
182 182 padding: 0 0 @sidebarpadding @sidebarpadding;
183 183 border: none;
184 184 }
185 185
186 186 .main-content {
187 187 position: relative;
188 188 margin: 0 @sidebarpadding @sidebarpadding;
189 189 padding: 0 0 0 @sidebarpadding;
190 190 border-left: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 margin: 0;
196 196 border: none;
197 197 }
198 198 }
199 199
200 200 .inner-column {
201 201 float: left;
202 202 width: 29.75%;
203 203 min-height: 150px;
204 204 margin: @sidebarpadding 2% 0 0;
205 205 padding: 0 2% 0 0;
206 206 border-right: @border-thickness solid @grey5;
207 207
208 208 @media (max-width:767px) {
209 209 clear: both;
210 210 width: 100%;
211 211 border: none;
212 212 }
213 213
214 214 ul {
215 215 padding-left: 1.25em;
216 216 }
217 217
218 218 &:last-child {
219 219 margin: @sidebarpadding 0 0;
220 220 border: none;
221 221 }
222 222
223 223 h4 {
224 224 margin: 0 0 @padding;
225 225 font-weight: @text-semibold-weight;
226 226 font-family: @text-semibold;
227 227 }
228 228 }
229 229 }
230 230 .error-page-logo {
231 231 width: 130px;
232 232 height: 160px;
233 233 }
234 234
235 235 // HEADER
236 236 .header {
237 237
238 238 // TODO: johbo: Fix login pages, so that they work without a min-height
239 239 // for the header and then remove the min-height. I chose a smaller value
240 240 // intentionally here to avoid rendering issues in the main navigation.
241 241 min-height: 49px;
242 242 min-width: 1024px;
243 243
244 244 position: relative;
245 245 vertical-align: bottom;
246 246 padding: 0 @header-padding;
247 247 background-color: @grey1;
248 248 color: @grey5;
249 249
250 250 .title {
251 251 overflow: visible;
252 252 }
253 253
254 254 &:before,
255 255 &:after {
256 256 content: "";
257 257 clear: both;
258 258 width: 100%;
259 259 }
260 260
261 261 // TODO: johbo: Avoids breaking "Repositories" chooser
262 262 .select2-container .select2-choice .select2-arrow {
263 263 display: none;
264 264 }
265 265 }
266 266
267 267 #header-inner {
268 268 &.title {
269 269 margin: 0;
270 270 }
271 271 &:before,
272 272 &:after {
273 273 content: "";
274 274 clear: both;
275 275 }
276 276 }
277 277
278 278 // Gists
279 279 #files_data {
280 280 clear: both; //for firefox
281 281 padding-top: 10px;
282 282 }
283 283
284 284 #gistid {
285 285 margin-right: @padding;
286 286 }
287 287
288 288 // Global Settings Editor
289 289 .textarea.editor {
290 290 float: left;
291 291 position: relative;
292 292 max-width: @texteditor-width;
293 293
294 294 select {
295 295 position: absolute;
296 296 top:10px;
297 297 right:0;
298 298 }
299 299
300 300 .CodeMirror {
301 301 margin: 0;
302 302 }
303 303
304 304 .help-block {
305 305 margin: 0 0 @padding;
306 306 padding:.5em;
307 307 background-color: @grey6;
308 308 &.pre-formatting {
309 309 white-space: pre;
310 310 }
311 311 }
312 312 }
313 313
314 314 ul.auth_plugins {
315 315 margin: @padding 0 @padding @legend-width;
316 316 padding: 0;
317 317
318 318 li {
319 319 margin-bottom: @padding;
320 320 line-height: 1em;
321 321 list-style-type: none;
322 322
323 323 .auth_buttons .btn {
324 324 margin-right: @padding;
325 325 }
326 326
327 327 }
328 328 }
329 329
330 330
331 331 // My Account PR list
332 332
333 333 #show_closed {
334 334 margin: 0 1em 0 0;
335 335 }
336 336
337 337 #pull_request_list_table {
338 338 .closed {
339 339 background-color: @grey6;
340 340 }
341 341
342 342 .state-creating,
343 343 .state-updating,
344 344 .state-merging
345 345 {
346 346 background-color: @grey6;
347 347 }
348 348
349 349 .td-status {
350 350 padding-left: .5em;
351 351 }
352 352 .log-container .truncate {
353 353 height: 2.75em;
354 354 white-space: pre-line;
355 355 }
356 356 table.rctable .user {
357 357 padding-left: 0;
358 358 }
359 359 table.rctable {
360 360 td.td-description,
361 361 .rc-user {
362 362 min-width: auto;
363 363 }
364 364 }
365 365 }
366 366
367 367 // Pull Requests
368 368
369 369 .pullrequests_section_head {
370 370 display: block;
371 371 clear: both;
372 372 margin: @padding 0;
373 373 font-weight: @text-bold-weight;
374 374 font-family: @text-bold;
375 375 }
376 376
377 377 .pr-origininfo, .pr-targetinfo {
378 378 position: relative;
379 379
380 380 .tag {
381 381 display: inline-block;
382 382 margin: 0 1em .5em 0;
383 383 }
384 384
385 385 .clone-url {
386 386 display: inline-block;
387 387 margin: 0 0 .5em 0;
388 388 padding: 0;
389 389 line-height: 1.2em;
390 390 }
391 391 }
392 392
393 393 .pr-mergeinfo {
394 394 min-width: 95% !important;
395 395 padding: 0 !important;
396 396 border: 0;
397 397 }
398 398 .pr-mergeinfo-copy {
399 399 padding: 0 0;
400 400 }
401 401
402 402 .pr-pullinfo {
403 403 min-width: 95% !important;
404 404 padding: 0 !important;
405 405 border: 0;
406 406 }
407 407 .pr-pullinfo-copy {
408 408 padding: 0 0;
409 409 }
410 410
411 411
412 412 #pr-title-input {
413 413 width: 72%;
414 414 font-size: 1em;
415 415 margin: 0;
416 416 padding: 0 0 0 @padding/4;
417 417 line-height: 1.7em;
418 418 color: @text-color;
419 419 letter-spacing: .02em;
420 420 font-weight: @text-bold-weight;
421 421 font-family: @text-bold;
422 422 }
423 423
424 424 #pullrequest_title {
425 425 width: 100%;
426 426 box-sizing: border-box;
427 427 }
428 428
429 429 #pr_open_message {
430 430 border: @border-thickness solid #fff;
431 431 border-radius: @border-radius;
432 432 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
433 433 text-align: left;
434 434 overflow: hidden;
435 435 }
436 436
437 437 .pr-submit-button {
438 438 float: right;
439 439 margin: 0 0 0 5px;
440 440 }
441 441
442 442 .pr-spacing-container {
443 443 padding: 20px;
444 444 clear: both
445 445 }
446 446
447 447 #pr-description-input {
448 448 margin-bottom: 0;
449 449 }
450 450
451 451 .pr-description-label {
452 452 vertical-align: top;
453 453 }
454 454
455 455 .perms_section_head {
456 456 min-width: 625px;
457 457
458 458 h2 {
459 459 margin-bottom: 0;
460 460 }
461 461
462 462 .label-checkbox {
463 463 float: left;
464 464 }
465 465
466 466 &.field {
467 467 margin: @space 0 @padding;
468 468 }
469 469
470 470 &:first-child.field {
471 471 margin-top: 0;
472 472
473 473 .label {
474 474 margin-top: 0;
475 475 padding-top: 0;
476 476 }
477 477
478 478 .radios {
479 479 padding-top: 0;
480 480 }
481 481 }
482 482
483 483 .radios {
484 484 position: relative;
485 485 width: 505px;
486 486 }
487 487 }
488 488
489 489 //--- MODULES ------------------//
490 490
491 491
492 492 // Server Announcement
493 493 #server-announcement {
494 494 width: 95%;
495 495 margin: @padding auto;
496 496 padding: @padding;
497 497 border-width: 2px;
498 498 border-style: solid;
499 499 .border-radius(2px);
500 500 font-weight: @text-bold-weight;
501 501 font-family: @text-bold;
502 502
503 503 &.info { border-color: @alert4; background-color: @alert4-inner; }
504 504 &.warning { border-color: @alert3; background-color: @alert3-inner; }
505 505 &.error { border-color: @alert2; background-color: @alert2-inner; }
506 506 &.success { border-color: @alert1; background-color: @alert1-inner; }
507 507 &.neutral { border-color: @grey3; background-color: @grey6; }
508 508 }
509 509
510 510 // Fixed Sidebar Column
511 511 .sidebar-col-wrapper {
512 512 padding-left: @sidebar-all-width;
513 513
514 514 .sidebar {
515 515 width: @sidebar-width;
516 516 margin-left: -@sidebar-all-width;
517 517 }
518 518 }
519 519
520 520 .sidebar-col-wrapper.scw-small {
521 521 padding-left: @sidebar-small-all-width;
522 522
523 523 .sidebar {
524 524 width: @sidebar-small-width;
525 525 margin-left: -@sidebar-small-all-width;
526 526 }
527 527 }
528 528
529 529
530 530 // FOOTER
531 531 #footer {
532 532 padding: 0;
533 533 text-align: center;
534 534 vertical-align: middle;
535 535 color: @grey2;
536 536 font-size: 11px;
537 537
538 538 p {
539 539 margin: 0;
540 540 padding: 1em;
541 541 line-height: 1em;
542 542 }
543 543
544 544 .server-instance { //server instance
545 545 display: none;
546 546 }
547 547
548 548 .title {
549 549 float: none;
550 550 margin: 0 auto;
551 551 }
552 552 }
553 553
554 554 button.close {
555 555 padding: 0;
556 556 cursor: pointer;
557 557 background: transparent;
558 558 border: 0;
559 559 .box-shadow(none);
560 560 -webkit-appearance: none;
561 561 }
562 562
563 563 .close {
564 564 float: right;
565 565 font-size: 21px;
566 566 font-family: @text-bootstrap;
567 567 line-height: 1em;
568 568 font-weight: bold;
569 569 color: @grey2;
570 570
571 571 &:hover,
572 572 &:focus {
573 573 color: @grey1;
574 574 text-decoration: none;
575 575 cursor: pointer;
576 576 }
577 577 }
578 578
579 579 // GRID
580 580 .sorting,
581 581 .sorting_desc,
582 582 .sorting_asc {
583 583 cursor: pointer;
584 584 }
585 585 .sorting_desc:after {
586 586 content: "\00A0\25B2";
587 587 font-size: .75em;
588 588 }
589 589 .sorting_asc:after {
590 590 content: "\00A0\25BC";
591 591 font-size: .68em;
592 592 }
593 593
594 594
595 595 .user_auth_tokens {
596 596
597 597 &.truncate {
598 598 white-space: nowrap;
599 599 overflow: hidden;
600 600 text-overflow: ellipsis;
601 601 }
602 602
603 603 .fields .field .input {
604 604 margin: 0;
605 605 }
606 606
607 607 input#description {
608 608 width: 100px;
609 609 margin: 0;
610 610 }
611 611
612 612 .drop-menu {
613 613 // TODO: johbo: Remove this, should work out of the box when
614 614 // having multiple inputs inline
615 615 margin: 0 0 0 5px;
616 616 }
617 617 }
618 618 #user_list_table {
619 619 .closed {
620 620 background-color: @grey6;
621 621 }
622 622 }
623 623
624 624
625 625 input, textarea {
626 626 &.disabled {
627 627 opacity: .5;
628 628 }
629 629
630 630 &:hover {
631 631 border-color: @grey3;
632 632 box-shadow: @button-shadow;
633 633 }
634 634
635 635 &:focus {
636 636 border-color: @rcblue;
637 637 box-shadow: @button-shadow;
638 638 }
639 639 }
640 640
641 641 // remove extra padding in firefox
642 642 input::-moz-focus-inner { border:0; padding:0 }
643 643
644 644 .adjacent input {
645 645 margin-bottom: @padding;
646 646 }
647 647
648 648 .permissions_boxes {
649 649 display: block;
650 650 }
651 651
652 652 //FORMS
653 653
654 654 .medium-inline,
655 655 input#description.medium-inline {
656 656 display: inline;
657 657 width: @medium-inline-input-width;
658 658 min-width: 100px;
659 659 }
660 660
661 661 select {
662 662 //reset
663 663 -webkit-appearance: none;
664 664 -moz-appearance: none;
665 665
666 666 display: inline-block;
667 667 height: 28px;
668 668 width: auto;
669 669 margin: 0 @padding @padding 0;
670 670 padding: 0 18px 0 8px;
671 671 line-height:1em;
672 672 font-size: @basefontsize;
673 673 border: @border-thickness solid @grey5;
674 674 border-radius: @border-radius;
675 675 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
676 676 color: @grey4;
677 677 box-shadow: @button-shadow;
678 678
679 679 &:after {
680 680 content: "\00A0\25BE";
681 681 }
682 682
683 683 &:focus, &:hover {
684 684 outline: none;
685 685 border-color: @grey4;
686 686 color: @rcdarkblue;
687 687 }
688 688 }
689 689
690 690 option {
691 691 &:focus {
692 692 outline: none;
693 693 }
694 694 }
695 695
696 696 input,
697 697 textarea {
698 698 padding: @input-padding;
699 699 border: @input-border-thickness solid @border-highlight-color;
700 700 .border-radius (@border-radius);
701 701 font-family: @text-light;
702 702 font-size: @basefontsize;
703 703
704 704 &.input-sm {
705 705 padding: 5px;
706 706 }
707 707
708 708 &#description {
709 709 min-width: @input-description-minwidth;
710 710 min-height: 1em;
711 711 padding: 10px;
712 712 }
713 713 }
714 714
715 715 .field-sm {
716 716 input,
717 717 textarea {
718 718 padding: 5px;
719 719 }
720 720 }
721 721
722 722 textarea {
723 723 display: block;
724 724 clear: both;
725 725 width: 100%;
726 726 min-height: 100px;
727 727 margin-bottom: @padding;
728 728 .box-sizing(border-box);
729 729 overflow: auto;
730 730 }
731 731
732 732 label {
733 733 font-family: @text-light;
734 734 }
735 735
736 736 // GRAVATARS
737 737 // centers gravatar on username to the right
738 738
739 739 .gravatar {
740 740 display: inline;
741 741 min-width: 16px;
742 742 min-height: 16px;
743 743 margin: -5px 0;
744 744 padding: 0;
745 745 line-height: 1em;
746 746 box-sizing: content-box;
747 747 border-radius: 50%;
748 748
749 749 &.gravatar-large {
750 750 margin: -0.5em .25em -0.5em 0;
751 751 }
752 752
753 753 & + .user {
754 754 display: inline;
755 755 margin: 0;
756 756 padding: 0 0 0 .17em;
757 757 line-height: 1em;
758 758 }
759 759 }
760 760
761 761 .user-inline-data {
762 762 display: inline-block;
763 763 float: left;
764 764 padding-left: .5em;
765 765 line-height: 1.3em;
766 766 }
767 767
768 768 .rc-user { // gravatar + user wrapper
769 769 float: left;
770 770 position: relative;
771 771 min-width: 100px;
772 772 max-width: 200px;
773 773 min-height: (@gravatar-size + @border-thickness * 2); // account for border
774 774 display: block;
775 775 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
776 776
777 777
778 778 .gravatar {
779 779 display: block;
780 780 position: absolute;
781 781 top: 0;
782 782 left: 0;
783 783 min-width: @gravatar-size;
784 784 min-height: @gravatar-size;
785 785 margin: 0;
786 786 }
787 787
788 788 .user {
789 789 display: block;
790 790 max-width: 175px;
791 791 padding-top: 2px;
792 792 overflow: hidden;
793 793 text-overflow: ellipsis;
794 794 }
795 795 }
796 796
797 797 .gist-gravatar,
798 798 .journal_container {
799 799 .gravatar-large {
800 800 margin: 0 .5em -10px 0;
801 801 }
802 802 }
803 803
804 804
805 805 // ADMIN SETTINGS
806 806
807 807 // Tag Patterns
808 808 .tag_patterns {
809 809 .tag_input {
810 810 margin-bottom: @padding;
811 811 }
812 812 }
813 813
814 814 .locked_input {
815 815 position: relative;
816 816
817 817 input {
818 818 display: inline;
819 819 margin: 3px 5px 0px 0px;
820 820 }
821 821
822 822 br {
823 823 display: none;
824 824 }
825 825
826 826 .error-message {
827 827 float: left;
828 828 width: 100%;
829 829 }
830 830
831 831 .lock_input_button {
832 832 display: inline;
833 833 }
834 834
835 835 .help-block {
836 836 clear: both;
837 837 }
838 838 }
839 839
840 840 // Notifications
841 841
842 842 .notifications_buttons {
843 843 margin: 0 0 @space 0;
844 844 padding: 0;
845 845
846 846 .btn {
847 847 display: inline-block;
848 848 }
849 849 }
850 850
851 851 .notification-list {
852 852
853 853 div {
854 854 display: inline-block;
855 855 vertical-align: middle;
856 856 }
857 857
858 858 .container {
859 859 display: block;
860 860 margin: 0 0 @padding 0;
861 861 }
862 862
863 863 .delete-notifications {
864 864 margin-left: @padding;
865 865 text-align: right;
866 866 cursor: pointer;
867 867 }
868 868
869 869 .read-notifications {
870 870 margin-left: @padding/2;
871 871 text-align: right;
872 872 width: 35px;
873 873 cursor: pointer;
874 874 }
875 875
876 876 .icon-minus-sign {
877 877 color: @alert2;
878 878 }
879 879
880 880 .icon-ok-sign {
881 881 color: @alert1;
882 882 }
883 883 }
884 884
885 885 .user_settings {
886 886 float: left;
887 887 clear: both;
888 888 display: block;
889 889 width: 100%;
890 890
891 891 .gravatar_box {
892 892 margin-bottom: @padding;
893 893
894 894 &:after {
895 895 content: " ";
896 896 clear: both;
897 897 width: 100%;
898 898 }
899 899 }
900 900
901 901 .fields .field {
902 902 clear: both;
903 903 }
904 904 }
905 905
906 906 .advanced_settings {
907 907 margin-bottom: @space;
908 908
909 909 .help-block {
910 910 margin-left: 0;
911 911 }
912 912
913 913 button + .help-block {
914 914 margin-top: @padding;
915 915 }
916 916 }
917 917
918 918 // admin settings radio buttons and labels
919 919 .label-2 {
920 920 float: left;
921 921 width: @label2-width;
922 922
923 923 label {
924 924 color: @grey1;
925 925 }
926 926 }
927 927 .checkboxes {
928 928 float: left;
929 929 width: @checkboxes-width;
930 930 margin-bottom: @padding;
931 931
932 932 .checkbox {
933 933 width: 100%;
934 934
935 935 label {
936 936 margin: 0;
937 937 padding: 0;
938 938 }
939 939 }
940 940
941 941 .checkbox + .checkbox {
942 942 display: inline-block;
943 943 }
944 944
945 945 label {
946 946 margin-right: 1em;
947 947 }
948 948 }
949 949
950 950 // CHANGELOG
951 951 .container_header {
952 952 float: left;
953 953 display: block;
954 954 width: 100%;
955 955 margin: @padding 0 @padding;
956 956
957 957 #filter_changelog {
958 958 float: left;
959 959 margin-right: @padding;
960 960 }
961 961
962 962 .breadcrumbs_light {
963 963 display: inline-block;
964 964 }
965 965 }
966 966
967 967 .info_box {
968 968 float: right;
969 969 }
970 970
971 971
972 972
973 973 #graph_content{
974 974
975 975 // adjust for table headers so that graph renders properly
976 976 // #graph_nodes padding - table cell padding
977 977 padding-top: (@space - (@basefontsize * 2.4));
978 978
979 979 &.graph_full_width {
980 980 width: 100%;
981 981 max-width: 100%;
982 982 }
983 983 }
984 984
985 985 #graph {
986 986
987 987 .pagination-left {
988 988 float: left;
989 989 clear: both;
990 990 }
991 991
992 992 .log-container {
993 993 max-width: 345px;
994 994
995 995 .message{
996 996 max-width: 340px;
997 997 }
998 998 }
999 999
1000 1000 .graph-col-wrapper {
1001 1001
1002 1002 #graph_nodes {
1003 1003 width: 100px;
1004 1004 position: absolute;
1005 1005 left: 70px;
1006 1006 z-index: -1;
1007 1007 }
1008 1008 }
1009 1009
1010 1010 .load-more-commits {
1011 1011 text-align: center;
1012 1012 }
1013 1013 .load-more-commits:hover {
1014 1014 background-color: @grey7;
1015 1015 }
1016 1016 .load-more-commits {
1017 1017 a {
1018 1018 display: block;
1019 1019 }
1020 1020 }
1021 1021 }
1022 1022
1023 1023 .obsolete-toggle {
1024 1024 line-height: 30px;
1025 1025 margin-left: -15px;
1026 1026 }
1027 1027
1028 1028 #rev_range_container, #rev_range_clear, #rev_range_more {
1029 1029 margin-top: -5px;
1030 1030 margin-bottom: -5px;
1031 1031 }
1032 1032
1033 1033 #filter_changelog {
1034 1034 float: left;
1035 1035 }
1036 1036
1037 1037
1038 1038 //--- THEME ------------------//
1039 1039
1040 1040 #logo {
1041 1041 float: left;
1042 1042 margin: 9px 0 0 0;
1043 1043
1044 1044 .header {
1045 1045 background-color: transparent;
1046 1046 }
1047 1047
1048 1048 a {
1049 1049 display: inline-block;
1050 1050 }
1051 1051
1052 1052 img {
1053 1053 height:30px;
1054 1054 }
1055 1055 }
1056 1056
1057 1057 .logo-wrapper {
1058 1058 float:left;
1059 1059 }
1060 1060
1061 1061 .branding {
1062 1062 float: left;
1063 1063 padding: 9px 2px;
1064 1064 line-height: 1em;
1065 1065 font-size: @navigation-fontsize;
1066 1066
1067 1067 a {
1068 1068 color: @grey5
1069 1069 }
1070 1070 @media screen and (max-width: 1200px) {
1071 1071 display: none;
1072 1072 }
1073 1073 }
1074 1074
1075 1075 img {
1076 1076 border: none;
1077 1077 outline: none;
1078 1078 }
1079 1079 user-profile-header
1080 1080 label {
1081 1081
1082 1082 input[type="checkbox"] {
1083 1083 margin-right: 1em;
1084 1084 }
1085 1085 input[type="radio"] {
1086 1086 margin-right: 1em;
1087 1087 }
1088 1088 }
1089 1089
1090 1090 .review-status {
1091 1091 &.under_review {
1092 1092 color: @alert3;
1093 1093 }
1094 1094 &.approved {
1095 1095 color: @alert1;
1096 1096 }
1097 1097 &.rejected,
1098 1098 &.forced_closed{
1099 1099 color: @alert2;
1100 1100 }
1101 1101 &.not_reviewed {
1102 1102 color: @grey5;
1103 1103 }
1104 1104 }
1105 1105
1106 1106 .review-status-under_review {
1107 1107 color: @alert3;
1108 1108 }
1109 1109 .status-tag-under_review {
1110 1110 border-color: @alert3;
1111 1111 }
1112 1112
1113 1113 .review-status-approved {
1114 1114 color: @alert1;
1115 1115 }
1116 1116 .status-tag-approved {
1117 1117 border-color: @alert1;
1118 1118 }
1119 1119
1120 1120 .review-status-rejected,
1121 1121 .review-status-forced_closed {
1122 1122 color: @alert2;
1123 1123 }
1124 1124 .status-tag-rejected,
1125 1125 .status-tag-forced_closed {
1126 1126 border-color: @alert2;
1127 1127 }
1128 1128
1129 1129 .review-status-not_reviewed {
1130 1130 color: @grey5;
1131 1131 }
1132 1132 .status-tag-not_reviewed {
1133 1133 border-color: @grey5;
1134 1134 }
1135 1135
1136 1136 .test_pattern_preview {
1137 1137 margin: @space 0;
1138 1138
1139 1139 p {
1140 1140 margin-bottom: 0;
1141 1141 border-bottom: @border-thickness solid @border-default-color;
1142 1142 color: @grey3;
1143 1143 }
1144 1144
1145 1145 .btn {
1146 1146 margin-bottom: @padding;
1147 1147 }
1148 1148 }
1149 1149 #test_pattern_result {
1150 1150 display: none;
1151 1151 &:extend(pre);
1152 1152 padding: .9em;
1153 1153 color: @grey3;
1154 1154 background-color: @grey7;
1155 1155 border-right: @border-thickness solid @border-default-color;
1156 1156 border-bottom: @border-thickness solid @border-default-color;
1157 1157 border-left: @border-thickness solid @border-default-color;
1158 1158 }
1159 1159
1160 1160 #repo_vcs_settings {
1161 1161 #inherit_overlay_vcs_default {
1162 1162 display: none;
1163 1163 }
1164 1164 #inherit_overlay_vcs_custom {
1165 1165 display: custom;
1166 1166 }
1167 1167 &.inherited {
1168 1168 #inherit_overlay_vcs_default {
1169 1169 display: block;
1170 1170 }
1171 1171 #inherit_overlay_vcs_custom {
1172 1172 display: none;
1173 1173 }
1174 1174 }
1175 1175 }
1176 1176
1177 1177 .issue-tracker-link {
1178 1178 color: @rcblue;
1179 1179 }
1180 1180
1181 1181 // Issue Tracker Table Show/Hide
1182 1182 #repo_issue_tracker {
1183 1183 #inherit_overlay {
1184 1184 display: none;
1185 1185 }
1186 1186 #custom_overlay {
1187 1187 display: custom;
1188 1188 }
1189 1189 &.inherited {
1190 1190 #inherit_overlay {
1191 1191 display: block;
1192 1192 }
1193 1193 #custom_overlay {
1194 1194 display: none;
1195 1195 }
1196 1196 }
1197 1197 }
1198 1198 table.issuetracker {
1199 1199 &.readonly {
1200 1200 tr, td {
1201 1201 color: @grey3;
1202 1202 }
1203 1203 }
1204 1204 .edit {
1205 1205 display: none;
1206 1206 }
1207 1207 .editopen {
1208 1208 .edit {
1209 1209 display: inline;
1210 1210 }
1211 1211 .entry {
1212 1212 display: none;
1213 1213 }
1214 1214 }
1215 1215 tr td.td-action {
1216 1216 min-width: 117px;
1217 1217 }
1218 1218 td input {
1219 1219 max-width: none;
1220 1220 min-width: 30px;
1221 1221 width: 80%;
1222 1222 }
1223 1223 .issuetracker_pref input {
1224 1224 width: 40%;
1225 1225 }
1226 1226 input.edit_issuetracker_update {
1227 1227 margin-right: 0;
1228 1228 width: auto;
1229 1229 }
1230 1230 }
1231 1231
1232 1232 table.integrations {
1233 1233 .td-icon {
1234 1234 width: 20px;
1235 1235 .integration-icon {
1236 1236 height: 20px;
1237 1237 width: 20px;
1238 1238 }
1239 1239 }
1240 1240 }
1241 1241
1242 1242 .integrations {
1243 1243 a.integration-box {
1244 1244 color: @text-color;
1245 1245 &:hover {
1246 1246 .panel {
1247 1247 background: #fbfbfb;
1248 1248 }
1249 1249 }
1250 1250 .integration-icon {
1251 1251 width: 30px;
1252 1252 height: 30px;
1253 1253 margin-right: 20px;
1254 1254 float: left;
1255 1255 }
1256 1256
1257 1257 .panel-body {
1258 1258 padding: 10px;
1259 1259 }
1260 1260 .panel {
1261 1261 margin-bottom: 10px;
1262 1262 }
1263 1263 h2 {
1264 1264 display: inline-block;
1265 1265 margin: 0;
1266 1266 min-width: 140px;
1267 1267 }
1268 1268 }
1269 1269 a.integration-box.dummy-integration {
1270 1270 color: @grey4
1271 1271 }
1272 1272 }
1273 1273
1274 1274 //Permissions Settings
1275 1275 #add_perm {
1276 1276 margin: 0 0 @padding;
1277 1277 cursor: pointer;
1278 1278 }
1279 1279
1280 1280 .perm_ac {
1281 1281 input {
1282 1282 width: 95%;
1283 1283 }
1284 1284 }
1285 1285
1286 1286 .autocomplete-suggestions {
1287 1287 width: auto !important; // overrides autocomplete.js
1288 1288 min-width: 278px;
1289 1289 margin: 0;
1290 1290 border: @border-thickness solid @grey5;
1291 1291 border-radius: @border-radius;
1292 1292 color: @grey2;
1293 1293 background-color: white;
1294 1294 }
1295 1295
1296 1296 .autocomplete-qfilter-suggestions {
1297 1297 width: auto !important; // overrides autocomplete.js
1298 1298 max-height: 100% !important;
1299 1299 min-width: 376px;
1300 1300 margin: 0;
1301 1301 border: @border-thickness solid @grey5;
1302 1302 color: @grey2;
1303 1303 background-color: white;
1304 1304 }
1305 1305
1306 1306 .autocomplete-selected {
1307 1307 background: #F0F0F0;
1308 1308 }
1309 1309
1310 1310 .ac-container-wrap {
1311 1311 margin: 0;
1312 1312 padding: 8px;
1313 1313 border-bottom: @border-thickness solid @grey5;
1314 1314 list-style-type: none;
1315 1315 cursor: pointer;
1316 1316
1317 1317 &:hover {
1318 1318 background-color: @grey7;
1319 1319 }
1320 1320
1321 1321 img {
1322 1322 height: @gravatar-size;
1323 1323 width: @gravatar-size;
1324 1324 margin-right: 1em;
1325 1325 }
1326 1326
1327 1327 strong {
1328 1328 font-weight: normal;
1329 1329 }
1330 1330 }
1331 1331
1332 1332 // Settings Dropdown
1333 1333 .user-menu .container {
1334 1334 padding: 0 4px;
1335 1335 margin: 0;
1336 1336 }
1337 1337
1338 1338 .user-menu .gravatar {
1339 1339 cursor: pointer;
1340 1340 }
1341 1341
1342 1342 .codeblock {
1343 1343 margin-bottom: @padding;
1344 1344 clear: both;
1345 1345
1346 1346 .stats {
1347 1347 overflow: hidden;
1348 1348 }
1349 1349
1350 1350 .message{
1351 1351 textarea{
1352 1352 margin: 0;
1353 1353 }
1354 1354 }
1355 1355
1356 1356 .code-header {
1357 1357 .stats {
1358 1358 line-height: 2em;
1359 1359
1360 1360 .revision_id {
1361 1361 margin-left: 0;
1362 1362 }
1363 1363 .buttons {
1364 1364 padding-right: 0;
1365 1365 }
1366 1366 }
1367 1367
1368 1368 .item{
1369 1369 margin-right: 0.5em;
1370 1370 }
1371 1371 }
1372 1372
1373 1373 #editor_container {
1374 1374 position: relative;
1375 1375 margin: @padding 10px;
1376 1376 }
1377 1377 }
1378 1378
1379 1379 #file_history_container {
1380 1380 display: none;
1381 1381 }
1382 1382
1383 1383 .file-history-inner {
1384 1384 margin-bottom: 10px;
1385 1385 }
1386 1386
1387 1387 // Pull Requests
1388 1388 .summary-details {
1389 1389 width: 72%;
1390 1390 }
1391 1391 .pr-summary {
1392 1392 border-bottom: @border-thickness solid @grey5;
1393 1393 margin-bottom: @space;
1394 1394 }
1395 1395 .reviewers-title {
1396 1396 width: 25%;
1397 1397 min-width: 200px;
1398 1398 }
1399 1399 .reviewers {
1400 1400 width: 25%;
1401 1401 min-width: 200px;
1402 1402 }
1403 1403 .reviewers ul li {
1404 1404 position: relative;
1405 1405 width: 100%;
1406 1406 padding-bottom: 8px;
1407 1407 list-style-type: none;
1408 1408 }
1409 1409
1410 1410 .reviewer_entry {
1411 1411 min-height: 55px;
1412 1412 }
1413 1413
1414 1414 .reviewers_member {
1415 1415 width: 100%;
1416 1416 overflow: auto;
1417 1417 }
1418 1418 .reviewer_reason {
1419 1419 padding-left: 20px;
1420 1420 line-height: 1.5em;
1421 1421 }
1422 1422 .reviewer_status {
1423 1423 display: inline-block;
1424 1424 vertical-align: top;
1425 1425 width: 25px;
1426 1426 min-width: 25px;
1427 1427 height: 1.2em;
1428 1428 margin-top: 3px;
1429 1429 line-height: 1em;
1430 1430 }
1431 1431
1432 1432 .reviewer_name {
1433 1433 display: inline-block;
1434 1434 max-width: 83%;
1435 1435 padding-right: 20px;
1436 1436 vertical-align: middle;
1437 1437 line-height: 1;
1438 1438
1439 1439 .rc-user {
1440 1440 min-width: 0;
1441 1441 margin: -2px 1em 0 0;
1442 1442 }
1443 1443
1444 1444 .reviewer {
1445 1445 float: left;
1446 1446 }
1447 1447 }
1448 1448
1449 1449 .reviewer_member_mandatory {
1450 1450 position: absolute;
1451 1451 left: 15px;
1452 1452 top: 8px;
1453 1453 width: 16px;
1454 1454 font-size: 11px;
1455 1455 margin: 0;
1456 1456 padding: 0;
1457 1457 color: black;
1458 1458 }
1459 1459
1460 1460 .reviewer_member_mandatory_remove,
1461 1461 .reviewer_member_remove {
1462 1462 position: absolute;
1463 1463 right: 0;
1464 1464 top: 0;
1465 1465 width: 16px;
1466 1466 margin-bottom: 10px;
1467 1467 padding: 0;
1468 1468 color: black;
1469 1469 }
1470 1470
1471 1471 .reviewer_member_mandatory_remove {
1472 1472 color: @grey4;
1473 1473 }
1474 1474
1475 1475 .reviewer_member_status {
1476 1476 margin-top: 5px;
1477 1477 }
1478 1478 .pr-summary #summary{
1479 1479 width: 100%;
1480 1480 }
1481 1481 .pr-summary .action_button:hover {
1482 1482 border: 0;
1483 1483 cursor: pointer;
1484 1484 }
1485 1485 .pr-details-title {
1486 1486 padding-bottom: 8px;
1487 1487 border-bottom: @border-thickness solid @grey5;
1488 1488
1489 1489 .action_button.disabled {
1490 1490 color: @grey4;
1491 1491 cursor: inherit;
1492 1492 }
1493 1493 .action_button {
1494 1494 color: @rcblue;
1495 1495 }
1496 1496 }
1497 1497 .pr-details-content {
1498 1498 margin-top: @textmargin;
1499 1499 margin-bottom: @textmargin;
1500 1500 }
1501 1501
1502 1502 .pr-reviewer-rules {
1503 1503 padding: 10px 0px 20px 0px;
1504 1504 }
1505 1505
1506 1506 .group_members {
1507 1507 margin-top: 0;
1508 1508 padding: 0;
1509 1509 list-style: outside none none;
1510 1510
1511 1511 img {
1512 1512 height: @gravatar-size;
1513 1513 width: @gravatar-size;
1514 1514 margin-right: .5em;
1515 1515 margin-left: 3px;
1516 1516 }
1517 1517
1518 1518 .to-delete {
1519 1519 .user {
1520 1520 text-decoration: line-through;
1521 1521 }
1522 1522 }
1523 1523 }
1524 1524
1525 1525 .compare_view_commits_title {
1526 1526 .disabled {
1527 1527 cursor: inherit;
1528 1528 &:hover{
1529 1529 background-color: inherit;
1530 1530 color: inherit;
1531 1531 }
1532 1532 }
1533 1533 }
1534 1534
1535 1535 .subtitle-compare {
1536 1536 margin: -15px 0px 0px 0px;
1537 1537 }
1538 1538
1539 .comments-summary-td {
1540 border-top: 1px dashed @grey5;
1541 }
1542
1543 1539 // new entry in group_members
1544 1540 .td-author-new-entry {
1545 1541 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1546 1542 }
1547 1543
1548 1544 .usergroup_member_remove {
1549 1545 width: 16px;
1550 1546 margin-bottom: 10px;
1551 1547 padding: 0;
1552 1548 color: black !important;
1553 1549 cursor: pointer;
1554 1550 }
1555 1551
1556 1552 .reviewer_ac .ac-input {
1557 1553 width: 92%;
1558 1554 margin-bottom: 1em;
1559 1555 }
1560 1556
1561 1557 .compare_view_commits tr{
1562 1558 height: 20px;
1563 1559 }
1564 1560 .compare_view_commits td {
1565 1561 vertical-align: top;
1566 1562 padding-top: 10px;
1567 1563 }
1568 1564 .compare_view_commits .author {
1569 1565 margin-left: 5px;
1570 1566 }
1571 1567
1572 1568 .compare_view_commits {
1573 1569 .color-a {
1574 1570 color: @alert1;
1575 1571 }
1576 1572
1577 1573 .color-c {
1578 1574 color: @color3;
1579 1575 }
1580 1576
1581 1577 .color-r {
1582 1578 color: @color5;
1583 1579 }
1584 1580
1585 1581 .color-a-bg {
1586 1582 background-color: @alert1;
1587 1583 }
1588 1584
1589 1585 .color-c-bg {
1590 1586 background-color: @alert3;
1591 1587 }
1592 1588
1593 1589 .color-r-bg {
1594 1590 background-color: @alert2;
1595 1591 }
1596 1592
1597 1593 .color-a-border {
1598 1594 border: 1px solid @alert1;
1599 1595 }
1600 1596
1601 1597 .color-c-border {
1602 1598 border: 1px solid @alert3;
1603 1599 }
1604 1600
1605 1601 .color-r-border {
1606 1602 border: 1px solid @alert2;
1607 1603 }
1608 1604
1609 1605 .commit-change-indicator {
1610 1606 width: 15px;
1611 1607 height: 15px;
1612 1608 position: relative;
1613 1609 left: 15px;
1614 1610 }
1615 1611
1616 1612 .commit-change-content {
1617 1613 text-align: center;
1618 1614 vertical-align: middle;
1619 1615 line-height: 15px;
1620 1616 }
1621 1617 }
1622 1618
1623 1619 .compare_view_filepath {
1624 1620 color: @grey1;
1625 1621 }
1626 1622
1627 1623 .show_more {
1628 1624 display: inline-block;
1629 1625 width: 0;
1630 1626 height: 0;
1631 1627 vertical-align: middle;
1632 1628 content: "";
1633 1629 border: 4px solid;
1634 1630 border-right-color: transparent;
1635 1631 border-bottom-color: transparent;
1636 1632 border-left-color: transparent;
1637 1633 font-size: 0;
1638 1634 }
1639 1635
1640 1636 .journal_more .show_more {
1641 1637 display: inline;
1642 1638
1643 1639 &:after {
1644 1640 content: none;
1645 1641 }
1646 1642 }
1647 1643
1648 1644 .compare_view_commits .collapse_commit:after {
1649 1645 cursor: pointer;
1650 1646 content: "\00A0\25B4";
1651 1647 margin-left: -3px;
1652 1648 font-size: 17px;
1653 1649 color: @grey4;
1654 1650 }
1655 1651
1656 1652 .diff_links {
1657 1653 margin-left: 8px;
1658 1654 }
1659 1655
1660 1656 #pull_request_overview {
1661 1657 div.ancestor {
1662 1658 margin: -33px 0;
1663 1659 }
1664 1660 }
1665 1661
1666 1662 div.ancestor {
1667 1663 line-height: 33px;
1668 1664 }
1669 1665
1670 1666 .cs_icon_td input[type="checkbox"] {
1671 1667 display: none;
1672 1668 }
1673 1669
1674 1670 .cs_icon_td .expand_file_icon:after {
1675 1671 cursor: pointer;
1676 1672 content: "\00A0\25B6";
1677 1673 font-size: 12px;
1678 1674 color: @grey4;
1679 1675 }
1680 1676
1681 1677 .cs_icon_td .collapse_file_icon:after {
1682 1678 cursor: pointer;
1683 1679 content: "\00A0\25BC";
1684 1680 font-size: 12px;
1685 1681 color: @grey4;
1686 1682 }
1687 1683
1688 1684 /*new binary
1689 1685 NEW_FILENODE = 1
1690 1686 DEL_FILENODE = 2
1691 1687 MOD_FILENODE = 3
1692 1688 RENAMED_FILENODE = 4
1693 1689 COPIED_FILENODE = 5
1694 1690 CHMOD_FILENODE = 6
1695 1691 BIN_FILENODE = 7
1696 1692 */
1697 1693 .cs_files_expand {
1698 1694 font-size: @basefontsize + 5px;
1699 1695 line-height: 1.8em;
1700 1696 float: right;
1701 1697 }
1702 1698
1703 1699 .cs_files_expand span{
1704 1700 color: @rcblue;
1705 1701 cursor: pointer;
1706 1702 }
1707 1703 .cs_files {
1708 1704 clear: both;
1709 1705 padding-bottom: @padding;
1710 1706
1711 1707 .cur_cs {
1712 1708 margin: 10px 2px;
1713 1709 font-weight: bold;
1714 1710 }
1715 1711
1716 1712 .node {
1717 1713 float: left;
1718 1714 }
1719 1715
1720 1716 .changes {
1721 1717 float: right;
1722 1718 color: white;
1723 1719 font-size: @basefontsize - 4px;
1724 1720 margin-top: 4px;
1725 1721 opacity: 0.6;
1726 1722 filter: Alpha(opacity=60); /* IE8 and earlier */
1727 1723
1728 1724 .added {
1729 1725 background-color: @alert1;
1730 1726 float: left;
1731 1727 text-align: center;
1732 1728 }
1733 1729
1734 1730 .deleted {
1735 1731 background-color: @alert2;
1736 1732 float: left;
1737 1733 text-align: center;
1738 1734 }
1739 1735
1740 1736 .bin {
1741 1737 background-color: @alert1;
1742 1738 text-align: center;
1743 1739 }
1744 1740
1745 1741 /*new binary*/
1746 1742 .bin.bin1 {
1747 1743 background-color: @alert1;
1748 1744 text-align: center;
1749 1745 }
1750 1746
1751 1747 /*deleted binary*/
1752 1748 .bin.bin2 {
1753 1749 background-color: @alert2;
1754 1750 text-align: center;
1755 1751 }
1756 1752
1757 1753 /*mod binary*/
1758 1754 .bin.bin3 {
1759 1755 background-color: @grey2;
1760 1756 text-align: center;
1761 1757 }
1762 1758
1763 1759 /*rename file*/
1764 1760 .bin.bin4 {
1765 1761 background-color: @alert4;
1766 1762 text-align: center;
1767 1763 }
1768 1764
1769 1765 /*copied file*/
1770 1766 .bin.bin5 {
1771 1767 background-color: @alert4;
1772 1768 text-align: center;
1773 1769 }
1774 1770
1775 1771 /*chmod file*/
1776 1772 .bin.bin6 {
1777 1773 background-color: @grey2;
1778 1774 text-align: center;
1779 1775 }
1780 1776 }
1781 1777 }
1782 1778
1783 1779 .cs_files .cs_added, .cs_files .cs_A,
1784 1780 .cs_files .cs_added, .cs_files .cs_M,
1785 1781 .cs_files .cs_added, .cs_files .cs_D {
1786 1782 height: 16px;
1787 1783 padding-right: 10px;
1788 1784 margin-top: 7px;
1789 1785 text-align: left;
1790 1786 }
1791 1787
1792 1788 .cs_icon_td {
1793 1789 min-width: 16px;
1794 1790 width: 16px;
1795 1791 }
1796 1792
1797 1793 .pull-request-merge {
1798 1794 border: 1px solid @grey5;
1799 1795 padding: 10px 0px 20px;
1800 1796 margin-top: 10px;
1801 1797 margin-bottom: 20px;
1802 1798 }
1803 1799
1804 1800 .pull-request-merge ul {
1805 1801 padding: 0px 0px;
1806 1802 }
1807 1803
1808 1804 .pull-request-merge li {
1809 1805 list-style-type: none;
1810 1806 }
1811 1807
1812 1808 .pull-request-merge .pull-request-wrap {
1813 1809 height: auto;
1814 1810 padding: 0px 0px;
1815 1811 text-align: right;
1816 1812 }
1817 1813
1818 1814 .pull-request-merge span {
1819 1815 margin-right: 5px;
1820 1816 }
1821 1817
1822 1818 .pull-request-merge-actions {
1823 1819 min-height: 30px;
1824 1820 padding: 0px 0px;
1825 1821 }
1826 1822
1827 1823 .pull-request-merge-info {
1828 1824 padding: 0px 5px 5px 0px;
1829 1825 }
1830 1826
1831 1827 .merge-status {
1832 1828 margin-right: 5px;
1833 1829 }
1834 1830
1835 1831 .merge-message {
1836 1832 font-size: 1.2em
1837 1833 }
1838 1834
1839 1835 .merge-message.success i,
1840 1836 .merge-icon.success i {
1841 1837 color:@alert1;
1842 1838 }
1843 1839
1844 1840 .merge-message.warning i,
1845 1841 .merge-icon.warning i {
1846 1842 color: @alert3;
1847 1843 }
1848 1844
1849 1845 .merge-message.error i,
1850 1846 .merge-icon.error i {
1851 1847 color:@alert2;
1852 1848 }
1853 1849
1854 1850 .pr-versions {
1855 1851 font-size: 1.1em;
1856 1852
1857 1853 table {
1858 1854 padding: 0px 5px;
1859 1855 }
1860 1856
1861 1857 td {
1862 1858 line-height: 15px;
1863 1859 }
1864 1860
1865 1861 .compare-radio-button {
1866 1862 position: relative;
1867 1863 top: -3px;
1868 1864 }
1869 1865 }
1870 1866
1871 1867
1872 1868 #close_pull_request {
1873 1869 margin-right: 0px;
1874 1870 }
1875 1871
1876 1872 .empty_data {
1877 1873 color: @grey4;
1878 1874 }
1879 1875
1880 1876 #changeset_compare_view_content {
1881 1877 clear: both;
1882 1878 width: 100%;
1883 1879 box-sizing: border-box;
1884 1880 .border-radius(@border-radius);
1885 1881
1886 1882 .help-block {
1887 1883 margin: @padding 0;
1888 1884 color: @text-color;
1889 1885 &.pre-formatting {
1890 1886 white-space: pre;
1891 1887 }
1892 1888 }
1893 1889
1894 1890 .empty_data {
1895 1891 margin: @padding 0;
1896 1892 }
1897 1893
1898 1894 .alert {
1899 1895 margin-bottom: @space;
1900 1896 }
1901 1897 }
1902 1898
1903 1899 .table_disp {
1904 1900 .status {
1905 1901 width: auto;
1906 1902 }
1907 1903 }
1908 1904
1909 1905
1910 1906 .creation_in_progress {
1911 1907 color: @grey4
1912 1908 }
1913 1909
1914 1910 .status_box_menu {
1915 1911 margin: 0;
1916 1912 }
1917 1913
1918 1914 .notification-table{
1919 1915 margin-bottom: @space;
1920 1916 display: table;
1921 1917 width: 100%;
1922 1918
1923 1919 .container{
1924 1920 display: table-row;
1925 1921
1926 1922 .notification-header{
1927 1923 border-bottom: @border-thickness solid @border-default-color;
1928 1924 }
1929 1925
1930 1926 .notification-subject{
1931 1927 display: table-cell;
1932 1928 }
1933 1929 }
1934 1930 }
1935 1931
1936 1932 // Notifications
1937 1933 .notification-header{
1938 1934 display: table;
1939 1935 width: 100%;
1940 1936 padding: floor(@basefontsize/2) 0;
1941 1937 line-height: 1em;
1942 1938
1943 1939 .desc, .delete-notifications, .read-notifications{
1944 1940 display: table-cell;
1945 1941 text-align: left;
1946 1942 }
1947 1943
1948 1944 .desc{
1949 1945 width: 1163px;
1950 1946 }
1951 1947
1952 1948 .delete-notifications, .read-notifications{
1953 1949 width: 35px;
1954 1950 min-width: 35px; //fixes when only one button is displayed
1955 1951 }
1956 1952 }
1957 1953
1958 1954 .notification-body {
1959 1955 .markdown-block,
1960 1956 .rst-block {
1961 1957 padding: @padding 0;
1962 1958 }
1963 1959
1964 1960 .notification-subject {
1965 1961 padding: @textmargin 0;
1966 1962 border-bottom: @border-thickness solid @border-default-color;
1967 1963 }
1968 1964 }
1969 1965
1970 1966
1971 1967 .notifications_buttons{
1972 1968 float: right;
1973 1969 }
1974 1970
1975 1971 #notification-status{
1976 1972 display: inline;
1977 1973 }
1978 1974
1979 1975 // Repositories
1980 1976
1981 1977 #summary.fields{
1982 1978 display: table;
1983 1979
1984 1980 .field{
1985 1981 display: table-row;
1986 1982
1987 1983 .label-summary{
1988 1984 display: table-cell;
1989 1985 min-width: @label-summary-minwidth;
1990 1986 padding-top: @padding/2;
1991 1987 padding-bottom: @padding/2;
1992 1988 padding-right: @padding/2;
1993 1989 }
1994 1990
1995 1991 .input{
1996 1992 display: table-cell;
1997 1993 padding: @padding/2;
1998 1994
1999 1995 input{
2000 1996 min-width: 29em;
2001 1997 padding: @padding/4;
2002 1998 }
2003 1999 }
2004 2000 .statistics, .downloads{
2005 2001 .disabled{
2006 2002 color: @grey4;
2007 2003 }
2008 2004 }
2009 2005 }
2010 2006 }
2011 2007
2012 2008 #summary{
2013 2009 width: 70%;
2014 2010 }
2015 2011
2016 2012
2017 2013 // Journal
2018 2014 .journal.title {
2019 2015 h5 {
2020 2016 float: left;
2021 2017 margin: 0;
2022 2018 width: 70%;
2023 2019 }
2024 2020
2025 2021 ul {
2026 2022 float: right;
2027 2023 display: inline-block;
2028 2024 margin: 0;
2029 2025 width: 30%;
2030 2026 text-align: right;
2031 2027
2032 2028 li {
2033 2029 display: inline;
2034 2030 font-size: @journal-fontsize;
2035 2031 line-height: 1em;
2036 2032
2037 2033 list-style-type: none;
2038 2034 }
2039 2035 }
2040 2036 }
2041 2037
2042 2038 .filterexample {
2043 2039 position: absolute;
2044 2040 top: 95px;
2045 2041 left: @contentpadding;
2046 2042 color: @rcblue;
2047 2043 font-size: 11px;
2048 2044 font-family: @text-regular;
2049 2045 cursor: help;
2050 2046
2051 2047 &:hover {
2052 2048 color: @rcdarkblue;
2053 2049 }
2054 2050
2055 2051 @media (max-width:768px) {
2056 2052 position: relative;
2057 2053 top: auto;
2058 2054 left: auto;
2059 2055 display: block;
2060 2056 }
2061 2057 }
2062 2058
2063 2059
2064 2060 #journal{
2065 2061 margin-bottom: @space;
2066 2062
2067 2063 .journal_day{
2068 2064 margin-bottom: @textmargin/2;
2069 2065 padding-bottom: @textmargin/2;
2070 2066 font-size: @journal-fontsize;
2071 2067 border-bottom: @border-thickness solid @border-default-color;
2072 2068 }
2073 2069
2074 2070 .journal_container{
2075 2071 margin-bottom: @space;
2076 2072
2077 2073 .journal_user{
2078 2074 display: inline-block;
2079 2075 }
2080 2076 .journal_action_container{
2081 2077 display: block;
2082 2078 margin-top: @textmargin;
2083 2079
2084 2080 div{
2085 2081 display: inline;
2086 2082 }
2087 2083
2088 2084 div.journal_action_params{
2089 2085 display: block;
2090 2086 }
2091 2087
2092 2088 div.journal_repo:after{
2093 2089 content: "\A";
2094 2090 white-space: pre;
2095 2091 }
2096 2092
2097 2093 div.date{
2098 2094 display: block;
2099 2095 margin-bottom: @textmargin;
2100 2096 }
2101 2097 }
2102 2098 }
2103 2099 }
2104 2100
2105 2101 // Files
2106 2102 .edit-file-title {
2107 2103 font-size: 16px;
2108 2104
2109 2105 .title-heading {
2110 2106 padding: 2px;
2111 2107 }
2112 2108 }
2113 2109
2114 2110 .edit-file-fieldset {
2115 2111 margin: @sidebarpadding 0;
2116 2112
2117 2113 .fieldset {
2118 2114 .left-label {
2119 2115 width: 13%;
2120 2116 }
2121 2117 .right-content {
2122 2118 width: 87%;
2123 2119 max-width: 100%;
2124 2120 }
2125 2121 .filename-label {
2126 2122 margin-top: 13px;
2127 2123 }
2128 2124 .commit-message-label {
2129 2125 margin-top: 4px;
2130 2126 }
2131 2127 .file-upload-input {
2132 2128 input {
2133 2129 display: none;
2134 2130 }
2135 2131 margin-top: 10px;
2136 2132 }
2137 2133 .file-upload-label {
2138 2134 margin-top: 10px;
2139 2135 }
2140 2136 p {
2141 2137 margin-top: 5px;
2142 2138 }
2143 2139
2144 2140 }
2145 2141 .custom-path-link {
2146 2142 margin-left: 5px;
2147 2143 }
2148 2144 #commit {
2149 2145 resize: vertical;
2150 2146 }
2151 2147 }
2152 2148
2153 2149 .delete-file-preview {
2154 2150 max-height: 250px;
2155 2151 }
2156 2152
2157 2153 .new-file,
2158 2154 #filter_activate,
2159 2155 #filter_deactivate {
2160 2156 float: right;
2161 2157 margin: 0 0 0 10px;
2162 2158 }
2163 2159
2164 2160 .file-upload-transaction-wrapper {
2165 2161 margin-top: 57px;
2166 2162 clear: both;
2167 2163 }
2168 2164
2169 2165 .file-upload-transaction-wrapper .error {
2170 2166 color: @color5;
2171 2167 }
2172 2168
2173 2169 .file-upload-transaction {
2174 2170 min-height: 200px;
2175 2171 padding: 54px;
2176 2172 border: 1px solid @grey5;
2177 2173 text-align: center;
2178 2174 clear: both;
2179 2175 }
2180 2176
2181 2177 .file-upload-transaction i {
2182 2178 font-size: 48px
2183 2179 }
2184 2180
2185 2181 h3.files_location{
2186 2182 line-height: 2.4em;
2187 2183 }
2188 2184
2189 2185 .browser-nav {
2190 2186 width: 100%;
2191 2187 display: table;
2192 2188 margin-bottom: 20px;
2193 2189
2194 2190 .info_box {
2195 2191 float: left;
2196 2192 display: inline-table;
2197 2193 height: 2.5em;
2198 2194
2199 2195 .browser-cur-rev, .info_box_elem {
2200 2196 display: table-cell;
2201 2197 vertical-align: middle;
2202 2198 }
2203 2199
2204 2200 .drop-menu {
2205 2201 margin: 0 10px;
2206 2202 }
2207 2203
2208 2204 .info_box_elem {
2209 2205 border-top: @border-thickness solid @grey5;
2210 2206 border-bottom: @border-thickness solid @grey5;
2211 2207 box-shadow: @button-shadow;
2212 2208
2213 2209 #at_rev, a {
2214 2210 padding: 0.6em 0.4em;
2215 2211 margin: 0;
2216 2212 .box-shadow(none);
2217 2213 border: 0;
2218 2214 height: 12px;
2219 2215 color: @grey2;
2220 2216 }
2221 2217
2222 2218 input#at_rev {
2223 2219 max-width: 50px;
2224 2220 text-align: center;
2225 2221 }
2226 2222
2227 2223 &.previous {
2228 2224 border: @border-thickness solid @grey5;
2229 2225 border-top-left-radius: @border-radius;
2230 2226 border-bottom-left-radius: @border-radius;
2231 2227
2232 2228 &:hover {
2233 2229 border-color: @grey4;
2234 2230 }
2235 2231
2236 2232 .disabled {
2237 2233 color: @grey5;
2238 2234 cursor: not-allowed;
2239 2235 opacity: 0.5;
2240 2236 }
2241 2237 }
2242 2238
2243 2239 &.next {
2244 2240 border: @border-thickness solid @grey5;
2245 2241 border-top-right-radius: @border-radius;
2246 2242 border-bottom-right-radius: @border-radius;
2247 2243
2248 2244 &:hover {
2249 2245 border-color: @grey4;
2250 2246 }
2251 2247
2252 2248 .disabled {
2253 2249 color: @grey5;
2254 2250 cursor: not-allowed;
2255 2251 opacity: 0.5;
2256 2252 }
2257 2253 }
2258 2254 }
2259 2255
2260 2256 .browser-cur-rev {
2261 2257
2262 2258 span{
2263 2259 margin: 0;
2264 2260 color: @rcblue;
2265 2261 height: 12px;
2266 2262 display: inline-block;
2267 2263 padding: 0.7em 1em ;
2268 2264 border: @border-thickness solid @rcblue;
2269 2265 margin-right: @padding;
2270 2266 }
2271 2267 }
2272 2268
2273 2269 }
2274 2270
2275 2271 .select-index-number {
2276 2272 margin: 0 0 0 20px;
2277 2273 color: @grey3;
2278 2274 }
2279 2275
2280 2276 .search_activate {
2281 2277 display: table-cell;
2282 2278 vertical-align: middle;
2283 2279
2284 2280 input, label{
2285 2281 margin: 0;
2286 2282 padding: 0;
2287 2283 }
2288 2284
2289 2285 input{
2290 2286 margin-left: @textmargin;
2291 2287 }
2292 2288
2293 2289 }
2294 2290 }
2295 2291
2296 2292 .browser-cur-rev{
2297 2293 margin-bottom: @textmargin;
2298 2294 }
2299 2295
2300 2296 #node_filter_box_loading{
2301 2297 .info_text;
2302 2298 }
2303 2299
2304 2300 .browser-search {
2305 2301 margin: -25px 0px 5px 0px;
2306 2302 }
2307 2303
2308 2304 .files-quick-filter {
2309 2305 float: right;
2310 2306 width: 180px;
2311 2307 position: relative;
2312 2308 }
2313 2309
2314 2310 .files-filter-box {
2315 2311 display: flex;
2316 2312 padding: 0px;
2317 2313 border-radius: 3px;
2318 2314 margin-bottom: 0;
2319 2315
2320 2316 a {
2321 2317 border: none !important;
2322 2318 }
2323 2319
2324 2320 li {
2325 2321 list-style-type: none
2326 2322 }
2327 2323 }
2328 2324
2329 2325 .files-filter-box-path {
2330 2326 line-height: 33px;
2331 2327 padding: 0;
2332 2328 width: 20px;
2333 2329 position: absolute;
2334 2330 z-index: 11;
2335 2331 left: 5px;
2336 2332 }
2337 2333
2338 2334 .files-filter-box-input {
2339 2335 margin-right: 0;
2340 2336
2341 2337 input {
2342 2338 border: 1px solid @white;
2343 2339 padding-left: 25px;
2344 2340 width: 145px;
2345 2341
2346 2342 &:hover {
2347 2343 border-color: @grey6;
2348 2344 }
2349 2345
2350 2346 &:focus {
2351 2347 border-color: @grey5;
2352 2348 }
2353 2349 }
2354 2350 }
2355 2351
2356 2352 .browser-result{
2357 2353 td a{
2358 2354 margin-left: 0.5em;
2359 2355 display: inline-block;
2360 2356
2361 2357 em {
2362 2358 font-weight: @text-bold-weight;
2363 2359 font-family: @text-bold;
2364 2360 }
2365 2361 }
2366 2362 }
2367 2363
2368 2364 .browser-highlight{
2369 2365 background-color: @grey5-alpha;
2370 2366 }
2371 2367
2372 2368
2373 2369 .edit-file-fieldset #location,
2374 2370 .edit-file-fieldset #filename {
2375 2371 display: flex;
2376 2372 width: -moz-available; /* WebKit-based browsers will ignore this. */
2377 2373 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2378 2374 width: fill-available;
2379 2375 border: 0;
2380 2376 }
2381 2377
2382 2378 .path-items {
2383 2379 display: flex;
2384 2380 padding: 0;
2385 2381 border: 1px solid #eeeeee;
2386 2382 width: 100%;
2387 2383 float: left;
2388 2384
2389 2385 .breadcrumb-path {
2390 2386 line-height: 30px;
2391 2387 padding: 0 4px;
2392 2388 white-space: nowrap;
2393 2389 }
2394 2390
2395 2391 .location-path {
2396 2392 width: -moz-available; /* WebKit-based browsers will ignore this. */
2397 2393 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2398 2394 width: fill-available;
2399 2395
2400 2396 .file-name-input {
2401 2397 padding: 0.5em 0;
2402 2398 }
2403 2399
2404 2400 }
2405 2401
2406 2402 ul {
2407 2403 display: flex;
2408 2404 margin: 0;
2409 2405 padding: 0;
2410 2406 width: 100%;
2411 2407 }
2412 2408
2413 2409 li {
2414 2410 list-style-type: none;
2415 2411 }
2416 2412
2417 2413 }
2418 2414
2419 2415 .editor-items {
2420 2416 height: 40px;
2421 2417 margin: 10px 0 -17px 10px;
2422 2418
2423 2419 .editor-action {
2424 2420 cursor: pointer;
2425 2421 }
2426 2422
2427 2423 .editor-action.active {
2428 2424 border-bottom: 2px solid #5C5C5C;
2429 2425 }
2430 2426
2431 2427 li {
2432 2428 list-style-type: none;
2433 2429 }
2434 2430 }
2435 2431
2436 2432 .edit-file-fieldset .message textarea {
2437 2433 border: 1px solid #eeeeee;
2438 2434 }
2439 2435
2440 2436 #files_data .codeblock {
2441 2437 background-color: #F5F5F5;
2442 2438 }
2443 2439
2444 2440 #editor_preview {
2445 2441 background: white;
2446 2442 }
2447 2443
2448 2444 .show-editor {
2449 2445 padding: 10px;
2450 2446 background-color: white;
2451 2447
2452 2448 }
2453 2449
2454 2450 .show-preview {
2455 2451 padding: 10px;
2456 2452 background-color: white;
2457 2453 border-left: 1px solid #eeeeee;
2458 2454 }
2459 2455 // quick filter
2460 2456 .grid-quick-filter {
2461 2457 float: right;
2462 2458 position: relative;
2463 2459 }
2464 2460
2465 2461 .grid-filter-box {
2466 2462 display: flex;
2467 2463 padding: 0px;
2468 2464 border-radius: 3px;
2469 2465 margin-bottom: 0;
2470 2466
2471 2467 a {
2472 2468 border: none !important;
2473 2469 }
2474 2470
2475 2471 li {
2476 2472 list-style-type: none
2477 2473 }
2478 2474 }
2479 2475
2480 2476 .grid-filter-box-icon {
2481 2477 line-height: 33px;
2482 2478 padding: 0;
2483 2479 width: 20px;
2484 2480 position: absolute;
2485 2481 z-index: 11;
2486 2482 left: 5px;
2487 2483 }
2488 2484
2489 2485 .grid-filter-box-input {
2490 2486 margin-right: 0;
2491 2487
2492 2488 input {
2493 2489 border: 1px solid @white;
2494 2490 padding-left: 25px;
2495 2491 width: 145px;
2496 2492
2497 2493 &:hover {
2498 2494 border-color: @grey6;
2499 2495 }
2500 2496
2501 2497 &:focus {
2502 2498 border-color: @grey5;
2503 2499 }
2504 2500 }
2505 2501 }
2506 2502
2507 2503
2508 2504
2509 2505 // Search
2510 2506
2511 2507 .search-form{
2512 2508 #q {
2513 2509 width: @search-form-width;
2514 2510 }
2515 2511 .fields{
2516 2512 margin: 0 0 @space;
2517 2513 }
2518 2514
2519 2515 label{
2520 2516 display: inline-block;
2521 2517 margin-right: @textmargin;
2522 2518 padding-top: 0.25em;
2523 2519 }
2524 2520
2525 2521
2526 2522 .results{
2527 2523 clear: both;
2528 2524 margin: 0 0 @padding;
2529 2525 }
2530 2526
2531 2527 .search-tags {
2532 2528 padding: 5px 0;
2533 2529 }
2534 2530 }
2535 2531
2536 2532 div.search-feedback-items {
2537 2533 display: inline-block;
2538 2534 }
2539 2535
2540 2536 div.search-code-body {
2541 2537 background-color: #ffffff; padding: 5px 0 5px 10px;
2542 2538 pre {
2543 2539 .match { background-color: #faffa6;}
2544 2540 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2545 2541 }
2546 2542 }
2547 2543
2548 2544 .expand_commit.search {
2549 2545 .show_more.open {
2550 2546 height: auto;
2551 2547 max-height: none;
2552 2548 }
2553 2549 }
2554 2550
2555 2551 .search-results {
2556 2552
2557 2553 h2 {
2558 2554 margin-bottom: 0;
2559 2555 }
2560 2556 .codeblock {
2561 2557 border: none;
2562 2558 background: transparent;
2563 2559 }
2564 2560
2565 2561 .codeblock-header {
2566 2562 border: none;
2567 2563 background: transparent;
2568 2564 }
2569 2565
2570 2566 .code-body {
2571 2567 border: @border-thickness solid @grey6;
2572 2568 .border-radius(@border-radius);
2573 2569 }
2574 2570
2575 2571 .td-commit {
2576 2572 &:extend(pre);
2577 2573 border-bottom: @border-thickness solid @border-default-color;
2578 2574 }
2579 2575
2580 2576 .message {
2581 2577 height: auto;
2582 2578 max-width: 350px;
2583 2579 white-space: normal;
2584 2580 text-overflow: initial;
2585 2581 overflow: visible;
2586 2582
2587 2583 .match { background-color: #faffa6;}
2588 2584 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2589 2585 }
2590 2586
2591 2587 .path {
2592 2588 border-bottom: none !important;
2593 2589 border-left: 1px solid @grey6 !important;
2594 2590 border-right: 1px solid @grey6 !important;
2595 2591 }
2596 2592 }
2597 2593
2598 2594 table.rctable td.td-search-results div {
2599 2595 max-width: 100%;
2600 2596 }
2601 2597
2602 2598 #tip-box, .tip-box{
2603 2599 padding: @menupadding/2;
2604 2600 display: block;
2605 2601 border: @border-thickness solid @border-highlight-color;
2606 2602 .border-radius(@border-radius);
2607 2603 background-color: white;
2608 2604 z-index: 99;
2609 2605 white-space: pre-wrap;
2610 2606 }
2611 2607
2612 2608 #linktt {
2613 2609 width: 79px;
2614 2610 }
2615 2611
2616 2612 #help_kb .modal-content{
2617 2613 max-width: 750px;
2618 2614 margin: 10% auto;
2619 2615
2620 2616 table{
2621 2617 td,th{
2622 2618 border-bottom: none;
2623 2619 line-height: 2.5em;
2624 2620 }
2625 2621 th{
2626 2622 padding-bottom: @textmargin/2;
2627 2623 }
2628 2624 td.keys{
2629 2625 text-align: center;
2630 2626 }
2631 2627 }
2632 2628
2633 2629 .block-left{
2634 2630 width: 45%;
2635 2631 margin-right: 5%;
2636 2632 }
2637 2633 .modal-footer{
2638 2634 clear: both;
2639 2635 }
2640 2636 .key.tag{
2641 2637 padding: 0.5em;
2642 2638 background-color: @rcblue;
2643 2639 color: white;
2644 2640 border-color: @rcblue;
2645 2641 .box-shadow(none);
2646 2642 }
2647 2643 }
2648 2644
2649 2645
2650 2646
2651 2647 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2652 2648
2653 2649 @import 'statistics-graph';
2654 2650 @import 'tables';
2655 2651 @import 'forms';
2656 2652 @import 'diff';
2657 2653 @import 'summary';
2658 2654 @import 'navigation';
2659 2655
2660 2656 //--- SHOW/HIDE SECTIONS --//
2661 2657
2662 2658 .btn-collapse {
2663 2659 float: right;
2664 2660 text-align: right;
2665 2661 font-family: @text-light;
2666 2662 font-size: @basefontsize;
2667 2663 cursor: pointer;
2668 2664 border: none;
2669 2665 color: @rcblue;
2670 2666 }
2671 2667
2672 2668 table.rctable,
2673 2669 table.dataTable {
2674 2670 .btn-collapse {
2675 2671 float: right;
2676 2672 text-align: right;
2677 2673 }
2678 2674 }
2679 2675
2680 2676 table.rctable {
2681 2677 &.permissions {
2682 2678
2683 2679 th.td-owner {
2684 2680 padding: 0;
2685 2681 }
2686 2682
2687 2683 th {
2688 2684 font-weight: normal;
2689 2685 padding: 0 5px;
2690 2686 }
2691 2687
2692 2688 }
2693 2689 }
2694 2690
2695 2691
2696 2692 // TODO: johbo: Fix for IE10, this avoids that we see a border
2697 2693 // and padding around checkboxes and radio boxes. Move to the right place,
2698 2694 // or better: Remove this once we did the form refactoring.
2699 2695 input[type=checkbox],
2700 2696 input[type=radio] {
2701 2697 padding: 0;
2702 2698 border: none;
2703 2699 }
2704 2700
2705 2701 .toggle-ajax-spinner{
2706 2702 height: 16px;
2707 2703 width: 16px;
2708 2704 }
2709 2705
2710 2706
2711 2707 .markup-form .clearfix {
2712 2708 .border-radius(@border-radius);
2713 2709 margin: 0px;
2714 2710 }
2715 2711
2716 2712 .markup-form-area {
2717 2713 padding: 8px 12px;
2718 2714 border: 1px solid @grey4;
2719 2715 .border-radius(@border-radius);
2720 2716 }
2721 2717
2722 2718 .markup-form-area-header .nav-links {
2723 2719 display: flex;
2724 2720 flex-flow: row wrap;
2725 2721 -webkit-flex-flow: row wrap;
2726 2722 width: 100%;
2727 2723 }
2728 2724
2729 2725 .markup-form-area-footer {
2730 2726 display: flex;
2731 2727 }
2732 2728
2733 2729 .markup-form-area-footer .toolbar {
2734 2730
2735 2731 }
2736 2732
2737 2733 // markup Form
2738 2734 div.markup-form {
2739 2735 margin-top: 20px;
2740 2736 }
2741 2737
2742 2738 .markup-form strong {
2743 2739 display: block;
2744 2740 margin-bottom: 15px;
2745 2741 }
2746 2742
2747 2743 .markup-form textarea {
2748 2744 width: 100%;
2749 2745 height: 100px;
2750 2746 font-family: @text-monospace;
2751 2747 }
2752 2748
2753 2749 form.markup-form {
2754 2750 margin-top: 10px;
2755 2751 margin-left: 10px;
2756 2752 }
2757 2753
2758 2754 .markup-form .comment-block-ta,
2759 2755 .markup-form .preview-box {
2760 2756 .border-radius(@border-radius);
2761 2757 .box-sizing(border-box);
2762 2758 background-color: white;
2763 2759 }
2764 2760
2765 2761 .markup-form .preview-box.unloaded {
2766 2762 height: 50px;
2767 2763 text-align: center;
2768 2764 padding: 20px;
2769 2765 background-color: white;
2770 2766 }
2771 2767
2772 2768
2773 2769 .dropzone-wrapper {
2774 2770 border: 1px solid @grey5;
2775 2771 padding: 20px;
2776 2772 }
2777 2773
2778 2774 .dropzone,
2779 2775 .dropzone-pure {
2780 2776 border: 2px dashed @grey5;
2781 2777 border-radius: 5px;
2782 2778 background: white;
2783 2779 min-height: 200px;
2784 2780 padding: 54px;
2785 2781
2786 2782 .dz-message {
2787 2783 font-weight: 700;
2788 2784 text-align: center;
2789 2785 margin: 2em 0;
2790 2786 }
2791 2787
2792 2788 }
2793 2789
2794 2790 .dz-preview {
2795 2791 margin: 10px 0 !important;
2796 2792 position: relative;
2797 2793 vertical-align: top;
2798 2794 padding: 10px;
2799 2795 border-bottom: 1px solid @grey5;
2800 2796 }
2801 2797
2802 2798 .dz-filename {
2803 2799 font-weight: 700;
2804 2800 float:left;
2805 2801 }
2806 2802
2807 2803 .dz-sending {
2808 2804 float: right;
2809 2805 }
2810 2806
2811 2807 .dz-response {
2812 2808 clear:both
2813 2809 }
2814 2810
2815 2811 .dz-filename-size {
2816 2812 float:right
2817 2813 }
2818 2814
2819 2815 .dz-error-message {
2820 2816 color: @alert2;
2821 2817 padding-top: 10px;
2822 2818 clear: both;
2823 2819 }
@@ -1,141 +1,141 b''
1 1 // tags.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // TAGS
6 6 .tag,
7 7 .tagtag {
8 8 display: inline-block;
9 9 min-height: 0;
10 10 margin: 0 auto;
11 11 padding: .25em;
12 12 text-align: center;
13 13 font-size: (-1 + @basefontsize); //fit in tables
14 line-height: .9em;
14 line-height: 1.1em;
15 15 border: none;
16 16 box-shadow: @button-shadow;
17 17 .border-radius(@border-radius);
18 18 font-family: @text-regular;
19 19 background-image: none;
20 20 color: @grey4;
21 21 .border ( @border-thickness-tags, @grey5 );
22 22 white-space: nowrap;
23 23 a {
24 24 color: inherit;
25 25
26 26 &:hover {
27 27 color: @grey2;
28 28 }
29 29
30 30 i,
31 31 [class^="icon-"]:before,
32 32 [class*=" icon-"]:before {
33 33 text-decoration: none;
34 34 }
35 35 }
36 36
37 37 &:hover {
38 38 border-color: @grey4;
39 39 }
40 40 }
41 41
42 42 .tag0 { .border ( @border-thickness-tags, @grey4 ); color:@grey4; }
43 43 .tag1 { .border ( @border-thickness-tags, @color1 ); color:@color1; }
44 44 .tag2 { .border ( @border-thickness-tags, @color2 ); color:@color2; }
45 45 .tag3 { .border ( @border-thickness-tags, @color3 ); color:@color3; }
46 46 .tag4 { .border ( @border-thickness-tags, @color4 ); color:@color4; }
47 47 .tag5 { .border ( @border-thickness-tags, @color5 ); color:@color5; }
48 48 .tag6 { .border ( @border-thickness-tags, @color6 ); color:@color6; }
49 49 .tag7 { .border ( @border-thickness-tags, @color7 ); color:@color7; }
50 50 .tag8 { .border ( @border-thickness-tags, @color8 ); color:@color8; }
51 51
52 52 .metatag-list {
53 53 margin: 0;
54 54 padding: 0;
55 55
56 56 li {
57 57 margin: 0 0 @padding;
58 58 line-height: 1em;
59 59 list-style-type: none;
60 60 }
61 61 }
62 62
63 63 .branchtag, .booktag {
64 64 &:extend(.tag);
65 65
66 66
67 67 a {
68 68 color:inherit;
69 69 }
70 70 }
71 71
72 72 .metatag {
73 73 &:extend(.tag);
74 74 a {
75 75 color:inherit;
76 76 text-decoration: underline;
77 77 }
78 78 }
79 79
80 80 [tag="generic"] { &:extend(.tag0); }
81 81 [tag="label"] { &:extend(.tag0); }
82 82
83 83 [tag="state featured"] { &:extend(.tag1); }
84 84 [tag="state dev"] { &:extend(.tag1); }
85 85 [tag="ref base"] { &:extend(.tag1); }
86 86
87 87 [tag="state stable"] { &:extend(.tag2); }
88 88 [tag="state stale"] { &:extend(.tag2); }
89 89
90 90 [tag="ref requires"] { &:extend(.tag3); }
91 91
92 92 [tag="state dead"] { &:extend(.tag4); }
93 93 [tag="state deprecated"] { &:extend(.tag4); }
94 94
95 95 [tag="ref conflicts"] { &:extend(.tag4); }
96 96
97 97 [tag="license"] { &:extend(.tag6); }
98 98
99 99 [tag="lang"] { &:extend(.tag7); }
100 100 [tag="language"] { &:extend(.tag7); }
101 101 [tag="ref recommends"] { &:extend(.tag7); }
102 102
103 103 [tag="see"] { &:extend(.tag8); }
104 104 [tag="url"] { &:extend(.tag8); }
105 105
106 106
107 107 .perm_overriden {
108 108 text-decoration: line-through;
109 109 opacity: 0.6;
110 110 }
111 111
112 112 .perm_tag {
113 113 &:extend(.tag);
114 114
115 115 &.read {
116 116 &:extend(.tag1);
117 117 }
118 118 &.write {
119 119 &:extend(.tag4);
120 120 }
121 121 &.admin {
122 122 &:extend(.tag5);
123 123 }
124 124 &.merge {
125 125 &:extend(.tag1);
126 126 }
127 127 &.push {
128 128 &:extend(.tag4);
129 129 }
130 130 &.push_force {
131 131 &:extend(.tag5);
132 132 }
133 133 }
134 134
135 135 .phase-draft {
136 136 color: @color3
137 137 }
138 138
139 139 .phase-secret {
140 140 color:@grey3
141 141 }
@@ -1,311 +1,312 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 6 <%namespace name="file_base" file="/files/base.mako"/>
7 7
8 8 <%def name="title()">
9 9 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='repositories')}
17 17 </%def>
18 18
19 19 <%def name="menu_bar_subnav()">
20 20 ${self.repo_menu(active='commits')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 <script type="text/javascript">
25 25 // TODO: marcink switch this to pyroutes
26 26 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
27 27 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
28 28 </script>
29 29
30 30 <div class="box">
31 31
32 32 <div class="summary">
33 33
34 34 <div class="fieldset">
35 35 <div class="left-content">
36 36
37 37 <div class="left-content-avatar">
38 38 ${base.gravatar(c.commit.author_email, 30)}
39 39 </div>
40 40
41 41 <div class="left-content-message">
42 42 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
43 43 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
44 44 </div>
45 45
46 46 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
47 47 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
48 48 </div>
49 49
50 50 <div class="fieldset" data-toggle="summary-details">
51 51 <div class="">
52 52 <table>
53 53 <tr class="file_author tooltip" title="${h.tooltip(h.author_string(c.commit.author_email))}">
54 54
55 55 <td>
56 56 <span class="user commit-author">${h.link_to_user(c.commit.author)}</span>
57 57 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
58 58 </td>
59 59
60 60 <td>
61 61 ## second cell for consistency with files
62 62 </td>
63 63 </tr>
64 64 </table>
65 65 </div>
66 66 </div>
67 67
68 68 </div>
69 69 </div>
70 70
71 71 <div class="right-content">
72 72
73 73 <div data-toggle="summary-details">
74 74 <div class="tags tags-main">
75 75 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
76 76 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
77 77 ${file_base.refs(c.commit)}
78 78
79 79 ## phase
80 80 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
81 81 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
82 82 <i class="icon-info"></i>${c.commit.phase}
83 83 </span>
84 84 % endif
85 85
86 86 ## obsolete commits
87 87 % if getattr(c.commit, 'obsolete', False):
88 88 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
89 89 ${_('obsolete')}
90 90 </span>
91 91 % endif
92 92
93 93 ## hidden commits
94 94 % if getattr(c.commit, 'hidden', False):
95 95 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
96 96 ${_('hidden')}
97 97 </span>
98 98 % endif
99 99 </div>
100 100
101 101 %if c.statuses:
102 102 <div class="tag status-tag-${c.statuses[0]} pull-right">
103 103 <i class="icon-circle review-status-${c.statuses[0]}"></i>
104 104 <div class="pull-right">${h.commit_status_lbl(c.statuses[0])}</div>
105 105 </div>
106 106 %endif
107 107
108 108 </div>
109 109
110 110 </div>
111 111 </div>
112 112
113 113 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
114 114 <div class="left-label-summary">
115 115 <p>${_('Commit navigation')}:</p>
116 116 <div class="right-label-summary">
117 117 <span id="parent_link" class="tag tagtag">
118 118 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
119 119 </span>
120 120
121 121 <span id="child_link" class="tag tagtag">
122 122 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
123 123 </span>
124 124 </div>
125 125 </div>
126 126 </div>
127 127
128 128 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
129 129 <div class="left-label-summary">
130 130 <p>${_('Diff options')}:</p>
131 131 <div class="right-label-summary">
132 132 <div class="diff-actions">
133 133 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
134 134 ${_('Raw Diff')}
135 135 </a>
136 136 |
137 137 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
138 138 ${_('Patch Diff')}
139 139 </a>
140 140 |
141 141 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
142 142 ${_('Download Diff')}
143 143 </a>
144 144 </div>
145 145 </div>
146 146 </div>
147 147 </div>
148 148
149 149 <div class="clear-fix"></div>
150 150
151 151 <div class="btn-collapse" data-toggle="summary-details">
152 152 ${_('Show More')}
153 153 </div>
154 154
155 155 </div>
156 156
157 157 <div class="cs_files">
158 158 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
159 159 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
160 160 ${cbdiffs.render_diffset(
161 161 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
162 162 </div>
163 163
164 ## template for inline comment form
165 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
166
167 ## comments heading with count
164 168 <div class="comments-heading">
165 169 <i class="icon-comment"></i>
166 170 ${_('Comments')} ${len(c.comments)}
167 171 </div>
168 172
169 ## template for inline comment form
170 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
171
172 173 ## render comments
173 174 ${comment.generate_comments(c.comments)}
174 175
175 176 ## main comment form and it status
176 177 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
177 178 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
178 179 </div>
179 180
180 181 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
181 182 <script type="text/javascript">
182 183
183 184 $(document).ready(function() {
184 185
185 186 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
186 187 if($('#trimmed_message_box').height() === boxmax){
187 188 $('#message_expand').show();
188 189 }
189 190
190 191 $('#message_expand').on('click', function(e){
191 192 $('#trimmed_message_box').css('max-height', 'none');
192 193 $(this).hide();
193 194 });
194 195
195 196 $('.show-inline-comments').on('click', function(e){
196 197 var boxid = $(this).attr('data-comment-id');
197 198 var button = $(this);
198 199
199 200 if(button.hasClass("comments-visible")) {
200 201 $('#{0} .inline-comments'.format(boxid)).each(function(index){
201 202 $(this).hide();
202 203 });
203 204 button.removeClass("comments-visible");
204 205 } else {
205 206 $('#{0} .inline-comments'.format(boxid)).each(function(index){
206 207 $(this).show();
207 208 });
208 209 button.addClass("comments-visible");
209 210 }
210 211 });
211 212
212 213
213 214 // next links
214 215 $('#child_link').on('click', function(e){
215 216 // fetch via ajax what is going to be the next link, if we have
216 217 // >1 links show them to user to choose
217 218 if(!$('#child_link').hasClass('disabled')){
218 219 $.ajax({
219 220 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
220 221 success: function(data) {
221 222 if(data.results.length === 0){
222 223 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
223 224 }
224 225 if(data.results.length === 1){
225 226 var commit = data.results[0];
226 227 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
227 228 }
228 229 else if(data.results.length === 2){
229 230 $('#child_link').addClass('disabled');
230 231 $('#child_link').addClass('double');
231 232
232 233 var _html = '';
233 234 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
234 235 .replace('__branch__', data.results[0].branch)
235 236 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
236 237 .replace('__title__', data.results[0].message)
237 238 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
238 239 _html +=' | ';
239 240 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
240 241 .replace('__branch__', data.results[1].branch)
241 242 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
242 243 .replace('__title__', data.results[1].message)
243 244 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
244 245 $('#child_link').html(_html);
245 246 }
246 247 }
247 248 });
248 249 e.preventDefault();
249 250 }
250 251 });
251 252
252 253 // prev links
253 254 $('#parent_link').on('click', function(e){
254 255 // fetch via ajax what is going to be the next link, if we have
255 256 // >1 links show them to user to choose
256 257 if(!$('#parent_link').hasClass('disabled')){
257 258 $.ajax({
258 259 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
259 260 success: function(data) {
260 261 if(data.results.length === 0){
261 262 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
262 263 }
263 264 if(data.results.length === 1){
264 265 var commit = data.results[0];
265 266 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
266 267 }
267 268 else if(data.results.length === 2){
268 269 $('#parent_link').addClass('disabled');
269 270 $('#parent_link').addClass('double');
270 271
271 272 var _html = '';
272 273 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
273 274 .replace('__branch__', data.results[0].branch)
274 275 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
275 276 .replace('__title__', data.results[0].message)
276 277 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
277 278 _html +=' | ';
278 279 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
279 280 .replace('__branch__', data.results[1].branch)
280 281 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
281 282 .replace('__title__', data.results[1].message)
282 283 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
283 284 $('#parent_link').html(_html);
284 285 }
285 286 }
286 287 });
287 288 e.preventDefault();
288 289 }
289 290 });
290 291
291 292 if (location.hash) {
292 293 var result = splitDelimitedHash(location.hash);
293 294 var line = $('html').find(result.loc);
294 295 if (line.length > 0){
295 296 offsetScroll(line, 70);
296 297 }
297 298 }
298 299
299 300 // browse tree @ revision
300 301 $('#files_link').on('click', function(e){
301 302 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
302 303 e.preventDefault();
303 304 });
304 305
305 306 // inject comments into their proper positions
306 307 var file_comments = $('.inline-comment-placeholder');
307 308
308 309 })
309 310 </script>
310 311
311 312 </%def>
@@ -1,407 +1,404 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 <% latest_ver = len(getattr(c, 'versions', [])) %>
11 11 % if inline:
12 12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
13 13 % else:
14 14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
15 15 % endif
16 16
17 17
18 18 <div class="comment
19 19 ${'comment-inline' if inline else 'comment-general'}
20 20 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
21 21 id="comment-${comment.comment_id}"
22 22 line="${comment.line_no}"
23 23 data-comment-id="${comment.comment_id}"
24 24 data-comment-type="${comment.comment_type}"
25 25 data-comment-line-no="${comment.line_no}"
26 26 data-comment-inline=${h.json.dumps(inline)}
27 27 style="${'display: none;' if outdated_at_ver else ''}">
28 28
29 29 <div class="meta">
30 30 <div class="comment-type-label">
31 31 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
32 32 % if comment.comment_type == 'todo':
33 33 % if comment.resolved:
34 34 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
35 35 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
36 36 </div>
37 37 % else:
38 38 <div class="resolved tooltip" style="display: none">
39 39 <span>${comment.comment_type}</span>
40 40 </div>
41 41 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
42 42 ${comment.comment_type}
43 43 </div>
44 44 % endif
45 45 % else:
46 46 % if comment.resolved_comment:
47 47 fix
48 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
49 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
50 </a>
48 51 % else:
49 52 ${comment.comment_type or 'note'}
50 53 % endif
51 54 % endif
52 55 </div>
53 56 </div>
54 57
55 58 <div class="author ${'author-inline' if inline else 'author-general'}">
56 59 ${base.gravatar_with_user(comment.author.email, 16)}
57 60 </div>
58 61 <div class="date">
59 62 ${h.age_component(comment.modified_at, time_is_local=True)}
60 63 </div>
61 64 % if inline:
62 65 <span></span>
63 66 % else:
64 67 <div class="status-change">
65 68 % if comment.pull_request:
66 69 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
67 70 % if comment.status_change:
68 71 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
69 72 % else:
70 73 ${_('pull request #%s') % comment.pull_request.pull_request_id}
71 74 % endif
72 75 </a>
73 76 % else:
74 77 % if comment.status_change:
75 78 ${_('Status change on commit')}:
76 79 % endif
77 80 % endif
78 81 </div>
79 82 % endif
80 83
81 84 % if comment.status_change:
82 85 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
83 86 <div title="${_('Commit status')}" class="changeset-status-lbl">
84 87 ${comment.status_change[0].status_lbl}
85 88 </div>
86 89 % endif
87 90
88 % if comment.resolved_comment:
89 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
90 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
91 </a>
92 % endif
93
94 91 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
95 92
96 93 <div class="comment-links-block">
97 94 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
98 95 <span class="tag authortag tooltip" title="${_('Pull request author')}">
99 96 ${_('author')}
100 97 </span>
101 98 |
102 99 % endif
103 100 % if inline:
104 101 <div class="pr-version-inline">
105 102 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
106 103 % if outdated_at_ver:
107 104 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
108 105 outdated ${'v{}'.format(pr_index_ver)} |
109 106 </code>
110 107 % elif pr_index_ver:
111 108 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
112 109 ${'v{}'.format(pr_index_ver)} |
113 110 </code>
114 111 % endif
115 112 </a>
116 113 </div>
117 114 % else:
118 115 % if comment.pull_request_version_id and pr_index_ver:
119 116 |
120 117 <div class="pr-version">
121 118 % if comment.outdated:
122 119 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
123 120 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
124 121 </a>
125 122 % else:
126 123 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
127 124 <a href="${h.route_path('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)}">
128 125 <code class="pr-version-num">
129 126 ${'v{}'.format(pr_index_ver)}
130 127 </code>
131 128 </a>
132 129 </div>
133 130 % endif
134 131 </div>
135 132 % endif
136 133 % endif
137 134
138 135 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
139 136 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
140 137 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
141 138 ## permissions to delete
142 139 %if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
143 140 ## TODO: dan: add edit comment here
144 141 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
145 142 %else:
146 143 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
147 144 %endif
148 145 %else:
149 146 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
150 147 %endif
151 148
152 149 % if outdated_at_ver:
153 150 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
154 151 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
155 152 % else:
156 153 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
157 154 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
158 155 % endif
159 156
160 157 </div>
161 158 </div>
162 159 <div class="text">
163 160 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
164 161 </div>
165 162
166 163 </div>
167 164 </%def>
168 165
169 166 ## generate main comments
170 167 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
171 168 <div class="general-comments" id="comments">
172 169 %for comment in comments:
173 170 <div id="comment-tr-${comment.comment_id}">
174 171 ## only render comments that are not from pull request, or from
175 172 ## pull request and a status change
176 173 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
177 174 ${comment_block(comment)}
178 175 %endif
179 176 </div>
180 177 %endfor
181 178 ## to anchor ajax comments
182 179 <div id="injected_page_comments"></div>
183 180 </div>
184 181 </%def>
185 182
186 183
187 184 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
188 185
189 186 <div class="comments">
190 187 <%
191 188 if is_pull_request:
192 189 placeholder = _('Leave a comment on this Pull Request.')
193 190 elif is_compare:
194 191 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
195 192 else:
196 193 placeholder = _('Leave a comment on this Commit.')
197 194 %>
198 195
199 196 % if c.rhodecode_user.username != h.DEFAULT_USER:
200 197 <div class="js-template" id="cb-comment-general-form-template">
201 198 ## template generated for injection
202 199 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
203 200 </div>
204 201
205 202 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
206 203 ## inject form here
207 204 </div>
208 205 <script type="text/javascript">
209 206 var lineNo = 'general';
210 207 var resolvesCommentId = null;
211 208 var generalCommentForm = Rhodecode.comments.createGeneralComment(
212 209 lineNo, "${placeholder}", resolvesCommentId);
213 210
214 211 // set custom success callback on rangeCommit
215 212 % if is_compare:
216 213 generalCommentForm.setHandleFormSubmit(function(o) {
217 214 var self = generalCommentForm;
218 215
219 216 var text = self.cm.getValue();
220 217 var status = self.getCommentStatus();
221 218 var commentType = self.getCommentType();
222 219
223 220 if (text === "" && !status) {
224 221 return;
225 222 }
226 223
227 224 // we can pick which commits we want to make the comment by
228 225 // selecting them via click on preview pane, this will alter the hidden inputs
229 226 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
230 227
231 228 var commitIds = [];
232 229 $('#changeset_compare_view_content .compare_select').each(function(el) {
233 230 var commitId = this.id.replace('row-', '');
234 231 if ($(this).hasClass('hl') || !cherryPicked) {
235 232 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
236 233 commitIds.push(commitId);
237 234 } else {
238 235 $("input[data-commit-id='{0}']".format(commitId)).val('')
239 236 }
240 237 });
241 238
242 239 self.setActionButtonsDisabled(true);
243 240 self.cm.setOption("readOnly", true);
244 241 var postData = {
245 242 'text': text,
246 243 'changeset_status': status,
247 244 'comment_type': commentType,
248 245 'commit_ids': commitIds,
249 246 'csrf_token': CSRF_TOKEN
250 247 };
251 248
252 249 var submitSuccessCallback = function(o) {
253 250 location.reload(true);
254 251 };
255 252 var submitFailCallback = function(){
256 253 self.resetCommentFormState(text)
257 254 };
258 255 self.submitAjaxPOST(
259 256 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
260 257 });
261 258 % endif
262 259
263 260
264 261 </script>
265 262 % else:
266 263 ## form state when not logged in
267 264 <div class="comment-form ac">
268 265
269 266 <div class="comment-area">
270 267 <div class="comment-area-header">
271 268 <ul class="nav-links clearfix">
272 269 <li class="active">
273 270 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
274 271 </li>
275 272 <li class="">
276 273 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
277 274 </li>
278 275 </ul>
279 276 </div>
280 277
281 278 <div class="comment-area-write" style="display: block;">
282 279 <div id="edit-container">
283 280 <div style="padding: 40px 0">
284 281 ${_('You need to be logged in to leave comments.')}
285 282 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
286 283 </div>
287 284 </div>
288 285 <div id="preview-container" class="clearfix" style="display: none;">
289 286 <div id="preview-box" class="preview-box"></div>
290 287 </div>
291 288 </div>
292 289
293 290 <div class="comment-area-footer">
294 291 <div class="toolbar">
295 292 <div class="toolbar-text">
296 293 </div>
297 294 </div>
298 295 </div>
299 296 </div>
300 297
301 298 <div class="comment-footer">
302 299 </div>
303 300
304 301 </div>
305 302 % endif
306 303
307 304 <script type="text/javascript">
308 305 bindToggleButtons();
309 306 </script>
310 307 </div>
311 308 </%def>
312 309
313 310
314 311 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
315 312 ## comment injected based on assumption that user is logged in
316 313
317 314 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
318 315
319 316 <div class="comment-area">
320 317 <div class="comment-area-header">
321 318 <ul class="nav-links clearfix">
322 319 <li class="active">
323 320 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
324 321 </li>
325 322 <li class="">
326 323 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
327 324 </li>
328 325 <li class="pull-right">
329 326 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
330 327 % for val in c.visual.comment_types:
331 328 <option value="${val}">${val.upper()}</option>
332 329 % endfor
333 330 </select>
334 331 </li>
335 332 </ul>
336 333 </div>
337 334
338 335 <div class="comment-area-write" style="display: block;">
339 336 <div id="edit-container_${lineno_id}">
340 337 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
341 338 </div>
342 339 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
343 340 <div id="preview-box_${lineno_id}" class="preview-box"></div>
344 341 </div>
345 342 </div>
346 343
347 344 <div class="comment-area-footer">
348 345 <div class="toolbar">
349 346 <div class="toolbar-text">
350 347 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
351 348 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
352 349 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
353 350 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
354 351 )
355 352 )|n}
356 353 </div>
357 354 </div>
358 355 </div>
359 356 </div>
360 357
361 358 <div class="comment-footer">
362 359
363 360 % if review_statuses:
364 361 <div class="status_box">
365 362 <select id="change_status_${lineno_id}" name="changeset_status">
366 363 <option></option> ## Placeholder
367 364 % for status, lbl in review_statuses:
368 365 <option value="${status}" data-status="${status}">${lbl}</option>
369 366 %if is_pull_request and change_status and status in ('approved', 'rejected'):
370 367 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
371 368 %endif
372 369 % endfor
373 370 </select>
374 371 </div>
375 372 % endif
376 373
377 374 ## inject extra inputs into the form
378 375 % if form_extras and isinstance(form_extras, (list, tuple)):
379 376 <div id="comment_form_extras">
380 377 % for form_ex_el in form_extras:
381 378 ${form_ex_el|n}
382 379 % endfor
383 380 </div>
384 381 % endif
385 382
386 383 <div class="action-buttons">
387 384 ## inline for has a file, and line-number together with cancel hide button.
388 385 % if form_type == 'inline':
389 386 <input type="hidden" name="f_path" value="{0}">
390 387 <input type="hidden" name="line" value="${lineno_id}">
391 388 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
392 389 ${_('Cancel')}
393 390 </button>
394 391 % endif
395 392
396 393 % if form_type != 'inline':
397 394 <div class="action-buttons-extra"></div>
398 395 % endif
399 396
400 397 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
401 398
402 399 </div>
403 400 </div>
404 401
405 402 </form>
406 403
407 404 </%def> No newline at end of file
@@ -1,1121 +1,1149 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 # additional menu for PRs
53 pull_request_menu=None
54
52 55 )">
53 56
54 57 <%
55 58 diffset_container_id = h.md5(diffset.target_ref)
56 59 collapse_all = len(diffset.files) > collapse_when_files_over
57 60 %>
58 61
59 62 %if use_comments:
60 63 <div id="cb-comments-inline-container-template" class="js-template">
61 64 ${inline_comments_container([], inline_comments)}
62 65 </div>
63 66 <div class="js-template" id="cb-comment-inline-form-template">
64 67 <div class="comment-inline-form ac">
65 68
66 69 %if c.rhodecode_user.username != h.DEFAULT_USER:
67 70 ## render template for inline comments
68 71 ${commentblock.comment_form(form_type='inline')}
69 72 %else:
70 73 ${h.form('', class_='inline-form comment-form-login', method='get')}
71 74 <div class="pull-left">
72 75 <div class="comment-help pull-right">
73 76 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
74 77 </div>
75 78 </div>
76 79 <div class="comment-button pull-right">
77 80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
78 81 ${_('Cancel')}
79 82 </button>
80 83 </div>
81 84 <div class="clearfix"></div>
82 85 ${h.end_form()}
83 86 %endif
84 87 </div>
85 88 </div>
86 89
87 90 %endif
88 91
89 92 %if c.user_session_attrs["diffmode"] == 'sideside':
90 93 <style>
91 94 .wrapper {
92 95 max-width: 1600px !important;
93 96 }
94 97 </style>
95 98 %endif
96 99
97 100 %if ruler_at_chars:
98 101 <style>
99 102 .diff table.cb .cb-content:after {
100 103 content: "";
101 104 border-left: 1px solid blue;
102 105 position: absolute;
103 106 top: 0;
104 107 height: 18px;
105 108 opacity: .2;
106 109 z-index: 10;
107 110 //## +5 to account for diff action (+/-)
108 111 left: ${ruler_at_chars + 5}ch;
109 112 </style>
110 113 %endif
111 114
112 115 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
113 116
114 117 <div style="height: 20px; line-height: 20px">
115 118 ## expand/collapse action
116 119 <div class="pull-left">
117 120 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
118 121 % if collapse_all:
119 122 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
120 123 % else:
121 124 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
122 125 % endif
123 126 </a>
124 127
125 128 </div>
126 129
127 130 ## todos
131 % if getattr(c, 'at_version', None):
132 <div class="pull-right">
133 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
134 ${_('not available in this view')}
135 </div>
136 % else:
128 137 <div class="pull-right">
129 138 <div class="comments-number" style="padding-left: 10px">
130 139 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
131 140 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
132 141 % if c.unresolved_comments:
133 142 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
134 143 ${_('{} unresolved').format(len(c.unresolved_comments))}
135 144 </a>
136 145 % else:
137 146 ${_('0 unresolved')}
138 147 % endif
139 148
140 ${_('{} Resolved').format(len(c.unresolved_comments))}
149 ${_('{} Resolved').format(len(c.resolved_comments))}
141 150 % endif
142 151 </div>
143 152 </div>
153 % endif
144 154
145 155 ## comments
146 156 <div class="pull-right">
147 157 <div class="comments-number" style="padding-left: 10px">
148 158 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
149 159 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
150 160 % if c.comments:
151 161 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
152 162 % else:
153 163 ${_('0 General')}
154 164 % endif
155 165
156 166 % if c.inline_cnt:
157 167 <a href="#" onclick="return Rhodecode.comments.nextComment();"
158 168 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
159 169 </a>
160 170 % else:
161 171 ${_('0 Inline')}
162 172 % endif
163 173 % endif
174
175 % if pull_request_menu:
176 <%
177 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
178 %>
179
180 % if outdated_comm_count_ver:
181 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
182 (${_("{} Outdated").format(outdated_comm_count_ver)})
183 </a>
184 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
185 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
186 % else:
187 (${_("{} Outdated").format(outdated_comm_count_ver)})
188 % endif
189
190 % endif
191
164 192 </div>
165 193 </div>
166 194
167 195 </div>
168 196
169 197 % if diffset.limited_diff:
170 198 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
171 199 <h2 class="clearinner">
172 200 ${_('The requested changes are too big and content was truncated.')}
173 201 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
174 202 </h2>
175 203 </div>
176 204 ## commit range header for each individual diff
177 205 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
178 206 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
179 207 <div class="clearinner">
180 208 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
181 209 </div>
182 210 </div>
183 211 % endif
184 212
185 213 <div id="todo-box">
186 214 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
187 215 % for co in c.unresolved_comments:
188 216 <a class="permalink" href="#comment-${co.comment_id}"
189 217 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
190 218 <i class="icon-flag-filled-red"></i>
191 219 ${co.comment_id}</a>${('' if loop.last else ',')}
192 220 % endfor
193 221 % endif
194 222 </div>
195 223 %if diffset.has_hidden_changes:
196 224 <p class="empty_data">${_('Some changes may be hidden')}</p>
197 225 %elif not diffset.files:
198 226 <p class="empty_data">${_('No files')}</p>
199 227 %endif
200 228
201 229 <div class="filediffs">
202 230
203 231 ## initial value could be marked as False later on
204 232 <% over_lines_changed_limit = False %>
205 233 %for i, filediff in enumerate(diffset.files):
206 234
207 235 <%
208 236 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
209 237 over_lines_changed_limit = lines_changed > lines_changed_limit
210 238 %>
211 239 ## anchor with support of sticky header
212 240 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
213 241
214 242 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
215 243 <div
216 244 class="filediff"
217 245 data-f-path="${filediff.patch['filename']}"
218 246 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
219 247 >
220 248 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
221 249 <div class="filediff-collapse-indicator icon-"></div>
222 250 ${diff_ops(filediff)}
223 251 </label>
224 252
225 253 ${diff_menu(filediff, use_comments=use_comments)}
226 254 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
227 255
228 256 ## new/deleted/empty content case
229 257 % if not filediff.hunks:
230 258 ## Comment container, on "fakes" hunk that contains all data to render comments
231 259 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
232 260 % endif
233 261
234 262 %if filediff.limited_diff:
235 263 <tr class="cb-warning cb-collapser">
236 264 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
237 265 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
238 266 </td>
239 267 </tr>
240 268 %else:
241 269 %if over_lines_changed_limit:
242 270 <tr class="cb-warning cb-collapser">
243 271 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
244 272 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
245 273 <a href="#" class="cb-expand"
246 274 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
247 275 </a>
248 276 <a href="#" class="cb-collapse"
249 277 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
250 278 </a>
251 279 </td>
252 280 </tr>
253 281 %endif
254 282 %endif
255 283
256 284 % for hunk in filediff.hunks:
257 285 <tr class="cb-hunk">
258 286 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
259 287 ## TODO: dan: add ajax loading of more context here
260 288 ## <a href="#">
261 289 <i class="icon-more"></i>
262 290 ## </a>
263 291 </td>
264 292 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
265 293 @@
266 294 -${hunk.source_start},${hunk.source_length}
267 295 +${hunk.target_start},${hunk.target_length}
268 296 ${hunk.section_header}
269 297 </td>
270 298 </tr>
271 299 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
272 300 % endfor
273 301
274 302 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
275 303
276 304 ## outdated comments that do not fit into currently displayed lines
277 305 % for lineno, comments in unmatched_comments.items():
278 306
279 307 %if c.user_session_attrs["diffmode"] == 'unified':
280 308 % if loop.index == 0:
281 309 <tr class="cb-hunk">
282 310 <td colspan="3"></td>
283 311 <td>
284 312 <div>
285 313 ${_('Unmatched inline comments below')}
286 314 </div>
287 315 </td>
288 316 </tr>
289 317 % endif
290 318 <tr class="cb-line">
291 319 <td class="cb-data cb-context"></td>
292 320 <td class="cb-lineno cb-context"></td>
293 321 <td class="cb-lineno cb-context"></td>
294 322 <td class="cb-content cb-context">
295 323 ${inline_comments_container(comments, inline_comments)}
296 324 </td>
297 325 </tr>
298 326 %elif c.user_session_attrs["diffmode"] == 'sideside':
299 327 % if loop.index == 0:
300 328 <tr class="cb-comment-info">
301 329 <td colspan="2"></td>
302 330 <td class="cb-line">
303 331 <div>
304 332 ${_('Unmatched inline comments below')}
305 333 </div>
306 334 </td>
307 335 <td colspan="2"></td>
308 336 <td class="cb-line">
309 337 <div>
310 338 ${_('Unmatched comments below')}
311 339 </div>
312 340 </td>
313 341 </tr>
314 342 % endif
315 343 <tr class="cb-line">
316 344 <td class="cb-data cb-context"></td>
317 345 <td class="cb-lineno cb-context"></td>
318 346 <td class="cb-content cb-context">
319 347 % if lineno.startswith('o'):
320 348 ${inline_comments_container(comments, inline_comments)}
321 349 % endif
322 350 </td>
323 351
324 352 <td class="cb-data cb-context"></td>
325 353 <td class="cb-lineno cb-context"></td>
326 354 <td class="cb-content cb-context">
327 355 % if lineno.startswith('n'):
328 356 ${inline_comments_container(comments, inline_comments)}
329 357 % endif
330 358 </td>
331 359 </tr>
332 360 %endif
333 361
334 362 % endfor
335 363
336 364 </table>
337 365 </div>
338 366 %endfor
339 367
340 368 ## outdated comments that are made for a file that has been deleted
341 369 % for filename, comments_dict in (deleted_files_comments or {}).items():
342 370 <%
343 371 display_state = 'display: none'
344 372 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
345 373 if open_comments_in_file:
346 374 display_state = ''
347 375 %>
348 376 <div class="filediffs filediff-outdated" style="${display_state}">
349 377 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
350 378 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
351 379 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
352 <div class="filediff-collapse-indicator"></div>
380 <div class="filediff-collapse-indicator icon-"></div>
353 381
354 382 <span class="pill">
355 383 ## file was deleted
356 384 ${filename}
357 385 </span>
358 386 <span class="pill-group pull-left" >
359 387 ## file op, doesn't need translation
360 388 <span class="pill" op="removed">removed in this version</span>
361 389 </span>
362 390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
363 391 <span class="pill-group pull-right">
364 392 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
365 393 </span>
366 394 </label>
367 395
368 396 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
369 397 <tr>
370 398 % if c.user_session_attrs["diffmode"] == 'unified':
371 399 <td></td>
372 400 %endif
373 401
374 402 <td></td>
375 403 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
376 404 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
377 405 </td>
378 406 </tr>
379 407 %if c.user_session_attrs["diffmode"] == 'unified':
380 408 <tr class="cb-line">
381 409 <td class="cb-data cb-context"></td>
382 410 <td class="cb-lineno cb-context"></td>
383 411 <td class="cb-lineno cb-context"></td>
384 412 <td class="cb-content cb-context">
385 413 ${inline_comments_container(comments_dict['comments'], inline_comments)}
386 414 </td>
387 415 </tr>
388 416 %elif c.user_session_attrs["diffmode"] == 'sideside':
389 417 <tr class="cb-line">
390 418 <td class="cb-data cb-context"></td>
391 419 <td class="cb-lineno cb-context"></td>
392 420 <td class="cb-content cb-context"></td>
393 421
394 422 <td class="cb-data cb-context"></td>
395 423 <td class="cb-lineno cb-context"></td>
396 424 <td class="cb-content cb-context">
397 425 ${inline_comments_container(comments_dict['comments'], inline_comments)}
398 426 </td>
399 427 </tr>
400 428 %endif
401 429 </table>
402 430 </div>
403 431 </div>
404 432 % endfor
405 433
406 434 </div>
407 435 </div>
408 436 </%def>
409 437
410 438 <%def name="diff_ops(filediff)">
411 439 <%
412 440 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
413 441 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
414 442 %>
415 443 <span class="pill">
416 444 <i class="icon-file-text"></i>
417 445 %if filediff.source_file_path and filediff.target_file_path:
418 446 %if filediff.source_file_path != filediff.target_file_path:
419 447 ## file was renamed, or copied
420 448 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
421 449 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
422 450 <% final_path = filediff.target_file_path %>
423 451 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
424 452 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
425 453 <% final_path = filediff.target_file_path %>
426 454 %endif
427 455 %else:
428 456 ## file was modified
429 457 ${filediff.source_file_path}
430 458 <% final_path = filediff.source_file_path %>
431 459 %endif
432 460 %else:
433 461 %if filediff.source_file_path:
434 462 ## file was deleted
435 463 ${filediff.source_file_path}
436 464 <% final_path = filediff.source_file_path %>
437 465 %else:
438 466 ## file was added
439 467 ${filediff.target_file_path}
440 468 <% final_path = filediff.target_file_path %>
441 469 %endif
442 470 %endif
443 471 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
444 472 </span>
445 473 ## anchor link
446 474 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
447 475
448 476 <span class="pill-group pull-right">
449 477
450 478 ## ops pills
451 479 %if filediff.limited_diff:
452 480 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
453 481 %endif
454 482
455 483 %if NEW_FILENODE in filediff.patch['stats']['ops']:
456 484 <span class="pill" op="created">created</span>
457 485 %if filediff['target_mode'].startswith('120'):
458 486 <span class="pill" op="symlink">symlink</span>
459 487 %else:
460 488 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
461 489 %endif
462 490 %endif
463 491
464 492 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
465 493 <span class="pill" op="renamed">renamed</span>
466 494 %endif
467 495
468 496 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
469 497 <span class="pill" op="copied">copied</span>
470 498 %endif
471 499
472 500 %if DEL_FILENODE in filediff.patch['stats']['ops']:
473 501 <span class="pill" op="removed">removed</span>
474 502 %endif
475 503
476 504 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
477 505 <span class="pill" op="mode">
478 506 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
479 507 </span>
480 508 %endif
481 509
482 510 %if BIN_FILENODE in filediff.patch['stats']['ops']:
483 511 <span class="pill" op="binary">binary</span>
484 512 %if MOD_FILENODE in filediff.patch['stats']['ops']:
485 513 <span class="pill" op="modified">modified</span>
486 514 %endif
487 515 %endif
488 516
489 517 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
490 518 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
491 519
492 520 </span>
493 521
494 522 </%def>
495 523
496 524 <%def name="nice_mode(filemode)">
497 525 ${(filemode.startswith('100') and filemode[3:] or filemode)}
498 526 </%def>
499 527
500 528 <%def name="diff_menu(filediff, use_comments=False)">
501 529 <div class="filediff-menu">
502 530
503 531 %if filediff.diffset.source_ref:
504 532
505 533 ## FILE BEFORE CHANGES
506 534 %if filediff.operation in ['D', 'M']:
507 535 <a
508 536 class="tooltip"
509 537 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
510 538 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
511 539 >
512 540 ${_('Show file before')}
513 541 </a> |
514 542 %else:
515 543 <span
516 544 class="tooltip"
517 545 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
518 546 >
519 547 ${_('Show file before')}
520 548 </span> |
521 549 %endif
522 550
523 551 ## FILE AFTER CHANGES
524 552 %if filediff.operation in ['A', 'M']:
525 553 <a
526 554 class="tooltip"
527 555 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
528 556 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
529 557 >
530 558 ${_('Show file after')}
531 559 </a>
532 560 %else:
533 561 <span
534 562 class="tooltip"
535 563 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
536 564 >
537 565 ${_('Show file after')}
538 566 </span>
539 567 %endif
540 568
541 569 % if use_comments:
542 570 |
543 571 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
544 572 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
545 573 </a>
546 574 % endif
547 575
548 576 %endif
549 577
550 578 </div>
551 579 </%def>
552 580
553 581
554 582 <%def name="inline_comments_container(comments, inline_comments)">
555 583 <div class="inline-comments">
556 584 %for comment in comments:
557 585 ${commentblock.comment_block(comment, inline=True)}
558 586 %endfor
559 587 % if comments and comments[-1].outdated:
560 588 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
561 589 ${_('Add another comment')}
562 590 </span>
563 591 % else:
564 592 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
565 593 ${_('Add another comment')}
566 594 </span>
567 595 % endif
568 596
569 597 </div>
570 598 </%def>
571 599
572 600 <%!
573 601 def get_comments_for(diff_type, comments, filename, line_version, line_number):
574 602 if hasattr(filename, 'unicode_path'):
575 603 filename = filename.unicode_path
576 604
577 605 if not isinstance(filename, (unicode, str)):
578 606 return None
579 607
580 608 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
581 609
582 610 if comments and filename in comments:
583 611 file_comments = comments[filename]
584 612 if line_key in file_comments:
585 613 data = file_comments.pop(line_key)
586 614 return data
587 615 %>
588 616
589 617 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
590 618 %for i, line in enumerate(hunk.sideside):
591 619 <%
592 620 old_line_anchor, new_line_anchor = None, None
593 621
594 622 if line.original.lineno:
595 623 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
596 624 if line.modified.lineno:
597 625 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
598 626 %>
599 627
600 628 <tr class="cb-line">
601 629 <td class="cb-data ${action_class(line.original.action)}"
602 630 data-line-no="${line.original.lineno}"
603 631 >
604 632 <div>
605 633
606 634 <% line_old_comments = None %>
607 635 %if line.original.get_comment_args:
608 636 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
609 637 %endif
610 638 %if line_old_comments:
611 639 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
612 640 % if has_outdated:
613 641 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
614 642 % else:
615 643 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
616 644 % endif
617 645 %endif
618 646 </div>
619 647 </td>
620 648 <td class="cb-lineno ${action_class(line.original.action)}"
621 649 data-line-no="${line.original.lineno}"
622 650 %if old_line_anchor:
623 651 id="${old_line_anchor}"
624 652 %endif
625 653 >
626 654 %if line.original.lineno:
627 655 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
628 656 %endif
629 657 </td>
630 658 <td class="cb-content ${action_class(line.original.action)}"
631 659 data-line-no="o${line.original.lineno}"
632 660 >
633 661 %if use_comments and line.original.lineno:
634 662 ${render_add_comment_button()}
635 663 %endif
636 664 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
637 665
638 666 %if use_comments and line.original.lineno and line_old_comments:
639 667 ${inline_comments_container(line_old_comments, inline_comments)}
640 668 %endif
641 669
642 670 </td>
643 671 <td class="cb-data ${action_class(line.modified.action)}"
644 672 data-line-no="${line.modified.lineno}"
645 673 >
646 674 <div>
647 675
648 676 %if line.modified.get_comment_args:
649 677 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
650 678 %else:
651 679 <% line_new_comments = None%>
652 680 %endif
653 681 %if line_new_comments:
654 682 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
655 683 % if has_outdated:
656 684 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
657 685 % else:
658 686 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
659 687 % endif
660 688 %endif
661 689 </div>
662 690 </td>
663 691 <td class="cb-lineno ${action_class(line.modified.action)}"
664 692 data-line-no="${line.modified.lineno}"
665 693 %if new_line_anchor:
666 694 id="${new_line_anchor}"
667 695 %endif
668 696 >
669 697 %if line.modified.lineno:
670 698 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
671 699 %endif
672 700 </td>
673 701 <td class="cb-content ${action_class(line.modified.action)}"
674 702 data-line-no="n${line.modified.lineno}"
675 703 >
676 704 %if use_comments and line.modified.lineno:
677 705 ${render_add_comment_button()}
678 706 %endif
679 707 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
680 708 %if use_comments and line.modified.lineno and line_new_comments:
681 709 ${inline_comments_container(line_new_comments, inline_comments)}
682 710 %endif
683 711 </td>
684 712 </tr>
685 713 %endfor
686 714 </%def>
687 715
688 716
689 717 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
690 718 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
691 719
692 720 <%
693 721 old_line_anchor, new_line_anchor = None, None
694 722 if old_line_no:
695 723 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
696 724 if new_line_no:
697 725 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
698 726 %>
699 727 <tr class="cb-line">
700 728 <td class="cb-data ${action_class(action)}">
701 729 <div>
702 730
703 731 %if comments_args:
704 732 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
705 733 %else:
706 734 <% comments = None %>
707 735 %endif
708 736
709 737 % if comments:
710 738 <% has_outdated = any([x.outdated for x in comments]) %>
711 739 % if has_outdated:
712 740 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
713 741 % else:
714 742 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
715 743 % endif
716 744 % endif
717 745 </div>
718 746 </td>
719 747 <td class="cb-lineno ${action_class(action)}"
720 748 data-line-no="${old_line_no}"
721 749 %if old_line_anchor:
722 750 id="${old_line_anchor}"
723 751 %endif
724 752 >
725 753 %if old_line_anchor:
726 754 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
727 755 %endif
728 756 </td>
729 757 <td class="cb-lineno ${action_class(action)}"
730 758 data-line-no="${new_line_no}"
731 759 %if new_line_anchor:
732 760 id="${new_line_anchor}"
733 761 %endif
734 762 >
735 763 %if new_line_anchor:
736 764 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
737 765 %endif
738 766 </td>
739 767 <td class="cb-content ${action_class(action)}"
740 768 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
741 769 >
742 770 %if use_comments:
743 771 ${render_add_comment_button()}
744 772 %endif
745 773 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
746 774 %if use_comments and comments:
747 775 ${inline_comments_container(comments, inline_comments)}
748 776 %endif
749 777 </td>
750 778 </tr>
751 779 %endfor
752 780 </%def>
753 781
754 782
755 783 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
756 784 % if diff_mode == 'unified':
757 785 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
758 786 % elif diff_mode == 'sideside':
759 787 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
760 788 % else:
761 789 <tr class="cb-line">
762 790 <td>unknown diff mode</td>
763 791 </tr>
764 792 % endif
765 793 </%def>file changes
766 794
767 795
768 796 <%def name="render_add_comment_button()">
769 797 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
770 798 <span><i class="icon-comment"></i></span>
771 799 </button>
772 800 </%def>
773 801
774 802 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
775 803 <% diffset_container_id = h.md5(diffset.target_ref) %>
776 804
777 805 <div id="diff-file-sticky" class="diffset-menu clearinner">
778 806 ## auto adjustable
779 807 <div class="sidebar__inner">
780 808 <div class="sidebar__bar">
781 809 <div class="pull-right">
782 810 <div class="btn-group">
783 811 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
784 812 <i class="icon-wide-mode"></i>
785 813 </a>
786 814 </div>
787 815 <div class="btn-group">
788 816
789 817 <a
790 818 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
791 819 title="${h.tooltip(_('View diff as side by side'))}"
792 820 href="${h.current_route_path(request, diffmode='sideside')}">
793 821 <span>${_('Side by Side')}</span>
794 822 </a>
795 823
796 824 <a
797 825 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
798 826 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
799 827 <span>${_('Unified')}</span>
800 828 </a>
801 829
802 830 % if range_diff_on is True:
803 831 <a
804 832 title="${_('Turn off: Show the diff as commit range')}"
805 833 class="btn btn-primary"
806 834 href="${h.current_route_path(request, **{"range-diff":"0"})}">
807 835 <span>${_('Range Diff')}</span>
808 836 </a>
809 837 % elif range_diff_on is False:
810 838 <a
811 839 title="${_('Show the diff as commit range')}"
812 840 class="btn"
813 841 href="${h.current_route_path(request, **{"range-diff":"1"})}">
814 842 <span>${_('Range Diff')}</span>
815 843 </a>
816 844 % endif
817 845 </div>
818 846 <div class="btn-group">
819 847
820 848 <div class="pull-left">
821 849 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
822 850 </div>
823 851
824 852 </div>
825 853 </div>
826 854 <div class="pull-left">
827 855 <div class="btn-group">
828 856 <div class="pull-left">
829 857 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
830 858 </div>
831 859
832 860 </div>
833 861 </div>
834 862 </div>
835 863 <div class="fpath-placeholder">
836 864 <i class="icon-file-text"></i>
837 865 <strong class="fpath-placeholder-text">
838 866 Context file:
839 867 </strong>
840 868 </div>
841 869 <div class="sidebar_inner_shadow"></div>
842 870 </div>
843 871 </div>
844 872
845 873 % if diffset:
846 874 %if diffset.limited_diff:
847 875 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
848 876 %else:
849 877 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
850 878 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
851 879
852 880 %endif
853 881 ## case on range-diff placeholder needs to be updated
854 882 % if range_diff_on is True:
855 883 <% file_placeholder = _('Disabled on range diff') %>
856 884 % endif
857 885
858 886 <script type="text/javascript">
859 887 var feedFilesOptions = function (query, initialData) {
860 888 var data = {results: []};
861 889 var isQuery = typeof query.term !== 'undefined';
862 890
863 891 var section = _gettext('Changed files');
864 892 var filteredData = [];
865 893
866 894 //filter results
867 895 $.each(initialData.results, function (idx, value) {
868 896
869 897 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
870 898 filteredData.push({
871 899 'id': this.id,
872 900 'text': this.text,
873 901 "ops": this.ops,
874 902 })
875 903 }
876 904
877 905 });
878 906
879 907 data.results = filteredData;
880 908
881 909 query.callback(data);
882 910 };
883 911
884 912 var selectionFormatter = function(data, escapeMarkup) {
885 913 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
886 914 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
887 915 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
888 916 '<span class="pill" op="added">{0}</span>' +
889 917 '<span class="pill" op="deleted">{1}</span>' +
890 918 '</div>'
891 919 ;
892 920 var added = data['ops']['added'];
893 921 if (added === 0) {
894 922 // don't show +0
895 923 added = 0;
896 924 } else {
897 925 added = '+' + added;
898 926 }
899 927
900 928 var deleted = -1*data['ops']['deleted'];
901 929
902 930 tmpl += pill.format(added, deleted);
903 931 return container.format(tmpl);
904 932 };
905 933 var formatFileResult = function(result, container, query, escapeMarkup) {
906 934 return selectionFormatter(result, escapeMarkup);
907 935 };
908 936
909 937 var formatSelection = function (data, container) {
910 938 return '${file_placeholder}'
911 939 };
912 940
913 941 if (window.preloadFileFilterData === undefined) {
914 942 window.preloadFileFilterData = {}
915 943 }
916 944
917 945 preloadFileFilterData["${diffset_container_id}"] = {
918 946 results: [
919 947 % for filediff in diffset.files:
920 948 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
921 949 text:"${filediff.patch['filename']}",
922 950 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
923 951 % endfor
924 952 ]
925 953 };
926 954
927 955 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
928 956 var diffFileFilter = $(diffFileFilterId).select2({
929 957 'dropdownAutoWidth': true,
930 958 'width': 'auto',
931 959
932 960 containerCssClass: "drop-menu",
933 961 dropdownCssClass: "drop-menu-dropdown",
934 962 data: preloadFileFilterData["${diffset_container_id}"],
935 963 query: function(query) {
936 964 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
937 965 },
938 966 initSelection: function(element, callback) {
939 967 callback({'init': true});
940 968 },
941 969 formatResult: formatFileResult,
942 970 formatSelection: formatSelection
943 971 });
944 972
945 973 % if range_diff_on is True:
946 974 diffFileFilter.select2("enable", false);
947 975 % endif
948 976
949 977 $(diffFileFilterId).on('select2-selecting', function (e) {
950 978 var idSelector = e.choice.id;
951 979
952 980 // expand the container if we quick-select the field
953 981 $('#'+idSelector).next().prop('checked', false);
954 982 // hide the mast as we later do preventDefault()
955 983 $("#select2-drop-mask").click();
956 984
957 985 window.location.hash = '#'+idSelector;
958 986 updateSticky();
959 987
960 988 e.preventDefault();
961 989 });
962 990
963 991 </script>
964 992 % endif
965 993
966 994 <script type="text/javascript">
967 995 $(document).ready(function () {
968 996
969 997 var contextPrefix = _gettext('Context file: ');
970 998 ## sticky sidebar
971 999 var sidebarElement = document.getElementById('diff-file-sticky');
972 1000 sidebar = new StickySidebar(sidebarElement, {
973 1001 topSpacing: 0,
974 1002 bottomSpacing: 0,
975 1003 innerWrapperSelector: '.sidebar__inner'
976 1004 });
977 1005 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
978 1006 // reset our file so it's not holding new value
979 1007 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
980 1008 });
981 1009
982 1010 updateSticky = function () {
983 1011 sidebar.updateSticky();
984 1012 Waypoint.refreshAll();
985 1013 };
986 1014
987 1015 var animateText = function (fPath, anchorId) {
988 1016 fPath = Select2.util.escapeMarkup(fPath);
989 1017 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
990 1018 };
991 1019
992 1020 ## dynamic file waypoints
993 1021 var setFPathInfo = function(fPath, anchorId){
994 1022 animateText(fPath, anchorId)
995 1023 };
996 1024
997 1025 var codeBlock = $('.filediff');
998 1026
999 1027 // forward waypoint
1000 1028 codeBlock.waypoint(
1001 1029 function(direction) {
1002 1030 if (direction === "down"){
1003 1031 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1004 1032 }
1005 1033 }, {
1006 1034 offset: function () {
1007 1035 return 70;
1008 1036 },
1009 1037 context: '.fpath-placeholder'
1010 1038 }
1011 1039 );
1012 1040
1013 1041 // backward waypoint
1014 1042 codeBlock.waypoint(
1015 1043 function(direction) {
1016 1044 if (direction === "up"){
1017 1045 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1018 1046 }
1019 1047 }, {
1020 1048 offset: function () {
1021 1049 return -this.element.clientHeight + 90;
1022 1050 },
1023 1051 context: '.fpath-placeholder'
1024 1052 }
1025 1053 );
1026 1054
1027 1055 toggleWideDiff = function (el) {
1028 1056 updateSticky();
1029 1057 var wide = Rhodecode.comments.toggleWideMode(this);
1030 1058 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1031 1059 if (wide === true) {
1032 1060 $(el).addClass('btn-active');
1033 1061 } else {
1034 1062 $(el).removeClass('btn-active');
1035 1063 }
1036 1064 return null;
1037 1065 };
1038 1066
1039 1067 var preloadDiffMenuData = {
1040 1068 results: [
1041 1069
1042 1070 ## Whitespace change
1043 1071 % if request.GET.get('ignorews', '') == '1':
1044 1072 {
1045 1073 id: 2,
1046 1074 text: _gettext('Show whitespace changes'),
1047 1075 action: function () {},
1048 1076 url: "${h.current_route_path(request, ignorews=0)|n}"
1049 1077 },
1050 1078 % else:
1051 1079 {
1052 1080 id: 2,
1053 1081 text: _gettext('Hide whitespace changes'),
1054 1082 action: function () {},
1055 1083 url: "${h.current_route_path(request, ignorews=1)|n}"
1056 1084 },
1057 1085 % endif
1058 1086
1059 1087 ## FULL CONTEXT
1060 1088 % if request.GET.get('fullcontext', '') == '1':
1061 1089 {
1062 1090 id: 3,
1063 1091 text: _gettext('Hide full context diff'),
1064 1092 action: function () {},
1065 1093 url: "${h.current_route_path(request, fullcontext=0)|n}"
1066 1094 },
1067 1095 % else:
1068 1096 {
1069 1097 id: 3,
1070 1098 text: _gettext('Show full context diff'),
1071 1099 action: function () {},
1072 1100 url: "${h.current_route_path(request, fullcontext=1)|n}"
1073 1101 },
1074 1102 % endif
1075 1103
1076 1104 ]
1077 1105 };
1078 1106
1079 1107 // get stored diff mode and pre-enable it
1080 1108 if (templateContext.session_attrs.wide_diff_mode === "true") {
1081 1109 Rhodecode.comments.toggleWideMode(null);
1082 1110 $('.toggle-wide-diff').addClass('btn-active');
1083 1111 }
1084 1112
1085 1113 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1086 1114 $(diffMenuId).select2({
1087 1115 minimumResultsForSearch: -1,
1088 1116 containerCssClass: "drop-menu-no-width",
1089 1117 dropdownCssClass: "drop-menu-dropdown",
1090 1118 dropdownAutoWidth: true,
1091 1119 data: preloadDiffMenuData,
1092 1120 placeholder: "${_('...')}",
1093 1121 });
1094 1122 $(diffMenuId).on('select2-selecting', function (e) {
1095 1123 e.choice.action();
1096 1124 if (e.choice.url !== null) {
1097 1125 window.location = e.choice.url
1098 1126 }
1099 1127 });
1100 1128 toggleExpand = function (el, diffsetEl) {
1101 1129 var el = $(el);
1102 1130 if (el.hasClass('collapsed')) {
1103 1131 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1104 1132 el.removeClass('collapsed');
1105 1133 el.html(
1106 1134 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1107 1135 _gettext('Collapse all files'));
1108 1136 }
1109 1137 else {
1110 1138 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1111 1139 el.addClass('collapsed');
1112 1140 el.html(
1113 1141 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1114 1142 _gettext('Expand all files'));
1115 1143 }
1116 1144 updateSticky()
1117 1145 }
1118 1146 });
1119 1147 </script>
1120 1148
1121 1149 </%def>
@@ -1,841 +1,814 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="breadcrumbs_links()">
13 13 <span id="pr-title">
14 14 ${c.pull_request.title}
15 15 %if c.pull_request.is_closed():
16 16 (${_('Closed')})
17 17 %endif
18 18 </span>
19 19 <div id="pr-title-edit" class="input" style="display: none;">
20 20 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
21 21 </div>
22 22 </%def>
23 23
24 24 <%def name="menu_bar_nav()">
25 25 ${self.menu_items(active='repositories')}
26 26 </%def>
27 27
28 28 <%def name="menu_bar_subnav()">
29 29 ${self.repo_menu(active='showpullrequest')}
30 30 </%def>
31 31
32 32 <%def name="main()">
33 33
34 34 <script type="text/javascript">
35 35 // TODO: marcink switch this to pyroutes
36 36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 38 </script>
39 39 <div class="box">
40 40
41 41 ${self.breadcrumbs()}
42 42
43 43 <div class="box pr-summary">
44 44
45 45 <div class="summary-details block-left">
46 46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 47 <div class="pr-details-title">
48 48 <a href="${h.route_path('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)}
49 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
52 52 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
53 53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 54 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 55 ${h.end_form()}
56 56 % else:
57 57 ${_('Delete')}
58 58 % endif
59 59 </div>
60 60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 62 %endif
63 63 </div>
64 64
65 65 <div id="summary" class="fields pr-details-content">
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Source')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div class="pr-origininfo">
72 72 ## branch link is only valid if it is a branch
73 73 <span class="tag">
74 74 %if c.pull_request.source_ref_parts.type == 'branch':
75 75 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 76 %else:
77 77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 78 %endif
79 79 </span>
80 80 <span class="clone-url">
81 81 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 82 </span>
83 83 <br/>
84 84 % if c.ancestor_commit:
85 85 ${_('Common ancestor')}:
86 86 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
87 87 % endif
88 88 </div>
89 89 %if h.is_hg(c.pull_request.source_repo):
90 90 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
91 91 %elif h.is_git(c.pull_request.source_repo):
92 92 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
93 93 %endif
94 94
95 95 <div class="">
96 96 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
97 97 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
98 98 </div>
99 99
100 100 </div>
101 101 </div>
102 102 <div class="field">
103 103 <div class="label-summary">
104 104 <label>${_('Target')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 <div class="pr-targetinfo">
108 108 ## branch link is only valid if it is a branch
109 109 <span class="tag">
110 110 %if c.pull_request.target_ref_parts.type == 'branch':
111 111 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
112 112 %else:
113 113 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
114 114 %endif
115 115 </span>
116 116 <span class="clone-url">
117 117 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
118 118 </span>
119 119 </div>
120 120 </div>
121 121 </div>
122 122
123 123 ## Link to the shadow repository.
124 124 <div class="field">
125 125 <div class="label-summary">
126 126 <label>${_('Merge')}:</label>
127 127 </div>
128 128 <div class="input">
129 129 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
134 134 %endif
135 135 <div class="">
136 136 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
137 137 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
138 138 </div>
139 139 % else:
140 140 <div class="">
141 141 ${_('Shadow repository data not available')}.
142 142 </div>
143 143 % endif
144 144 </div>
145 145 </div>
146 146
147 147 <div class="field">
148 148 <div class="label-summary">
149 149 <label>${_('Review')}:</label>
150 150 </div>
151 151 <div class="input">
152 152 %if c.pull_request_review_status:
153 153 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
154 154 <span class="changeset-status-lbl tooltip">
155 155 %if c.pull_request.is_closed():
156 156 ${_('Closed')},
157 157 %endif
158 158 ${h.commit_status_lbl(c.pull_request_review_status)}
159 159 </span>
160 160 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
161 161 %endif
162 162 </div>
163 163 </div>
164 164 <div class="field">
165 165 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
166 166 <label>${_('Description')}:</label>
167 167 </div>
168 168 <div id="pr-desc" class="input">
169 169 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer)}</div>
170 170 </div>
171 171 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
172 172 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
173 173 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
174 174 </div>
175 175 </div>
176 176
177 177 <div class="field">
178 178 <div class="label-summary">
179 179 <label>${_('Versions')}:</label>
180 180 </div>
181 181
182 182 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
183 183 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
184 184
185 185 <div class="pr-versions">
186 186 % if c.show_version_changes:
187 187 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
188 188 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
189 189 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
190 190 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
191 191 data-toggle-off="${_('Hide all versions of this pull request')}">
192 192 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
193 193 </a>
194 194 <table>
195 195 ## SHOW ALL VERSIONS OF PR
196 196 <% ver_pr = None %>
197 197
198 198 % for data in reversed(list(enumerate(c.versions, 1))):
199 199 <% ver_pos = data[0] %>
200 200 <% ver = data[1] %>
201 201 <% ver_pr = ver.pull_request_version_id %>
202 202 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
203 203
204 204 <tr class="version-pr" style="display: ${display_row}">
205 205 <td>
206 206 <code>
207 207 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
208 208 </code>
209 209 </td>
210 210 <td>
211 211 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
212 212 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
213 213 </td>
214 214 <td>
215 215 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
216 216 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
217 217 </div>
218 218 </td>
219 219 <td>
220 220 % if c.at_version_num != ver_pr:
221 221 <i class="icon-comment"></i>
222 222 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
223 223 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
224 224 </code>
225 225 % endif
226 226 </td>
227 227 <td>
228 228 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
229 229 </td>
230 230 <td>
231 231 ${h.age_component(ver.updated_on, time_is_local=True)}
232 232 </td>
233 233 </tr>
234 234 % endfor
235 235
236 236 <tr>
237 237 <td colspan="6">
238 238 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
239 239 data-label-text-locked="${_('select versions to show changes')}"
240 240 data-label-text-diff="${_('show changes between versions')}"
241 241 data-label-text-show="${_('show pull request for this version')}"
242 242 >
243 243 ${_('select versions to show changes')}
244 244 </button>
245 245 </td>
246 246 </tr>
247
248 ## show comment/inline comments summary
249 <%def name="comments_summary()">
250 <tr>
251 <td colspan="6" class="comments-summary-td">
252
253 % if c.at_version:
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
256 ${_('Comments at this version')}:
257 % else:
258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
260 ${_('Comments for this pull request')}:
261 % endif
262
263
264 %if general_comm_count_ver:
265 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
266 %else:
267 ${_("%d General ") % general_comm_count_ver}
268 %endif
269
270 %if inline_comm_count_ver:
271 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
272 %else:
273 , ${_("%d Inline") % inline_comm_count_ver}
274 %endif
275
276 %if outdated_comm_count_ver:
277 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
278 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
279 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
280 %else:
281 , ${_("%d Outdated") % outdated_comm_count_ver}
282 %endif
283 </td>
284 </tr>
285 </%def>
286 ${comments_summary()}
287 247 </table>
288 248 % else:
289 249 <div class="input">
290 250 ${_('Pull request versions not available')}.
291 251 </div>
292 <div>
293 <table>
294 ${comments_summary()}
295 </table>
296 </div>
297 252 % endif
298 253 </div>
299 254 </div>
300 255
301 256 <div id="pr-save" class="field" style="display: none;">
302 257 <div class="label-summary"></div>
303 258 <div class="input">
304 259 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
305 260 </div>
306 261 </div>
307 262 </div>
308 263 </div>
309 264 <div>
310 265 ## AUTHOR
311 266 <div class="reviewers-title block-right">
312 267 <div class="pr-details-title">
313 268 ${_('Author of this pull request')}
314 269 </div>
315 270 </div>
316 271 <div class="block-right pr-details-content reviewers">
317 272 <ul class="group_members">
318 273 <li>
319 274 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
320 275 </li>
321 276 </ul>
322 277 </div>
323 278
324 279 ## REVIEW RULES
325 280 <div id="review_rules" style="display: none" class="reviewers-title block-right">
326 281 <div class="pr-details-title">
327 282 ${_('Reviewer rules')}
328 283 %if c.allowed_to_update:
329 284 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
330 285 %endif
331 286 </div>
332 287 <div class="pr-reviewer-rules">
333 288 ## review rules will be appended here, by default reviewers logic
334 289 </div>
335 290 <input id="review_data" type="hidden" name="review_data" value="">
336 291 </div>
337 292
338 293 ## REVIEWERS
339 294 <div class="reviewers-title block-right">
340 295 <div class="pr-details-title">
341 296 ${_('Pull request reviewers')}
342 297 %if c.allowed_to_update:
343 298 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
344 299 %endif
345 300 </div>
346 301 </div>
347 302 <div id="reviewers" class="block-right pr-details-content reviewers">
348 303
349 304 ## members redering block
350 305 <input type="hidden" name="__start__" value="review_members:sequence">
351 306 <ul id="review_members" class="group_members">
352 307
353 308 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
354 309 <script>
355 310 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
356 311 var status = "${(status[0][1].status if status else 'not_reviewed')}";
357 312 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
358 313 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
359 314
360 315 var entry = renderTemplate('reviewMemberEntry', {
361 316 'member': member,
362 317 'mandatory': member.mandatory,
363 318 'reasons': member.reasons,
364 319 'allowed_to_update': allowed_to_update,
365 320 'review_status': status,
366 321 'review_status_label': status_lbl,
367 322 'user_group': member.user_group,
368 323 'create': false
369 324 });
370 325 $('#review_members').append(entry)
371 326 </script>
372 327
373 328 % endfor
374 329
375 330 </ul>
376 331 <input type="hidden" name="__end__" value="review_members:sequence">
377 332 ## end members redering block
378 333
379 334 %if not c.pull_request.is_closed():
380 335 <div id="add_reviewer" class="ac" style="display: none;">
381 336 %if c.allowed_to_update:
382 337 % if not c.forbid_adding_reviewers:
383 338 <div id="add_reviewer_input" class="reviewer_ac">
384 339 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
385 340 <div id="reviewers_container"></div>
386 341 </div>
387 342 % endif
388 343 <div class="pull-right">
389 344 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
390 345 </div>
391 346 %endif
392 347 </div>
393 348 %endif
394 349 </div>
395 350 </div>
396 351 </div>
397 352 <div class="box">
398 353 ##DIFF
399 354 <div class="table" >
400 355 <div id="changeset_compare_view_content">
401 356 ##CS
402 357 % if c.missing_requirements:
403 358 <div class="box">
404 359 <div class="alert alert-warning">
405 360 <div>
406 361 <strong>${_('Missing requirements:')}</strong>
407 362 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
408 363 </div>
409 364 </div>
410 365 </div>
411 366 % elif c.missing_commits:
412 367 <div class="box">
413 368 <div class="alert alert-warning">
414 369 <div>
415 370 <strong>${_('Missing commits')}:</strong>
416 371 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
417 372 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
418 373 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
419 374 </div>
420 375 </div>
421 376 </div>
422 377 % endif
423 378
424 379 <div class="compare_view_commits_title">
425 380 % if not c.compare_mode:
426 381
427 382 % if c.at_version_pos:
428 383 <h4>
429 384 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
430 385 </h4>
431 386 % endif
432 387
433 388 <div class="pull-left">
434 389 <div class="btn-group">
435 390 <a
436 391 class="btn"
437 392 href="#"
438 393 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
439 394 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
440 395 </a>
441 396 <a
442 397 class="btn"
443 398 href="#"
444 399 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
445 400 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
446 401 </a>
447 402 </div>
448 403 </div>
449 404
450 405 <div class="pull-right">
451 406 % if c.allowed_to_update and not c.pull_request.is_closed():
452 407 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
453 408 % else:
454 409 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
455 410 % endif
456 411
457 412 </div>
458 413 % endif
459 414 </div>
460 415
461 416 % if not c.missing_commits:
462 417 % if c.compare_mode:
463 418 % if c.at_version:
464 419 <h4>
465 420 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
466 421 </h4>
467 422
468 423 <div class="subtitle-compare">
469 424 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
470 425 </div>
471 426
472 427 <div class="container">
473 428 <table class="rctable compare_view_commits">
474 429 <tr>
475 430 <th></th>
476 431 <th>${_('Time')}</th>
477 432 <th>${_('Author')}</th>
478 433 <th>${_('Commit')}</th>
479 434 <th></th>
480 435 <th>${_('Description')}</th>
481 436 </tr>
482 437
483 438 % for c_type, commit in c.commit_changes:
484 439 % if c_type in ['a', 'r']:
485 440 <%
486 441 if c_type == 'a':
487 442 cc_title = _('Commit added in displayed changes')
488 443 elif c_type == 'r':
489 444 cc_title = _('Commit removed in displayed changes')
490 445 else:
491 446 cc_title = ''
492 447 %>
493 448 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
494 449 <td>
495 450 <div class="commit-change-indicator color-${c_type}-border">
496 451 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
497 452 ${c_type.upper()}
498 453 </div>
499 454 </div>
500 455 </td>
501 456 <td class="td-time">
502 457 ${h.age_component(commit.date)}
503 458 </td>
504 459 <td class="td-user">
505 460 ${base.gravatar_with_user(commit.author, 16)}
506 461 </td>
507 462 <td class="td-hash">
508 463 <code>
509 464 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
510 465 r${commit.idx}:${h.short_id(commit.raw_id)}
511 466 </a>
512 467 ${h.hidden('revisions', commit.raw_id)}
513 468 </code>
514 469 </td>
515 470 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
516 471 <i class="icon-expand-linked"></i>
517 472 </td>
518 473 <td class="mid td-description">
519 474 <div class="log-container truncate-wrap">
520 475 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
521 476 </div>
522 477 </td>
523 478 </tr>
524 479 % endif
525 480 % endfor
526 481 </table>
527 482 </div>
528 483
529 484 % endif
530 485
531 486 % else:
532 487 <%include file="/compare/compare_commits.mako" />
533 488 % endif
534 489
535 490 <div class="cs_files">
536 491 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
492 % if c.at_version:
493 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
494 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
495 % else:
496 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
497 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
498 % endif
499
500 <%
501 pr_menu_data = {
502 'outdated_comm_count_ver': outdated_comm_count_ver
503 }
504 %>
537 505
538 506 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
539 507
540 508 % if c.range_diff_on:
541 509 % for commit in c.commit_ranges:
542 510 ${cbdiffs.render_diffset(
543 511 c.changes[commit.raw_id],
544 512 commit=commit, use_comments=True,
545 513 collapse_when_files_over=5,
546 514 disable_new_comments=True,
547 515 deleted_files_comments=c.deleted_files_comments,
548 inline_comments=c.inline_comments)}
516 inline_comments=c.inline_comments,
517 pull_request_menu=pr_menu_data)}
549 518 % endfor
550 519 % else:
551 520 ${cbdiffs.render_diffset(
552 521 c.diffset, use_comments=True,
553 522 collapse_when_files_over=30,
554 523 disable_new_comments=not c.allowed_to_comment,
555 524 deleted_files_comments=c.deleted_files_comments,
556 inline_comments=c.inline_comments)}
525 inline_comments=c.inline_comments,
526 pull_request_menu=pr_menu_data)}
557 527 % endif
558 528
559 529 </div>
560 530 % else:
561 531 ## skipping commits we need to clear the view for missing commits
562 532 <div style="clear:both;"></div>
563 533 % endif
564 534
565 535 </div>
566 536 </div>
567 537
568 538 ## template for inline comment form
569 539 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
570 540
541 ## comments heading with count
542 <div class="comments-heading">
543 <i class="icon-comment"></i>
544 ${_('Comments')} ${len(c.comments)}
545 </div>
546
571 547 ## render general comments
572
573 548 <div id="comment-tr-show">
574 <div class="comment">
575 549 % if general_outdated_comm_count_ver:
576 <div class="meta">
550 <div class="info-box">
577 551 % if general_outdated_comm_count_ver == 1:
578 552 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
579 553 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
580 554 % else:
581 555 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
582 556 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
583 557 % endif
584 558 </div>
585 559 % endif
586 560 </div>
587 </div>
588 561
589 562 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
590 563
591 564 % if not c.pull_request.is_closed():
592 565 ## merge status, and merge action
593 566 <div class="pull-request-merge">
594 567 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
595 568 </div>
596 569
597 570 ## main comment form and it status
598 571 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
599 572 pull_request_id=c.pull_request.pull_request_id),
600 573 c.pull_request_review_status,
601 574 is_pull_request=True, change_status=c.allowed_to_change_status)}
602 575 %endif
603 576
604 577 <script type="text/javascript">
605 578 if (location.hash) {
606 579 var result = splitDelimitedHash(location.hash);
607 580 var line = $('html').find(result.loc);
608 581 // show hidden comments if we use location.hash
609 582 if (line.hasClass('comment-general')) {
610 583 $(line).show();
611 584 } else if (line.hasClass('comment-inline')) {
612 585 $(line).show();
613 586 var $cb = $(line).closest('.cb');
614 587 $cb.removeClass('cb-collapsed')
615 588 }
616 589 if (line.length > 0){
617 590 offsetScroll(line, 70);
618 591 }
619 592 }
620 593
621 594 versionController = new VersionController();
622 595 versionController.init();
623 596
624 597 reviewersController = new ReviewersController();
625 598 commitsController = new CommitsController();
626 599
627 600 $(function(){
628 601
629 602 // custom code mirror
630 603 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
631 604
632 605 var PRDetails = {
633 606 editButton: $('#open_edit_pullrequest'),
634 607 closeButton: $('#close_edit_pullrequest'),
635 608 deleteButton: $('#delete_pullrequest'),
636 609 viewFields: $('#pr-desc, #pr-title'),
637 610 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
638 611
639 612 init: function() {
640 613 var that = this;
641 614 this.editButton.on('click', function(e) { that.edit(); });
642 615 this.closeButton.on('click', function(e) { that.view(); });
643 616 },
644 617
645 618 edit: function(event) {
646 619 this.viewFields.hide();
647 620 this.editButton.hide();
648 621 this.deleteButton.hide();
649 622 this.closeButton.show();
650 623 this.editFields.show();
651 624 codeMirrorInstance.refresh();
652 625 },
653 626
654 627 view: function(event) {
655 628 this.editButton.show();
656 629 this.deleteButton.show();
657 630 this.editFields.hide();
658 631 this.closeButton.hide();
659 632 this.viewFields.show();
660 633 }
661 634 };
662 635
663 636 var ReviewersPanel = {
664 637 editButton: $('#open_edit_reviewers'),
665 638 closeButton: $('#close_edit_reviewers'),
666 639 addButton: $('#add_reviewer'),
667 640 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
668 641
669 642 init: function() {
670 643 var self = this;
671 644 this.editButton.on('click', function(e) { self.edit(); });
672 645 this.closeButton.on('click', function(e) { self.close(); });
673 646 },
674 647
675 648 edit: function(event) {
676 649 this.editButton.hide();
677 650 this.closeButton.show();
678 651 this.addButton.show();
679 652 this.removeButtons.css('visibility', 'visible');
680 653 // review rules
681 654 reviewersController.loadReviewRules(
682 655 ${c.pull_request.reviewer_data_json | n});
683 656 },
684 657
685 658 close: function(event) {
686 659 this.editButton.show();
687 660 this.closeButton.hide();
688 661 this.addButton.hide();
689 662 this.removeButtons.css('visibility', 'hidden');
690 663 // hide review rules
691 664 reviewersController.hideReviewRules()
692 665 }
693 666 };
694 667
695 668 PRDetails.init();
696 669 ReviewersPanel.init();
697 670
698 671 showOutdated = function(self){
699 672 $('.comment-inline.comment-outdated').show();
700 673 $('.filediff-outdated').show();
701 674 $('.showOutdatedComments').hide();
702 675 $('.hideOutdatedComments').show();
703 676 };
704 677
705 678 hideOutdated = function(self){
706 679 $('.comment-inline.comment-outdated').hide();
707 680 $('.filediff-outdated').hide();
708 681 $('.hideOutdatedComments').hide();
709 682 $('.showOutdatedComments').show();
710 683 };
711 684
712 685 refreshMergeChecks = function(){
713 686 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
714 687 $('.pull-request-merge').css('opacity', 0.3);
715 688 $('.action-buttons-extra').css('opacity', 0.3);
716 689
717 690 $('.pull-request-merge').load(
718 691 loadUrl, function() {
719 692 $('.pull-request-merge').css('opacity', 1);
720 693
721 694 $('.action-buttons-extra').css('opacity', 1);
722 695 injectCloseAction();
723 696 }
724 697 );
725 698 };
726 699
727 700 injectCloseAction = function() {
728 701 var closeAction = $('#close-pull-request-action').html();
729 702 var $actionButtons = $('.action-buttons-extra');
730 703 // clear the action before
731 704 $actionButtons.html("");
732 705 $actionButtons.html(closeAction);
733 706 };
734 707
735 708 closePullRequest = function (status) {
736 709 // inject closing flag
737 710 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
738 711 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
739 712 $(generalCommentForm.submitForm).submit();
740 713 };
741 714
742 715 $('#show-outdated-comments').on('click', function(e){
743 716 var button = $(this);
744 717 var outdated = $('.comment-outdated');
745 718
746 719 if (button.html() === "(Show)") {
747 720 button.html("(Hide)");
748 721 outdated.show();
749 722 } else {
750 723 button.html("(Show)");
751 724 outdated.hide();
752 725 }
753 726 });
754 727
755 728 $('.show-inline-comments').on('change', function(e){
756 729 var show = 'none';
757 730 var target = e.currentTarget;
758 731 if(target.checked){
759 732 show = ''
760 733 }
761 734 var boxid = $(target).attr('id_for');
762 735 var comments = $('#{0} .inline-comments'.format(boxid));
763 736 var fn_display = function(idx){
764 737 $(this).css('display', show);
765 738 };
766 739 $(comments).each(fn_display);
767 740 var btns = $('#{0} .inline-comments-button'.format(boxid));
768 741 $(btns).each(fn_display);
769 742 });
770 743
771 744 $('#merge_pull_request_form').submit(function() {
772 745 if (!$('#merge_pull_request').attr('disabled')) {
773 746 $('#merge_pull_request').attr('disabled', 'disabled');
774 747 }
775 748 return true;
776 749 });
777 750
778 751 $('#edit_pull_request').on('click', function(e){
779 752 var title = $('#pr-title-input').val();
780 753 var description = codeMirrorInstance.getValue();
781 754 var renderer = $('#pr-renderer-input').val();
782 755 editPullRequest(
783 756 "${c.repo_name}", "${c.pull_request.pull_request_id}",
784 757 title, description, renderer);
785 758 });
786 759
787 760 $('#update_pull_request').on('click', function(e){
788 761 $(this).attr('disabled', 'disabled');
789 762 $(this).addClass('disabled');
790 763 $(this).html(_gettext('Saving...'));
791 764 reviewersController.updateReviewers(
792 765 "${c.repo_name}", "${c.pull_request.pull_request_id}");
793 766 });
794 767
795 768 $('#update_commits').on('click', function(e){
796 769 var isDisabled = !$(e.currentTarget).attr('disabled');
797 770 $(e.currentTarget).attr('disabled', 'disabled');
798 771 $(e.currentTarget).addClass('disabled');
799 772 $(e.currentTarget).removeClass('btn-primary');
800 773 $(e.currentTarget).text(_gettext('Updating...'));
801 774 if(isDisabled){
802 775 updateCommits(
803 776 "${c.repo_name}", "${c.pull_request.pull_request_id}");
804 777 }
805 778 });
806 779 // fixing issue with caches on firefox
807 780 $('#update_commits').removeAttr("disabled");
808 781
809 782 $('.show-inline-comments').on('click', function(e){
810 783 var boxid = $(this).attr('data-comment-id');
811 784 var button = $(this);
812 785
813 786 if(button.hasClass("comments-visible")) {
814 787 $('#{0} .inline-comments'.format(boxid)).each(function(index){
815 788 $(this).hide();
816 789 });
817 790 button.removeClass("comments-visible");
818 791 } else {
819 792 $('#{0} .inline-comments'.format(boxid)).each(function(index){
820 793 $(this).show();
821 794 });
822 795 button.addClass("comments-visible");
823 796 }
824 797 });
825 798
826 799 // register submit callback on commentForm form to track TODOs
827 800 window.commentFormGlobalSubmitSuccessCallback = function(){
828 801 refreshMergeChecks();
829 802 };
830 803 // initial injection
831 804 injectCloseAction();
832 805
833 806 ReviewerAutoComplete('#user');
834 807
835 808 })
836 809 </script>
837 810
838 811 </div>
839 812 </div>
840 813
841 814 </%def>
General Comments 0
You need to be logged in to leave comments. Login now