##// END OF EJS Templates
forks: don't expose fork link if we don't have permission to read it, and also don't pre-select in pull request.
marcink -
r3367:2e466d45 default
parent child Browse files
Show More
@@ -1,680 +1,686 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 26
27 27 from rhodecode.lib import helpers as h, diffs
28 28 from rhodecode.lib.utils2 import (
29 29 StrictAttributeDict, safe_int, datetime_to_time, safe_unicode)
30 30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 31 from rhodecode.model import repo
32 32 from rhodecode.model import repo_group
33 33 from rhodecode.model import user_group
34 34 from rhodecode.model import user
35 35 from rhodecode.model.db import User
36 36 from rhodecode.model.scm import ScmModel
37 37 from rhodecode.model.settings import VcsSettingsModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 ADMIN_PREFIX = '/_admin'
43 43 STATIC_FILE_PREFIX = '/_static'
44 44
45 45 URL_NAME_REQUIREMENTS = {
46 46 # group name can have a slash in them, but they must not end with a slash
47 47 'group_name': r'.*?[^/]',
48 48 'repo_group_name': r'.*?[^/]',
49 49 # repo names can have a slash in them, but they must not end with a slash
50 50 'repo_name': r'.*?[^/]',
51 51 # file path eats up everything at the end
52 52 'f_path': r'.*',
53 53 # reference types
54 54 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
55 55 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
56 56 }
57 57
58 58
59 59 def add_route_with_slash(config,name, pattern, **kw):
60 60 config.add_route(name, pattern, **kw)
61 61 if not pattern.endswith('/'):
62 62 config.add_route(name + '_slash', pattern + '/', **kw)
63 63
64 64
65 65 def add_route_requirements(route_path, requirements=None):
66 66 """
67 67 Adds regex requirements to pyramid routes using a mapping dict
68 68 e.g::
69 69 add_route_requirements('{repo_name}/settings')
70 70 """
71 71 requirements = requirements or URL_NAME_REQUIREMENTS
72 72 for key, regex in requirements.items():
73 73 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
74 74 return route_path
75 75
76 76
77 77 def get_format_ref_id(repo):
78 78 """Returns a `repo` specific reference formatter function"""
79 79 if h.is_svn(repo):
80 80 return _format_ref_id_svn
81 81 else:
82 82 return _format_ref_id
83 83
84 84
85 85 def _format_ref_id(name, raw_id):
86 86 """Default formatting of a given reference `name`"""
87 87 return name
88 88
89 89
90 90 def _format_ref_id_svn(name, raw_id):
91 91 """Special way of formatting a reference for Subversion including path"""
92 92 return '%s@%s' % (name, raw_id)
93 93
94 94
95 95 class TemplateArgs(StrictAttributeDict):
96 96 pass
97 97
98 98
99 99 class BaseAppView(object):
100 100
101 101 def __init__(self, context, request):
102 102 self.request = request
103 103 self.context = context
104 104 self.session = request.session
105 105 if not hasattr(request, 'user'):
106 106 # NOTE(marcink): edge case, we ended up in matched route
107 107 # but probably of web-app context, e.g API CALL/VCS CALL
108 108 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
109 109 log.warning('Unable to process request `%s` in this scope', request)
110 110 raise HTTPBadRequest()
111 111
112 112 self._rhodecode_user = request.user # auth user
113 113 self._rhodecode_db_user = self._rhodecode_user.get_instance()
114 114 self._maybe_needs_password_change(
115 115 request.matched_route.name, self._rhodecode_db_user)
116 116
117 117 def _maybe_needs_password_change(self, view_name, user_obj):
118 118 log.debug('Checking if user %s needs password change on view %s',
119 119 user_obj, view_name)
120 120 skip_user_views = [
121 121 'logout', 'login',
122 122 'my_account_password', 'my_account_password_update'
123 123 ]
124 124
125 125 if not user_obj:
126 126 return
127 127
128 128 if user_obj.username == User.DEFAULT_USER:
129 129 return
130 130
131 131 now = time.time()
132 132 should_change = user_obj.user_data.get('force_password_change')
133 133 change_after = safe_int(should_change) or 0
134 134 if should_change and now > change_after:
135 135 log.debug('User %s requires password change', user_obj)
136 136 h.flash('You are required to change your password', 'warning',
137 137 ignore_duplicate=True)
138 138
139 139 if view_name not in skip_user_views:
140 140 raise HTTPFound(
141 141 self.request.route_path('my_account_password'))
142 142
143 143 def _log_creation_exception(self, e, repo_name):
144 144 _ = self.request.translate
145 145 reason = None
146 146 if len(e.args) == 2:
147 147 reason = e.args[1]
148 148
149 149 if reason == 'INVALID_CERTIFICATE':
150 150 log.exception(
151 151 'Exception creating a repository: invalid certificate')
152 152 msg = (_('Error creating repository %s: invalid certificate')
153 153 % repo_name)
154 154 else:
155 155 log.exception("Exception creating a repository")
156 156 msg = (_('Error creating repository %s')
157 157 % repo_name)
158 158 return msg
159 159
160 160 def _get_local_tmpl_context(self, include_app_defaults=True):
161 161 c = TemplateArgs()
162 162 c.auth_user = self.request.user
163 163 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
164 164 c.rhodecode_user = self.request.user
165 165
166 166 if include_app_defaults:
167 167 from rhodecode.lib.base import attach_context_attributes
168 168 attach_context_attributes(c, self.request, self.request.user.user_id)
169 169
170 170 return c
171 171
172 172 def _get_template_context(self, tmpl_args, **kwargs):
173 173
174 174 local_tmpl_args = {
175 175 'defaults': {},
176 176 'errors': {},
177 177 'c': tmpl_args
178 178 }
179 179 local_tmpl_args.update(kwargs)
180 180 return local_tmpl_args
181 181
182 182 def load_default_context(self):
183 183 """
184 184 example:
185 185
186 186 def load_default_context(self):
187 187 c = self._get_local_tmpl_context()
188 188 c.custom_var = 'foobar'
189 189
190 190 return c
191 191 """
192 192 raise NotImplementedError('Needs implementation in view class')
193 193
194 194
195 195 class RepoAppView(BaseAppView):
196 196
197 197 def __init__(self, context, request):
198 198 super(RepoAppView, self).__init__(context, request)
199 199 self.db_repo = request.db_repo
200 200 self.db_repo_name = self.db_repo.repo_name
201 201 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
202 202
203 203 def _handle_missing_requirements(self, error):
204 204 log.error(
205 205 'Requirements are missing for repository %s: %s',
206 206 self.db_repo_name, safe_unicode(error))
207 207
208 208 def _get_local_tmpl_context(self, include_app_defaults=True):
209 209 _ = self.request.translate
210 210 c = super(RepoAppView, self)._get_local_tmpl_context(
211 211 include_app_defaults=include_app_defaults)
212 212
213 213 # register common vars for this type of view
214 214 c.rhodecode_db_repo = self.db_repo
215 215 c.repo_name = self.db_repo_name
216 216 c.repository_pull_requests = self.db_repo_pull_requests
217 217 self.path_filter = PathFilter(None)
218 218
219 219 c.repository_requirements_missing = {}
220 220 try:
221 221 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
222 222 if self.rhodecode_vcs_repo:
223 223 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
224 224 c.auth_user.username)
225 225 self.path_filter = PathFilter(path_perms)
226 226 except RepositoryRequirementError as e:
227 227 c.repository_requirements_missing = {'error': str(e)}
228 228 self._handle_missing_requirements(e)
229 229 self.rhodecode_vcs_repo = None
230 230
231 231 c.path_filter = self.path_filter # used by atom_feed_entry.mako
232 232
233 233 if self.rhodecode_vcs_repo is None:
234 234 # unable to fetch this repo as vcs instance, report back to user
235 235 h.flash(_(
236 236 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
237 237 "Please check if it exist, or is not damaged.") %
238 238 {'repo_name': c.repo_name},
239 239 category='error', ignore_duplicate=True)
240 240 if c.repository_requirements_missing:
241 241 route = self.request.matched_route.name
242 242 if route.startswith(('edit_repo', 'repo_summary')):
243 243 # allow summary and edit repo on missing requirements
244 244 return c
245 245
246 246 raise HTTPFound(
247 247 h.route_path('repo_summary', repo_name=self.db_repo_name))
248 248
249 249 else: # redirect if we don't show missing requirements
250 250 raise HTTPFound(h.route_path('home'))
251 251
252 c.has_origin_repo_read_perm = False
253 if self.db_repo.fork:
254 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
255 'repository.write', 'repository.read', 'repository.admin')(
256 self.db_repo.fork.repo_name, 'summary fork link')
257
252 258 return c
253 259
254 260 def _get_f_path_unchecked(self, matchdict, default=None):
255 261 """
256 262 Should only be used by redirects, everything else should call _get_f_path
257 263 """
258 264 f_path = matchdict.get('f_path')
259 265 if f_path:
260 266 # fix for multiple initial slashes that causes errors for GIT
261 267 return f_path.lstrip('/')
262 268
263 269 return default
264 270
265 271 def _get_f_path(self, matchdict, default=None):
266 272 f_path_match = self._get_f_path_unchecked(matchdict, default)
267 273 return self.path_filter.assert_path_permissions(f_path_match)
268 274
269 275 def _get_general_setting(self, target_repo, settings_key, default=False):
270 276 settings_model = VcsSettingsModel(repo=target_repo)
271 277 settings = settings_model.get_general_settings()
272 278 return settings.get(settings_key, default)
273 279
274 280
275 281 class PathFilter(object):
276 282
277 283 # Expects and instance of BasePathPermissionChecker or None
278 284 def __init__(self, permission_checker):
279 285 self.permission_checker = permission_checker
280 286
281 287 def assert_path_permissions(self, path):
282 288 if path and self.permission_checker and not self.permission_checker.has_access(path):
283 289 raise HTTPForbidden()
284 290 return path
285 291
286 292 def filter_patchset(self, patchset):
287 293 if not self.permission_checker or not patchset:
288 294 return patchset, False
289 295 had_filtered = False
290 296 filtered_patchset = []
291 297 for patch in patchset:
292 298 filename = patch.get('filename', None)
293 299 if not filename or self.permission_checker.has_access(filename):
294 300 filtered_patchset.append(patch)
295 301 else:
296 302 had_filtered = True
297 303 if had_filtered:
298 304 if isinstance(patchset, diffs.LimitedDiffContainer):
299 305 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
300 306 return filtered_patchset, True
301 307 else:
302 308 return patchset, False
303 309
304 310 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
305 311 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
306 312 result = diffset.render_patchset(
307 313 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
308 314 result.has_hidden_changes = has_hidden_changes
309 315 return result
310 316
311 317 def get_raw_patch(self, diff_processor):
312 318 if self.permission_checker is None:
313 319 return diff_processor.as_raw()
314 320 elif self.permission_checker.has_full_access:
315 321 return diff_processor.as_raw()
316 322 else:
317 323 return '# Repository has user-specific filters, raw patch generation is disabled.'
318 324
319 325 @property
320 326 def is_enabled(self):
321 327 return self.permission_checker is not None
322 328
323 329
324 330 class RepoGroupAppView(BaseAppView):
325 331 def __init__(self, context, request):
326 332 super(RepoGroupAppView, self).__init__(context, request)
327 333 self.db_repo_group = request.db_repo_group
328 334 self.db_repo_group_name = self.db_repo_group.group_name
329 335
330 336 def _revoke_perms_on_yourself(self, form_result):
331 337 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
332 338 form_result['perm_updates'])
333 339 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
334 340 form_result['perm_additions'])
335 341 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
336 342 form_result['perm_deletions'])
337 343 admin_perm = 'group.admin'
338 344 if _updates and _updates[0][1] != admin_perm or \
339 345 _additions and _additions[0][1] != admin_perm or \
340 346 _deletions and _deletions[0][1] != admin_perm:
341 347 return True
342 348 return False
343 349
344 350
345 351 class UserGroupAppView(BaseAppView):
346 352 def __init__(self, context, request):
347 353 super(UserGroupAppView, self).__init__(context, request)
348 354 self.db_user_group = request.db_user_group
349 355 self.db_user_group_name = self.db_user_group.users_group_name
350 356
351 357
352 358 class UserAppView(BaseAppView):
353 359 def __init__(self, context, request):
354 360 super(UserAppView, self).__init__(context, request)
355 361 self.db_user = request.db_user
356 362 self.db_user_id = self.db_user.user_id
357 363
358 364 _ = self.request.translate
359 365 if not request.db_user_supports_default:
360 366 if self.db_user.username == User.DEFAULT_USER:
361 367 h.flash(_("Editing user `{}` is disabled.".format(
362 368 User.DEFAULT_USER)), category='warning')
363 369 raise HTTPFound(h.route_path('users'))
364 370
365 371
366 372 class DataGridAppView(object):
367 373 """
368 374 Common class to have re-usable grid rendering components
369 375 """
370 376
371 377 def _extract_ordering(self, request, column_map=None):
372 378 column_map = column_map or {}
373 379 column_index = safe_int(request.GET.get('order[0][column]'))
374 380 order_dir = request.GET.get(
375 381 'order[0][dir]', 'desc')
376 382 order_by = request.GET.get(
377 383 'columns[%s][data][sort]' % column_index, 'name_raw')
378 384
379 385 # translate datatable to DB columns
380 386 order_by = column_map.get(order_by) or order_by
381 387
382 388 search_q = request.GET.get('search[value]')
383 389 return search_q, order_by, order_dir
384 390
385 391 def _extract_chunk(self, request):
386 392 start = safe_int(request.GET.get('start'), 0)
387 393 length = safe_int(request.GET.get('length'), 25)
388 394 draw = safe_int(request.GET.get('draw'))
389 395 return draw, start, length
390 396
391 397 def _get_order_col(self, order_by, model):
392 398 if isinstance(order_by, basestring):
393 399 try:
394 400 return operator.attrgetter(order_by)(model)
395 401 except AttributeError:
396 402 return None
397 403 else:
398 404 return order_by
399 405
400 406
401 407 class BaseReferencesView(RepoAppView):
402 408 """
403 409 Base for reference view for branches, tags and bookmarks.
404 410 """
405 411 def load_default_context(self):
406 412 c = self._get_local_tmpl_context()
407 413
408 414
409 415 return c
410 416
411 417 def load_refs_context(self, ref_items, partials_template):
412 418 _render = self.request.get_partial_renderer(partials_template)
413 419 pre_load = ["author", "date", "message"]
414 420
415 421 is_svn = h.is_svn(self.rhodecode_vcs_repo)
416 422 is_hg = h.is_hg(self.rhodecode_vcs_repo)
417 423
418 424 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
419 425
420 426 closed_refs = {}
421 427 if is_hg:
422 428 closed_refs = self.rhodecode_vcs_repo.branches_closed
423 429
424 430 data = []
425 431 for ref_name, commit_id in ref_items:
426 432 commit = self.rhodecode_vcs_repo.get_commit(
427 433 commit_id=commit_id, pre_load=pre_load)
428 434 closed = ref_name in closed_refs
429 435
430 436 # TODO: johbo: Unify generation of reference links
431 437 use_commit_id = '/' in ref_name or is_svn
432 438
433 439 if use_commit_id:
434 440 files_url = h.route_path(
435 441 'repo_files',
436 442 repo_name=self.db_repo_name,
437 443 f_path=ref_name if is_svn else '',
438 444 commit_id=commit_id)
439 445
440 446 else:
441 447 files_url = h.route_path(
442 448 'repo_files',
443 449 repo_name=self.db_repo_name,
444 450 f_path=ref_name if is_svn else '',
445 451 commit_id=ref_name,
446 452 _query=dict(at=ref_name))
447 453
448 454 data.append({
449 455 "name": _render('name', ref_name, files_url, closed),
450 456 "name_raw": ref_name,
451 457 "date": _render('date', commit.date),
452 458 "date_raw": datetime_to_time(commit.date),
453 459 "author": _render('author', commit.author),
454 460 "commit": _render(
455 461 'commit', commit.message, commit.raw_id, commit.idx),
456 462 "commit_raw": commit.idx,
457 463 "compare": _render(
458 464 'compare', format_ref_id(ref_name, commit.raw_id)),
459 465 })
460 466
461 467 return data
462 468
463 469
464 470 class RepoRoutePredicate(object):
465 471 def __init__(self, val, config):
466 472 self.val = val
467 473
468 474 def text(self):
469 475 return 'repo_route = %s' % self.val
470 476
471 477 phash = text
472 478
473 479 def __call__(self, info, request):
474 480 if hasattr(request, 'vcs_call'):
475 481 # skip vcs calls
476 482 return
477 483
478 484 repo_name = info['match']['repo_name']
479 485 repo_model = repo.RepoModel()
480 486
481 487 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
482 488
483 489 def redirect_if_creating(route_info, db_repo):
484 490 skip_views = ['edit_repo_advanced_delete']
485 491 route = route_info['route']
486 492 # we should skip delete view so we can actually "remove" repositories
487 493 # if they get stuck in creating state.
488 494 if route.name in skip_views:
489 495 return
490 496
491 497 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
492 498 repo_creating_url = request.route_path(
493 499 'repo_creating', repo_name=db_repo.repo_name)
494 500 raise HTTPFound(repo_creating_url)
495 501
496 502 if by_name_match:
497 503 # register this as request object we can re-use later
498 504 request.db_repo = by_name_match
499 505 redirect_if_creating(info, by_name_match)
500 506 return True
501 507
502 508 by_id_match = repo_model.get_repo_by_id(repo_name)
503 509 if by_id_match:
504 510 request.db_repo = by_id_match
505 511 redirect_if_creating(info, by_id_match)
506 512 return True
507 513
508 514 return False
509 515
510 516
511 517 class RepoForbidArchivedRoutePredicate(object):
512 518 def __init__(self, val, config):
513 519 self.val = val
514 520
515 521 def text(self):
516 522 return 'repo_forbid_archived = %s' % self.val
517 523
518 524 phash = text
519 525
520 526 def __call__(self, info, request):
521 527 _ = request.translate
522 528 rhodecode_db_repo = request.db_repo
523 529
524 530 log.debug(
525 531 '%s checking if archived flag for repo for %s',
526 532 self.__class__.__name__, rhodecode_db_repo.repo_name)
527 533
528 534 if rhodecode_db_repo.archived:
529 535 log.warning('Current view is not supported for archived repo:%s',
530 536 rhodecode_db_repo.repo_name)
531 537
532 538 h.flash(
533 539 h.literal(_('Action not supported for archived repository.')),
534 540 category='warning')
535 541 summary_url = request.route_path(
536 542 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
537 543 raise HTTPFound(summary_url)
538 544 return True
539 545
540 546
541 547 class RepoTypeRoutePredicate(object):
542 548 def __init__(self, val, config):
543 549 self.val = val or ['hg', 'git', 'svn']
544 550
545 551 def text(self):
546 552 return 'repo_accepted_type = %s' % self.val
547 553
548 554 phash = text
549 555
550 556 def __call__(self, info, request):
551 557 if hasattr(request, 'vcs_call'):
552 558 # skip vcs calls
553 559 return
554 560
555 561 rhodecode_db_repo = request.db_repo
556 562
557 563 log.debug(
558 564 '%s checking repo type for %s in %s',
559 565 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
560 566
561 567 if rhodecode_db_repo.repo_type in self.val:
562 568 return True
563 569 else:
564 570 log.warning('Current view is not supported for repo type:%s',
565 571 rhodecode_db_repo.repo_type)
566 572 return False
567 573
568 574
569 575 class RepoGroupRoutePredicate(object):
570 576 def __init__(self, val, config):
571 577 self.val = val
572 578
573 579 def text(self):
574 580 return 'repo_group_route = %s' % self.val
575 581
576 582 phash = text
577 583
578 584 def __call__(self, info, request):
579 585 if hasattr(request, 'vcs_call'):
580 586 # skip vcs calls
581 587 return
582 588
583 589 repo_group_name = info['match']['repo_group_name']
584 590 repo_group_model = repo_group.RepoGroupModel()
585 591 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
586 592
587 593 if by_name_match:
588 594 # register this as request object we can re-use later
589 595 request.db_repo_group = by_name_match
590 596 return True
591 597
592 598 return False
593 599
594 600
595 601 class UserGroupRoutePredicate(object):
596 602 def __init__(self, val, config):
597 603 self.val = val
598 604
599 605 def text(self):
600 606 return 'user_group_route = %s' % self.val
601 607
602 608 phash = text
603 609
604 610 def __call__(self, info, request):
605 611 if hasattr(request, 'vcs_call'):
606 612 # skip vcs calls
607 613 return
608 614
609 615 user_group_id = info['match']['user_group_id']
610 616 user_group_model = user_group.UserGroup()
611 617 by_id_match = user_group_model.get(user_group_id, cache=False)
612 618
613 619 if by_id_match:
614 620 # register this as request object we can re-use later
615 621 request.db_user_group = by_id_match
616 622 return True
617 623
618 624 return False
619 625
620 626
621 627 class UserRoutePredicateBase(object):
622 628 supports_default = None
623 629
624 630 def __init__(self, val, config):
625 631 self.val = val
626 632
627 633 def text(self):
628 634 raise NotImplementedError()
629 635
630 636 def __call__(self, info, request):
631 637 if hasattr(request, 'vcs_call'):
632 638 # skip vcs calls
633 639 return
634 640
635 641 user_id = info['match']['user_id']
636 642 user_model = user.User()
637 643 by_id_match = user_model.get(user_id, cache=False)
638 644
639 645 if by_id_match:
640 646 # register this as request object we can re-use later
641 647 request.db_user = by_id_match
642 648 request.db_user_supports_default = self.supports_default
643 649 return True
644 650
645 651 return False
646 652
647 653
648 654 class UserRoutePredicate(UserRoutePredicateBase):
649 655 supports_default = False
650 656
651 657 def text(self):
652 658 return 'user_route = %s' % self.val
653 659
654 660 phash = text
655 661
656 662
657 663 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
658 664 supports_default = True
659 665
660 666 def text(self):
661 667 return 'user_with_default_route = %s' % self.val
662 668
663 669 phash = text
664 670
665 671
666 672 def includeme(config):
667 673 config.add_route_predicate(
668 674 'repo_route', RepoRoutePredicate)
669 675 config.add_route_predicate(
670 676 'repo_accepted_types', RepoTypeRoutePredicate)
671 677 config.add_route_predicate(
672 678 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
673 679 config.add_route_predicate(
674 680 'repo_group_route', RepoGroupRoutePredicate)
675 681 config.add_route_predicate(
676 682 'user_group_route', UserGroupRoutePredicate)
677 683 config.add_route_predicate(
678 684 'user_route_with_default', UserRouteWithDefaultPredicate)
679 685 config.add_route_predicate(
680 686 'user_route', UserRoutePredicate)
@@ -1,1412 +1,1413 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 import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name, source=source, opened_by=opened_by,
81 81 statuses=statuses, offset=start, length=limit,
82 82 order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name, source=source, statuses=statuses,
85 85 opened_by=opened_by)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, source=source, opened_by=opened_by,
89 89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by,
91 91 order_dir=order_dir)
92 92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
94 94 statuses=statuses, opened_by=opened_by)
95 95 else:
96 96 pull_requests = PullRequestModel().get_all(
97 97 repo_name, source=source, opened_by=opened_by,
98 98 statuses=statuses, offset=start, length=limit,
99 99 order_by=order_by, order_dir=order_dir)
100 100 pull_requests_total_count = PullRequestModel().count_all(
101 101 repo_name, source=source, statuses=statuses,
102 102 opened_by=opened_by)
103 103
104 104 data = []
105 105 comments_model = CommentsModel()
106 106 for pr in pull_requests:
107 107 comments = comments_model.get_all_comments(
108 108 self.db_repo.repo_id, pull_request=pr)
109 109
110 110 data.append({
111 111 'name': _render('pullrequest_name',
112 112 pr.pull_request_id, pr.target_repo.repo_name),
113 113 'name_raw': pr.pull_request_id,
114 114 'status': _render('pullrequest_status',
115 115 pr.calculated_review_status()),
116 116 'title': _render(
117 117 'pullrequest_title', pr.title, pr.description),
118 118 'description': h.escape(pr.description),
119 119 'updated_on': _render('pullrequest_updated_on',
120 120 h.datetime_to_time(pr.updated_on)),
121 121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 122 'created_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.created_on)),
124 124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 125 'author': _render('pullrequest_author',
126 126 pr.author.full_contact, ),
127 127 'author_raw': pr.author.full_name,
128 128 'comments': _render('pullrequest_comments', len(comments)),
129 129 'comments_raw': len(comments),
130 130 'closed': pr.is_closed(),
131 131 })
132 132
133 133 data = ({
134 134 'draw': draw,
135 135 'data': data,
136 136 'recordsTotal': pull_requests_total_count,
137 137 'recordsFiltered': pull_requests_total_count,
138 138 })
139 139 return data
140 140
141 141 def get_recache_flag(self):
142 142 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
143 143 flag_val = self.request.GET.get(flag_name)
144 144 if str2bool(flag_val):
145 145 return True
146 146 return False
147 147
148 148 @LoginRequired()
149 149 @HasRepoPermissionAnyDecorator(
150 150 'repository.read', 'repository.write', 'repository.admin')
151 151 @view_config(
152 152 route_name='pullrequest_show_all', request_method='GET',
153 153 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
154 154 def pull_request_list(self):
155 155 c = self.load_default_context()
156 156
157 157 req_get = self.request.GET
158 158 c.source = str2bool(req_get.get('source'))
159 159 c.closed = str2bool(req_get.get('closed'))
160 160 c.my = str2bool(req_get.get('my'))
161 161 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
162 162 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
163 163
164 164 c.active = 'open'
165 165 if c.my:
166 166 c.active = 'my'
167 167 if c.closed:
168 168 c.active = 'closed'
169 169 if c.awaiting_review and not c.source:
170 170 c.active = 'awaiting'
171 171 if c.source and not c.awaiting_review:
172 172 c.active = 'source'
173 173 if c.awaiting_my_review:
174 174 c.active = 'awaiting_my'
175 175
176 176 return self._get_template_context(c)
177 177
178 178 @LoginRequired()
179 179 @HasRepoPermissionAnyDecorator(
180 180 'repository.read', 'repository.write', 'repository.admin')
181 181 @view_config(
182 182 route_name='pullrequest_show_all_data', request_method='GET',
183 183 renderer='json_ext', xhr=True)
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 source_ref_id, target_ref_id,
221 221 target_commit, source_commit, diff_limit, file_limit,
222 222 fulldiff, hide_whitespace_changes, diff_context):
223 223
224 224 vcs_diff = PullRequestModel().get_diff(
225 225 source_repo, source_ref_id, target_ref_id,
226 226 hide_whitespace_changes, diff_context)
227 227
228 228 diff_processor = diffs.DiffProcessor(
229 229 vcs_diff, format='newdiff', diff_limit=diff_limit,
230 230 file_limit=file_limit, show_full_diff=fulldiff)
231 231
232 232 _parsed = diff_processor.prepare()
233 233
234 234 diffset = codeblocks.DiffSet(
235 235 repo_name=self.db_repo_name,
236 236 source_repo_name=source_repo_name,
237 237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
238 238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
239 239 )
240 240 diffset = self.path_filter.render_patchset_filtered(
241 241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
242 242
243 243 return diffset
244 244
245 245 def _get_range_diffset(self, source_scm, source_repo,
246 246 commit1, commit2, diff_limit, file_limit,
247 247 fulldiff, hide_whitespace_changes, diff_context):
248 248 vcs_diff = source_scm.get_diff(
249 249 commit1, commit2,
250 250 ignore_whitespace=hide_whitespace_changes,
251 251 context=diff_context)
252 252
253 253 diff_processor = diffs.DiffProcessor(
254 254 vcs_diff, format='newdiff', diff_limit=diff_limit,
255 255 file_limit=file_limit, show_full_diff=fulldiff)
256 256
257 257 _parsed = diff_processor.prepare()
258 258
259 259 diffset = codeblocks.DiffSet(
260 260 repo_name=source_repo.repo_name,
261 261 source_node_getter=codeblocks.diffset_node_getter(commit1),
262 262 target_node_getter=codeblocks.diffset_node_getter(commit2))
263 263
264 264 diffset = self.path_filter.render_patchset_filtered(
265 265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
266 266
267 267 return diffset
268 268
269 269 @LoginRequired()
270 270 @HasRepoPermissionAnyDecorator(
271 271 'repository.read', 'repository.write', 'repository.admin')
272 272 @view_config(
273 273 route_name='pullrequest_show', request_method='GET',
274 274 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
275 275 def pull_request_show(self):
276 276 pull_request_id = self.request.matchdict['pull_request_id']
277 277
278 278 c = self.load_default_context()
279 279
280 280 version = self.request.GET.get('version')
281 281 from_version = self.request.GET.get('from_version') or version
282 282 merge_checks = self.request.GET.get('merge_checks')
283 283 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
284 284
285 285 # fetch global flags of ignore ws or context lines
286 286 diff_context = diffs.get_diff_context(self.request)
287 287 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
288 288
289 289 force_refresh = str2bool(self.request.GET.get('force_refresh'))
290 290
291 291 (pull_request_latest,
292 292 pull_request_at_ver,
293 293 pull_request_display_obj,
294 294 at_version) = PullRequestModel().get_pr_version(
295 295 pull_request_id, version=version)
296 296 pr_closed = pull_request_latest.is_closed()
297 297
298 298 if pr_closed and (version or from_version):
299 299 # not allow to browse versions
300 300 raise HTTPFound(h.route_path(
301 301 'pullrequest_show', repo_name=self.db_repo_name,
302 302 pull_request_id=pull_request_id))
303 303
304 304 versions = pull_request_display_obj.versions()
305 305 # used to store per-commit range diffs
306 306 c.changes = collections.OrderedDict()
307 307 c.range_diff_on = self.request.GET.get('range-diff') == "1"
308 308
309 309 c.at_version = at_version
310 310 c.at_version_num = (at_version
311 311 if at_version and at_version != 'latest'
312 312 else None)
313 313 c.at_version_pos = ChangesetComment.get_index_from_version(
314 314 c.at_version_num, versions)
315 315
316 316 (prev_pull_request_latest,
317 317 prev_pull_request_at_ver,
318 318 prev_pull_request_display_obj,
319 319 prev_at_version) = PullRequestModel().get_pr_version(
320 320 pull_request_id, version=from_version)
321 321
322 322 c.from_version = prev_at_version
323 323 c.from_version_num = (prev_at_version
324 324 if prev_at_version and prev_at_version != 'latest'
325 325 else None)
326 326 c.from_version_pos = ChangesetComment.get_index_from_version(
327 327 c.from_version_num, versions)
328 328
329 329 # define if we're in COMPARE mode or VIEW at version mode
330 330 compare = at_version != prev_at_version
331 331
332 332 # pull_requests repo_name we opened it against
333 333 # ie. target_repo must match
334 334 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
335 335 raise HTTPNotFound()
336 336
337 337 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
338 338 pull_request_at_ver)
339 339
340 340 c.pull_request = pull_request_display_obj
341 341 c.renderer = pull_request_at_ver.description_renderer or c.renderer
342 342 c.pull_request_latest = pull_request_latest
343 343
344 344 if compare or (at_version and not at_version == 'latest'):
345 345 c.allowed_to_change_status = False
346 346 c.allowed_to_update = False
347 347 c.allowed_to_merge = False
348 348 c.allowed_to_delete = False
349 349 c.allowed_to_comment = False
350 350 c.allowed_to_close = False
351 351 else:
352 352 can_change_status = PullRequestModel().check_user_change_status(
353 353 pull_request_at_ver, self._rhodecode_user)
354 354 c.allowed_to_change_status = can_change_status and not pr_closed
355 355
356 356 c.allowed_to_update = PullRequestModel().check_user_update(
357 357 pull_request_latest, self._rhodecode_user) and not pr_closed
358 358 c.allowed_to_merge = PullRequestModel().check_user_merge(
359 359 pull_request_latest, self._rhodecode_user) and not pr_closed
360 360 c.allowed_to_delete = PullRequestModel().check_user_delete(
361 361 pull_request_latest, self._rhodecode_user) and not pr_closed
362 362 c.allowed_to_comment = not pr_closed
363 363 c.allowed_to_close = c.allowed_to_merge and not pr_closed
364 364
365 365 c.forbid_adding_reviewers = False
366 366 c.forbid_author_to_review = False
367 367 c.forbid_commit_author_to_review = False
368 368
369 369 if pull_request_latest.reviewer_data and \
370 370 'rules' in pull_request_latest.reviewer_data:
371 371 rules = pull_request_latest.reviewer_data['rules'] or {}
372 372 try:
373 373 c.forbid_adding_reviewers = rules.get(
374 374 'forbid_adding_reviewers')
375 375 c.forbid_author_to_review = rules.get(
376 376 'forbid_author_to_review')
377 377 c.forbid_commit_author_to_review = rules.get(
378 378 'forbid_commit_author_to_review')
379 379 except Exception:
380 380 pass
381 381
382 382 # check merge capabilities
383 383 _merge_check = MergeCheck.validate(
384 384 pull_request_latest, auth_user=self._rhodecode_user,
385 385 translator=self.request.translate,
386 386 force_shadow_repo_refresh=force_refresh)
387 387 c.pr_merge_errors = _merge_check.error_details
388 388 c.pr_merge_possible = not _merge_check.failed
389 389 c.pr_merge_message = _merge_check.merge_msg
390 390
391 391 c.pr_merge_info = MergeCheck.get_merge_conditions(
392 392 pull_request_latest, translator=self.request.translate)
393 393
394 394 c.pull_request_review_status = _merge_check.review_status
395 395 if merge_checks:
396 396 self.request.override_renderer = \
397 397 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
398 398 return self._get_template_context(c)
399 399
400 400 comments_model = CommentsModel()
401 401
402 402 # reviewers and statuses
403 403 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
404 404 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
405 405
406 406 # GENERAL COMMENTS with versions #
407 407 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
408 408 q = q.order_by(ChangesetComment.comment_id.asc())
409 409 general_comments = q
410 410
411 411 # pick comments we want to render at current version
412 412 c.comment_versions = comments_model.aggregate_comments(
413 413 general_comments, versions, c.at_version_num)
414 414 c.comments = c.comment_versions[c.at_version_num]['until']
415 415
416 416 # INLINE COMMENTS with versions #
417 417 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
418 418 q = q.order_by(ChangesetComment.comment_id.asc())
419 419 inline_comments = q
420 420
421 421 c.inline_versions = comments_model.aggregate_comments(
422 422 inline_comments, versions, c.at_version_num, inline=True)
423 423
424 424 # inject latest version
425 425 latest_ver = PullRequest.get_pr_display_object(
426 426 pull_request_latest, pull_request_latest)
427 427
428 428 c.versions = versions + [latest_ver]
429 429
430 430 # if we use version, then do not show later comments
431 431 # than current version
432 432 display_inline_comments = collections.defaultdict(
433 433 lambda: collections.defaultdict(list))
434 434 for co in inline_comments:
435 435 if c.at_version_num:
436 436 # pick comments that are at least UPTO given version, so we
437 437 # don't render comments for higher version
438 438 should_render = co.pull_request_version_id and \
439 439 co.pull_request_version_id <= c.at_version_num
440 440 else:
441 441 # showing all, for 'latest'
442 442 should_render = True
443 443
444 444 if should_render:
445 445 display_inline_comments[co.f_path][co.line_no].append(co)
446 446
447 447 # load diff data into template context, if we use compare mode then
448 448 # diff is calculated based on changes between versions of PR
449 449
450 450 source_repo = pull_request_at_ver.source_repo
451 451 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
452 452
453 453 target_repo = pull_request_at_ver.target_repo
454 454 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
455 455
456 456 if compare:
457 457 # in compare switch the diff base to latest commit from prev version
458 458 target_ref_id = prev_pull_request_display_obj.revisions[0]
459 459
460 460 # despite opening commits for bookmarks/branches/tags, we always
461 461 # convert this to rev to prevent changes after bookmark or branch change
462 462 c.source_ref_type = 'rev'
463 463 c.source_ref = source_ref_id
464 464
465 465 c.target_ref_type = 'rev'
466 466 c.target_ref = target_ref_id
467 467
468 468 c.source_repo = source_repo
469 469 c.target_repo = target_repo
470 470
471 471 c.commit_ranges = []
472 472 source_commit = EmptyCommit()
473 473 target_commit = EmptyCommit()
474 474 c.missing_requirements = False
475 475
476 476 source_scm = source_repo.scm_instance()
477 477 target_scm = target_repo.scm_instance()
478 478
479 479 shadow_scm = None
480 480 try:
481 481 shadow_scm = pull_request_latest.get_shadow_repo()
482 482 except Exception:
483 483 log.debug('Failed to get shadow repo', exc_info=True)
484 484 # try first the existing source_repo, and then shadow
485 485 # repo if we can obtain one
486 486 commits_source_repo = source_scm or shadow_scm
487 487
488 488 c.commits_source_repo = commits_source_repo
489 489 c.ancestor = None # set it to None, to hide it from PR view
490 490
491 491 # empty version means latest, so we keep this to prevent
492 492 # double caching
493 493 version_normalized = version or 'latest'
494 494 from_version_normalized = from_version or 'latest'
495 495
496 496 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
497 497 cache_file_path = diff_cache_exist(
498 498 cache_path, 'pull_request', pull_request_id, version_normalized,
499 499 from_version_normalized, source_ref_id, target_ref_id,
500 500 hide_whitespace_changes, diff_context, c.fulldiff)
501 501
502 502 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
503 503 force_recache = self.get_recache_flag()
504 504
505 505 cached_diff = None
506 506 if caching_enabled:
507 507 cached_diff = load_cached_diff(cache_file_path)
508 508
509 509 has_proper_commit_cache = (
510 510 cached_diff and cached_diff.get('commits')
511 511 and len(cached_diff.get('commits', [])) == 5
512 512 and cached_diff.get('commits')[0]
513 513 and cached_diff.get('commits')[3])
514 514
515 515 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
516 516 diff_commit_cache = \
517 517 (ancestor_commit, commit_cache, missing_requirements,
518 518 source_commit, target_commit) = cached_diff['commits']
519 519 else:
520 520 diff_commit_cache = \
521 521 (ancestor_commit, commit_cache, missing_requirements,
522 522 source_commit, target_commit) = self.get_commits(
523 523 commits_source_repo,
524 524 pull_request_at_ver,
525 525 source_commit,
526 526 source_ref_id,
527 527 source_scm,
528 528 target_commit,
529 529 target_ref_id,
530 530 target_scm)
531 531
532 532 # register our commit range
533 533 for comm in commit_cache.values():
534 534 c.commit_ranges.append(comm)
535 535
536 536 c.missing_requirements = missing_requirements
537 537 c.ancestor_commit = ancestor_commit
538 538 c.statuses = source_repo.statuses(
539 539 [x.raw_id for x in c.commit_ranges])
540 540
541 541 # auto collapse if we have more than limit
542 542 collapse_limit = diffs.DiffProcessor._collapse_commits_over
543 543 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
544 544 c.compare_mode = compare
545 545
546 546 # diff_limit is the old behavior, will cut off the whole diff
547 547 # if the limit is applied otherwise will just hide the
548 548 # big files from the front-end
549 549 diff_limit = c.visual.cut_off_limit_diff
550 550 file_limit = c.visual.cut_off_limit_file
551 551
552 552 c.missing_commits = False
553 553 if (c.missing_requirements
554 554 or isinstance(source_commit, EmptyCommit)
555 555 or source_commit == target_commit):
556 556
557 557 c.missing_commits = True
558 558 else:
559 559 c.inline_comments = display_inline_comments
560 560
561 561 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
562 562 if not force_recache and has_proper_diff_cache:
563 563 c.diffset = cached_diff['diff']
564 564 (ancestor_commit, commit_cache, missing_requirements,
565 565 source_commit, target_commit) = cached_diff['commits']
566 566 else:
567 567 c.diffset = self._get_diffset(
568 568 c.source_repo.repo_name, commits_source_repo,
569 569 source_ref_id, target_ref_id,
570 570 target_commit, source_commit,
571 571 diff_limit, file_limit, c.fulldiff,
572 572 hide_whitespace_changes, diff_context)
573 573
574 574 # save cached diff
575 575 if caching_enabled:
576 576 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
577 577
578 578 c.limited_diff = c.diffset.limited_diff
579 579
580 580 # calculate removed files that are bound to comments
581 581 comment_deleted_files = [
582 582 fname for fname in display_inline_comments
583 583 if fname not in c.diffset.file_stats]
584 584
585 585 c.deleted_files_comments = collections.defaultdict(dict)
586 586 for fname, per_line_comments in display_inline_comments.items():
587 587 if fname in comment_deleted_files:
588 588 c.deleted_files_comments[fname]['stats'] = 0
589 589 c.deleted_files_comments[fname]['comments'] = list()
590 590 for lno, comments in per_line_comments.items():
591 591 c.deleted_files_comments[fname]['comments'].extend(comments)
592 592
593 593 # maybe calculate the range diff
594 594 if c.range_diff_on:
595 595 # TODO(marcink): set whitespace/context
596 596 context_lcl = 3
597 597 ign_whitespace_lcl = False
598 598
599 599 for commit in c.commit_ranges:
600 600 commit2 = commit
601 601 commit1 = commit.first_parent
602 602
603 603 range_diff_cache_file_path = diff_cache_exist(
604 604 cache_path, 'diff', commit.raw_id,
605 605 ign_whitespace_lcl, context_lcl, c.fulldiff)
606 606
607 607 cached_diff = None
608 608 if caching_enabled:
609 609 cached_diff = load_cached_diff(range_diff_cache_file_path)
610 610
611 611 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
612 612 if not force_recache and has_proper_diff_cache:
613 613 diffset = cached_diff['diff']
614 614 else:
615 615 diffset = self._get_range_diffset(
616 616 source_scm, source_repo,
617 617 commit1, commit2, diff_limit, file_limit,
618 618 c.fulldiff, ign_whitespace_lcl, context_lcl
619 619 )
620 620
621 621 # save cached diff
622 622 if caching_enabled:
623 623 cache_diff(range_diff_cache_file_path, diffset, None)
624 624
625 625 c.changes[commit.raw_id] = diffset
626 626
627 627 # this is a hack to properly display links, when creating PR, the
628 628 # compare view and others uses different notation, and
629 629 # compare_commits.mako renders links based on the target_repo.
630 630 # We need to swap that here to generate it properly on the html side
631 631 c.target_repo = c.source_repo
632 632
633 633 c.commit_statuses = ChangesetStatus.STATUSES
634 634
635 635 c.show_version_changes = not pr_closed
636 636 if c.show_version_changes:
637 637 cur_obj = pull_request_at_ver
638 638 prev_obj = prev_pull_request_at_ver
639 639
640 640 old_commit_ids = prev_obj.revisions
641 641 new_commit_ids = cur_obj.revisions
642 642 commit_changes = PullRequestModel()._calculate_commit_id_changes(
643 643 old_commit_ids, new_commit_ids)
644 644 c.commit_changes_summary = commit_changes
645 645
646 646 # calculate the diff for commits between versions
647 647 c.commit_changes = []
648 648 mark = lambda cs, fw: list(
649 649 h.itertools.izip_longest([], cs, fillvalue=fw))
650 650 for c_type, raw_id in mark(commit_changes.added, 'a') \
651 651 + mark(commit_changes.removed, 'r') \
652 652 + mark(commit_changes.common, 'c'):
653 653
654 654 if raw_id in commit_cache:
655 655 commit = commit_cache[raw_id]
656 656 else:
657 657 try:
658 658 commit = commits_source_repo.get_commit(raw_id)
659 659 except CommitDoesNotExistError:
660 660 # in case we fail extracting still use "dummy" commit
661 661 # for display in commit diff
662 662 commit = h.AttributeDict(
663 663 {'raw_id': raw_id,
664 664 'message': 'EMPTY or MISSING COMMIT'})
665 665 c.commit_changes.append([c_type, commit])
666 666
667 667 # current user review statuses for each version
668 668 c.review_versions = {}
669 669 if self._rhodecode_user.user_id in allowed_reviewers:
670 670 for co in general_comments:
671 671 if co.author.user_id == self._rhodecode_user.user_id:
672 672 status = co.status_change
673 673 if status:
674 674 _ver_pr = status[0].comment.pull_request_version_id
675 675 c.review_versions[_ver_pr] = status[0]
676 676
677 677 return self._get_template_context(c)
678 678
679 679 def get_commits(
680 680 self, commits_source_repo, pull_request_at_ver, source_commit,
681 681 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
682 682 commit_cache = collections.OrderedDict()
683 683 missing_requirements = False
684 684 try:
685 685 pre_load = ["author", "branch", "date", "message", "parents"]
686 686 show_revs = pull_request_at_ver.revisions
687 687 for rev in show_revs:
688 688 comm = commits_source_repo.get_commit(
689 689 commit_id=rev, pre_load=pre_load)
690 690 commit_cache[comm.raw_id] = comm
691 691
692 692 # Order here matters, we first need to get target, and then
693 693 # the source
694 694 target_commit = commits_source_repo.get_commit(
695 695 commit_id=safe_str(target_ref_id))
696 696
697 697 source_commit = commits_source_repo.get_commit(
698 698 commit_id=safe_str(source_ref_id))
699 699 except CommitDoesNotExistError:
700 700 log.warning(
701 701 'Failed to get commit from `{}` repo'.format(
702 702 commits_source_repo), exc_info=True)
703 703 except RepositoryRequirementError:
704 704 log.warning(
705 705 'Failed to get all required data from repo', exc_info=True)
706 706 missing_requirements = True
707 707 ancestor_commit = None
708 708 try:
709 709 ancestor_id = source_scm.get_common_ancestor(
710 710 source_commit.raw_id, target_commit.raw_id, target_scm)
711 711 ancestor_commit = source_scm.get_commit(ancestor_id)
712 712 except Exception:
713 713 ancestor_commit = None
714 714 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
715 715
716 716 def assure_not_empty_repo(self):
717 717 _ = self.request.translate
718 718
719 719 try:
720 720 self.db_repo.scm_instance().get_commit()
721 721 except EmptyRepositoryError:
722 722 h.flash(h.literal(_('There are no commits yet')),
723 723 category='warning')
724 724 raise HTTPFound(
725 725 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
726 726
727 727 @LoginRequired()
728 728 @NotAnonymous()
729 729 @HasRepoPermissionAnyDecorator(
730 730 'repository.read', 'repository.write', 'repository.admin')
731 731 @view_config(
732 732 route_name='pullrequest_new', request_method='GET',
733 733 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
734 734 def pull_request_new(self):
735 735 _ = self.request.translate
736 736 c = self.load_default_context()
737 737
738 738 self.assure_not_empty_repo()
739 739 source_repo = self.db_repo
740 740
741 741 commit_id = self.request.GET.get('commit')
742 742 branch_ref = self.request.GET.get('branch')
743 743 bookmark_ref = self.request.GET.get('bookmark')
744 744
745 745 try:
746 746 source_repo_data = PullRequestModel().generate_repo_data(
747 747 source_repo, commit_id=commit_id,
748 748 branch=branch_ref, bookmark=bookmark_ref,
749 749 translator=self.request.translate)
750 750 except CommitDoesNotExistError as e:
751 751 log.exception(e)
752 752 h.flash(_('Commit does not exist'), 'error')
753 753 raise HTTPFound(
754 754 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
755 755
756 756 default_target_repo = source_repo
757 757
758 if source_repo.parent:
758 if source_repo.parent and c.has_origin_repo_read_perm:
759 759 parent_vcs_obj = source_repo.parent.scm_instance()
760 760 if parent_vcs_obj and not parent_vcs_obj.is_empty():
761 761 # change default if we have a parent repo
762 762 default_target_repo = source_repo.parent
763 763
764 764 target_repo_data = PullRequestModel().generate_repo_data(
765 765 default_target_repo, translator=self.request.translate)
766 766
767 767 selected_source_ref = source_repo_data['refs']['selected_ref']
768 768 title_source_ref = ''
769 769 if selected_source_ref:
770 770 title_source_ref = selected_source_ref.split(':', 2)[1]
771 771 c.default_title = PullRequestModel().generate_pullrequest_title(
772 772 source=source_repo.repo_name,
773 773 source_ref=title_source_ref,
774 774 target=default_target_repo.repo_name
775 775 )
776 776
777 777 c.default_repo_data = {
778 778 'source_repo_name': source_repo.repo_name,
779 779 'source_refs_json': json.dumps(source_repo_data),
780 780 'target_repo_name': default_target_repo.repo_name,
781 781 'target_refs_json': json.dumps(target_repo_data),
782 782 }
783 783 c.default_source_ref = selected_source_ref
784 784
785 785 return self._get_template_context(c)
786 786
787 787 @LoginRequired()
788 788 @NotAnonymous()
789 789 @HasRepoPermissionAnyDecorator(
790 790 'repository.read', 'repository.write', 'repository.admin')
791 791 @view_config(
792 792 route_name='pullrequest_repo_refs', request_method='GET',
793 793 renderer='json_ext', xhr=True)
794 794 def pull_request_repo_refs(self):
795 795 self.load_default_context()
796 796 target_repo_name = self.request.matchdict['target_repo_name']
797 797 repo = Repository.get_by_repo_name(target_repo_name)
798 798 if not repo:
799 799 raise HTTPNotFound()
800 800
801 801 target_perm = HasRepoPermissionAny(
802 802 'repository.read', 'repository.write', 'repository.admin')(
803 803 target_repo_name)
804 804 if not target_perm:
805 805 raise HTTPNotFound()
806 806
807 807 return PullRequestModel().generate_repo_data(
808 808 repo, translator=self.request.translate)
809 809
810 810 @LoginRequired()
811 811 @NotAnonymous()
812 812 @HasRepoPermissionAnyDecorator(
813 813 'repository.read', 'repository.write', 'repository.admin')
814 814 @view_config(
815 815 route_name='pullrequest_repo_targets', request_method='GET',
816 816 renderer='json_ext', xhr=True)
817 817 def pullrequest_repo_targets(self):
818 818 _ = self.request.translate
819 819 filter_query = self.request.GET.get('query')
820 820
821 821 # get the parents
822 822 parent_target_repos = []
823 823 if self.db_repo.parent:
824 824 parents_query = Repository.query() \
825 825 .order_by(func.length(Repository.repo_name)) \
826 826 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
827 827
828 828 if filter_query:
829 829 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
830 830 parents_query = parents_query.filter(
831 831 Repository.repo_name.ilike(ilike_expression))
832 832 parents = parents_query.limit(20).all()
833 833
834 834 for parent in parents:
835 835 parent_vcs_obj = parent.scm_instance()
836 836 if parent_vcs_obj and not parent_vcs_obj.is_empty():
837 837 parent_target_repos.append(parent)
838 838
839 839 # get other forks, and repo itself
840 840 query = Repository.query() \
841 841 .order_by(func.length(Repository.repo_name)) \
842 842 .filter(
843 843 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
844 844 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
845 845 ) \
846 846 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
847 847
848 848 if filter_query:
849 849 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
850 850 query = query.filter(Repository.repo_name.ilike(ilike_expression))
851 851
852 852 limit = max(20 - len(parent_target_repos), 5) # not less then 5
853 853 target_repos = query.limit(limit).all()
854 854
855 855 all_target_repos = target_repos + parent_target_repos
856 856
857 857 repos = []
858 # This checks permissions to the repositories
858 859 for obj in ScmModel().get_repos(all_target_repos):
859 860 repos.append({
860 861 'id': obj['name'],
861 862 'text': obj['name'],
862 863 'type': 'repo',
863 864 'repo_id': obj['dbrepo']['repo_id'],
864 865 'repo_type': obj['dbrepo']['repo_type'],
865 866 'private': obj['dbrepo']['private'],
866 867
867 868 })
868 869
869 870 data = {
870 871 'more': False,
871 872 'results': [{
872 873 'text': _('Repositories'),
873 874 'children': repos
874 875 }] if repos else []
875 876 }
876 877 return data
877 878
878 879 @LoginRequired()
879 880 @NotAnonymous()
880 881 @HasRepoPermissionAnyDecorator(
881 882 'repository.read', 'repository.write', 'repository.admin')
882 883 @CSRFRequired()
883 884 @view_config(
884 885 route_name='pullrequest_create', request_method='POST',
885 886 renderer=None)
886 887 def pull_request_create(self):
887 888 _ = self.request.translate
888 889 self.assure_not_empty_repo()
889 890 self.load_default_context()
890 891
891 892 controls = peppercorn.parse(self.request.POST.items())
892 893
893 894 try:
894 895 form = PullRequestForm(
895 896 self.request.translate, self.db_repo.repo_id)()
896 897 _form = form.to_python(controls)
897 898 except formencode.Invalid as errors:
898 899 if errors.error_dict.get('revisions'):
899 900 msg = 'Revisions: %s' % errors.error_dict['revisions']
900 901 elif errors.error_dict.get('pullrequest_title'):
901 902 msg = errors.error_dict.get('pullrequest_title')
902 903 else:
903 904 msg = _('Error creating pull request: {}').format(errors)
904 905 log.exception(msg)
905 906 h.flash(msg, 'error')
906 907
907 908 # would rather just go back to form ...
908 909 raise HTTPFound(
909 910 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
910 911
911 912 source_repo = _form['source_repo']
912 913 source_ref = _form['source_ref']
913 914 target_repo = _form['target_repo']
914 915 target_ref = _form['target_ref']
915 916 commit_ids = _form['revisions'][::-1]
916 917
917 918 # find the ancestor for this pr
918 919 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
919 920 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
920 921
921 922 # re-check permissions again here
922 923 # source_repo we must have read permissions
923 924
924 925 source_perm = HasRepoPermissionAny(
925 926 'repository.read',
926 927 'repository.write', 'repository.admin')(source_db_repo.repo_name)
927 928 if not source_perm:
928 929 msg = _('Not Enough permissions to source repo `{}`.'.format(
929 930 source_db_repo.repo_name))
930 931 h.flash(msg, category='error')
931 932 # copy the args back to redirect
932 933 org_query = self.request.GET.mixed()
933 934 raise HTTPFound(
934 935 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
935 936 _query=org_query))
936 937
937 938 # target repo we must have read permissions, and also later on
938 939 # we want to check branch permissions here
939 940 target_perm = HasRepoPermissionAny(
940 941 'repository.read',
941 942 'repository.write', 'repository.admin')(target_db_repo.repo_name)
942 943 if not target_perm:
943 944 msg = _('Not Enough permissions to target repo `{}`.'.format(
944 945 target_db_repo.repo_name))
945 946 h.flash(msg, category='error')
946 947 # copy the args back to redirect
947 948 org_query = self.request.GET.mixed()
948 949 raise HTTPFound(
949 950 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
950 951 _query=org_query))
951 952
952 953 source_scm = source_db_repo.scm_instance()
953 954 target_scm = target_db_repo.scm_instance()
954 955
955 956 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
956 957 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
957 958
958 959 ancestor = source_scm.get_common_ancestor(
959 960 source_commit.raw_id, target_commit.raw_id, target_scm)
960 961
961 962 # recalculate target ref based on ancestor
962 963 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
963 964 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
964 965
965 966 get_default_reviewers_data, validate_default_reviewers = \
966 967 PullRequestModel().get_reviewer_functions()
967 968
968 969 # recalculate reviewers logic, to make sure we can validate this
969 970 reviewer_rules = get_default_reviewers_data(
970 971 self._rhodecode_db_user, source_db_repo,
971 972 source_commit, target_db_repo, target_commit)
972 973
973 974 given_reviewers = _form['review_members']
974 975 reviewers = validate_default_reviewers(
975 976 given_reviewers, reviewer_rules)
976 977
977 978 pullrequest_title = _form['pullrequest_title']
978 979 title_source_ref = source_ref.split(':', 2)[1]
979 980 if not pullrequest_title:
980 981 pullrequest_title = PullRequestModel().generate_pullrequest_title(
981 982 source=source_repo,
982 983 source_ref=title_source_ref,
983 984 target=target_repo
984 985 )
985 986
986 987 description = _form['pullrequest_desc']
987 988 description_renderer = _form['description_renderer']
988 989
989 990 try:
990 991 pull_request = PullRequestModel().create(
991 992 created_by=self._rhodecode_user.user_id,
992 993 source_repo=source_repo,
993 994 source_ref=source_ref,
994 995 target_repo=target_repo,
995 996 target_ref=target_ref,
996 997 revisions=commit_ids,
997 998 reviewers=reviewers,
998 999 title=pullrequest_title,
999 1000 description=description,
1000 1001 description_renderer=description_renderer,
1001 1002 reviewer_data=reviewer_rules,
1002 1003 auth_user=self._rhodecode_user
1003 1004 )
1004 1005 Session().commit()
1005 1006
1006 1007 h.flash(_('Successfully opened new pull request'),
1007 1008 category='success')
1008 1009 except Exception:
1009 1010 msg = _('Error occurred during creation of this pull request.')
1010 1011 log.exception(msg)
1011 1012 h.flash(msg, category='error')
1012 1013
1013 1014 # copy the args back to redirect
1014 1015 org_query = self.request.GET.mixed()
1015 1016 raise HTTPFound(
1016 1017 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1017 1018 _query=org_query))
1018 1019
1019 1020 raise HTTPFound(
1020 1021 h.route_path('pullrequest_show', repo_name=target_repo,
1021 1022 pull_request_id=pull_request.pull_request_id))
1022 1023
1023 1024 @LoginRequired()
1024 1025 @NotAnonymous()
1025 1026 @HasRepoPermissionAnyDecorator(
1026 1027 'repository.read', 'repository.write', 'repository.admin')
1027 1028 @CSRFRequired()
1028 1029 @view_config(
1029 1030 route_name='pullrequest_update', request_method='POST',
1030 1031 renderer='json_ext')
1031 1032 def pull_request_update(self):
1032 1033 pull_request = PullRequest.get_or_404(
1033 1034 self.request.matchdict['pull_request_id'])
1034 1035 _ = self.request.translate
1035 1036
1036 1037 self.load_default_context()
1037 1038
1038 1039 if pull_request.is_closed():
1039 1040 log.debug('update: forbidden because pull request is closed')
1040 1041 msg = _(u'Cannot update closed pull requests.')
1041 1042 h.flash(msg, category='error')
1042 1043 return True
1043 1044
1044 1045 # only owner or admin can update it
1045 1046 allowed_to_update = PullRequestModel().check_user_update(
1046 1047 pull_request, self._rhodecode_user)
1047 1048 if allowed_to_update:
1048 1049 controls = peppercorn.parse(self.request.POST.items())
1049 1050
1050 1051 if 'review_members' in controls:
1051 1052 self._update_reviewers(
1052 1053 pull_request, controls['review_members'],
1053 1054 pull_request.reviewer_data)
1054 1055 elif str2bool(self.request.POST.get('update_commits', 'false')):
1055 1056 self._update_commits(pull_request)
1056 1057 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1057 1058 self._edit_pull_request(pull_request)
1058 1059 else:
1059 1060 raise HTTPBadRequest()
1060 1061 return True
1061 1062 raise HTTPForbidden()
1062 1063
1063 1064 def _edit_pull_request(self, pull_request):
1064 1065 _ = self.request.translate
1065 1066
1066 1067 try:
1067 1068 PullRequestModel().edit(
1068 1069 pull_request,
1069 1070 self.request.POST.get('title'),
1070 1071 self.request.POST.get('description'),
1071 1072 self.request.POST.get('description_renderer'),
1072 1073 self._rhodecode_user)
1073 1074 except ValueError:
1074 1075 msg = _(u'Cannot update closed pull requests.')
1075 1076 h.flash(msg, category='error')
1076 1077 return
1077 1078 else:
1078 1079 Session().commit()
1079 1080
1080 1081 msg = _(u'Pull request title & description updated.')
1081 1082 h.flash(msg, category='success')
1082 1083 return
1083 1084
1084 1085 def _update_commits(self, pull_request):
1085 1086 _ = self.request.translate
1086 1087 resp = PullRequestModel().update_commits(pull_request)
1087 1088
1088 1089 if resp.executed:
1089 1090
1090 1091 if resp.target_changed and resp.source_changed:
1091 1092 changed = 'target and source repositories'
1092 1093 elif resp.target_changed and not resp.source_changed:
1093 1094 changed = 'target repository'
1094 1095 elif not resp.target_changed and resp.source_changed:
1095 1096 changed = 'source repository'
1096 1097 else:
1097 1098 changed = 'nothing'
1098 1099
1099 1100 msg = _(
1100 1101 u'Pull request updated to "{source_commit_id}" with '
1101 1102 u'{count_added} added, {count_removed} removed commits. '
1102 1103 u'Source of changes: {change_source}')
1103 1104 msg = msg.format(
1104 1105 source_commit_id=pull_request.source_ref_parts.commit_id,
1105 1106 count_added=len(resp.changes.added),
1106 1107 count_removed=len(resp.changes.removed),
1107 1108 change_source=changed)
1108 1109 h.flash(msg, category='success')
1109 1110
1110 1111 channel = '/repo${}$/pr/{}'.format(
1111 1112 pull_request.target_repo.repo_name,
1112 1113 pull_request.pull_request_id)
1113 1114 message = msg + (
1114 1115 ' - <a onclick="window.location.reload()">'
1115 1116 '<strong>{}</strong></a>'.format(_('Reload page')))
1116 1117 channelstream.post_message(
1117 1118 channel, message, self._rhodecode_user.username,
1118 1119 registry=self.request.registry)
1119 1120 else:
1120 1121 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1121 1122 warning_reasons = [
1122 1123 UpdateFailureReason.NO_CHANGE,
1123 1124 UpdateFailureReason.WRONG_REF_TYPE,
1124 1125 ]
1125 1126 category = 'warning' if resp.reason in warning_reasons else 'error'
1126 1127 h.flash(msg, category=category)
1127 1128
1128 1129 @LoginRequired()
1129 1130 @NotAnonymous()
1130 1131 @HasRepoPermissionAnyDecorator(
1131 1132 'repository.read', 'repository.write', 'repository.admin')
1132 1133 @CSRFRequired()
1133 1134 @view_config(
1134 1135 route_name='pullrequest_merge', request_method='POST',
1135 1136 renderer='json_ext')
1136 1137 def pull_request_merge(self):
1137 1138 """
1138 1139 Merge will perform a server-side merge of the specified
1139 1140 pull request, if the pull request is approved and mergeable.
1140 1141 After successful merging, the pull request is automatically
1141 1142 closed, with a relevant comment.
1142 1143 """
1143 1144 pull_request = PullRequest.get_or_404(
1144 1145 self.request.matchdict['pull_request_id'])
1145 1146
1146 1147 self.load_default_context()
1147 1148 check = MergeCheck.validate(
1148 1149 pull_request, auth_user=self._rhodecode_user,
1149 1150 translator=self.request.translate)
1150 1151 merge_possible = not check.failed
1151 1152
1152 1153 for err_type, error_msg in check.errors:
1153 1154 h.flash(error_msg, category=err_type)
1154 1155
1155 1156 if merge_possible:
1156 1157 log.debug("Pre-conditions checked, trying to merge.")
1157 1158 extras = vcs_operation_context(
1158 1159 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1159 1160 username=self._rhodecode_db_user.username, action='push',
1160 1161 scm=pull_request.target_repo.repo_type)
1161 1162 self._merge_pull_request(
1162 1163 pull_request, self._rhodecode_db_user, extras)
1163 1164 else:
1164 1165 log.debug("Pre-conditions failed, NOT merging.")
1165 1166
1166 1167 raise HTTPFound(
1167 1168 h.route_path('pullrequest_show',
1168 1169 repo_name=pull_request.target_repo.repo_name,
1169 1170 pull_request_id=pull_request.pull_request_id))
1170 1171
1171 1172 def _merge_pull_request(self, pull_request, user, extras):
1172 1173 _ = self.request.translate
1173 1174 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1174 1175
1175 1176 if merge_resp.executed:
1176 1177 log.debug("The merge was successful, closing the pull request.")
1177 1178 PullRequestModel().close_pull_request(
1178 1179 pull_request.pull_request_id, user)
1179 1180 Session().commit()
1180 1181 msg = _('Pull request was successfully merged and closed.')
1181 1182 h.flash(msg, category='success')
1182 1183 else:
1183 1184 log.debug(
1184 1185 "The merge was not successful. Merge response: %s", merge_resp)
1185 1186 msg = merge_resp.merge_status_message
1186 1187 h.flash(msg, category='error')
1187 1188
1188 1189 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1189 1190 _ = self.request.translate
1190 1191 get_default_reviewers_data, validate_default_reviewers = \
1191 1192 PullRequestModel().get_reviewer_functions()
1192 1193
1193 1194 try:
1194 1195 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1195 1196 except ValueError as e:
1196 1197 log.error('Reviewers Validation: {}'.format(e))
1197 1198 h.flash(e, category='error')
1198 1199 return
1199 1200
1200 1201 PullRequestModel().update_reviewers(
1201 1202 pull_request, reviewers, self._rhodecode_user)
1202 1203 h.flash(_('Pull request reviewers updated.'), category='success')
1203 1204 Session().commit()
1204 1205
1205 1206 @LoginRequired()
1206 1207 @NotAnonymous()
1207 1208 @HasRepoPermissionAnyDecorator(
1208 1209 'repository.read', 'repository.write', 'repository.admin')
1209 1210 @CSRFRequired()
1210 1211 @view_config(
1211 1212 route_name='pullrequest_delete', request_method='POST',
1212 1213 renderer='json_ext')
1213 1214 def pull_request_delete(self):
1214 1215 _ = self.request.translate
1215 1216
1216 1217 pull_request = PullRequest.get_or_404(
1217 1218 self.request.matchdict['pull_request_id'])
1218 1219 self.load_default_context()
1219 1220
1220 1221 pr_closed = pull_request.is_closed()
1221 1222 allowed_to_delete = PullRequestModel().check_user_delete(
1222 1223 pull_request, self._rhodecode_user) and not pr_closed
1223 1224
1224 1225 # only owner can delete it !
1225 1226 if allowed_to_delete:
1226 1227 PullRequestModel().delete(pull_request, self._rhodecode_user)
1227 1228 Session().commit()
1228 1229 h.flash(_('Successfully deleted pull request'),
1229 1230 category='success')
1230 1231 raise HTTPFound(h.route_path('pullrequest_show_all',
1231 1232 repo_name=self.db_repo_name))
1232 1233
1233 1234 log.warning('user %s tried to delete pull request without access',
1234 1235 self._rhodecode_user)
1235 1236 raise HTTPNotFound()
1236 1237
1237 1238 @LoginRequired()
1238 1239 @NotAnonymous()
1239 1240 @HasRepoPermissionAnyDecorator(
1240 1241 'repository.read', 'repository.write', 'repository.admin')
1241 1242 @CSRFRequired()
1242 1243 @view_config(
1243 1244 route_name='pullrequest_comment_create', request_method='POST',
1244 1245 renderer='json_ext')
1245 1246 def pull_request_comment_create(self):
1246 1247 _ = self.request.translate
1247 1248
1248 1249 pull_request = PullRequest.get_or_404(
1249 1250 self.request.matchdict['pull_request_id'])
1250 1251 pull_request_id = pull_request.pull_request_id
1251 1252
1252 1253 if pull_request.is_closed():
1253 1254 log.debug('comment: forbidden because pull request is closed')
1254 1255 raise HTTPForbidden()
1255 1256
1256 1257 allowed_to_comment = PullRequestModel().check_user_comment(
1257 1258 pull_request, self._rhodecode_user)
1258 1259 if not allowed_to_comment:
1259 1260 log.debug(
1260 1261 'comment: forbidden because pull request is from forbidden repo')
1261 1262 raise HTTPForbidden()
1262 1263
1263 1264 c = self.load_default_context()
1264 1265
1265 1266 status = self.request.POST.get('changeset_status', None)
1266 1267 text = self.request.POST.get('text')
1267 1268 comment_type = self.request.POST.get('comment_type')
1268 1269 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1269 1270 close_pull_request = self.request.POST.get('close_pull_request')
1270 1271
1271 1272 # the logic here should work like following, if we submit close
1272 1273 # pr comment, use `close_pull_request_with_comment` function
1273 1274 # else handle regular comment logic
1274 1275
1275 1276 if close_pull_request:
1276 1277 # only owner or admin or person with write permissions
1277 1278 allowed_to_close = PullRequestModel().check_user_update(
1278 1279 pull_request, self._rhodecode_user)
1279 1280 if not allowed_to_close:
1280 1281 log.debug('comment: forbidden because not allowed to close '
1281 1282 'pull request %s', pull_request_id)
1282 1283 raise HTTPForbidden()
1283 1284 comment, status = PullRequestModel().close_pull_request_with_comment(
1284 1285 pull_request, self._rhodecode_user, self.db_repo, message=text,
1285 1286 auth_user=self._rhodecode_user)
1286 1287 Session().flush()
1287 1288 events.trigger(
1288 1289 events.PullRequestCommentEvent(pull_request, comment))
1289 1290
1290 1291 else:
1291 1292 # regular comment case, could be inline, or one with status.
1292 1293 # for that one we check also permissions
1293 1294
1294 1295 allowed_to_change_status = PullRequestModel().check_user_change_status(
1295 1296 pull_request, self._rhodecode_user)
1296 1297
1297 1298 if status and allowed_to_change_status:
1298 1299 message = (_('Status change %(transition_icon)s %(status)s')
1299 1300 % {'transition_icon': '>',
1300 1301 'status': ChangesetStatus.get_status_lbl(status)})
1301 1302 text = text or message
1302 1303
1303 1304 comment = CommentsModel().create(
1304 1305 text=text,
1305 1306 repo=self.db_repo.repo_id,
1306 1307 user=self._rhodecode_user.user_id,
1307 1308 pull_request=pull_request,
1308 1309 f_path=self.request.POST.get('f_path'),
1309 1310 line_no=self.request.POST.get('line'),
1310 1311 status_change=(ChangesetStatus.get_status_lbl(status)
1311 1312 if status and allowed_to_change_status else None),
1312 1313 status_change_type=(status
1313 1314 if status and allowed_to_change_status else None),
1314 1315 comment_type=comment_type,
1315 1316 resolves_comment_id=resolves_comment_id,
1316 1317 auth_user=self._rhodecode_user
1317 1318 )
1318 1319
1319 1320 if allowed_to_change_status:
1320 1321 # calculate old status before we change it
1321 1322 old_calculated_status = pull_request.calculated_review_status()
1322 1323
1323 1324 # get status if set !
1324 1325 if status:
1325 1326 ChangesetStatusModel().set_status(
1326 1327 self.db_repo.repo_id,
1327 1328 status,
1328 1329 self._rhodecode_user.user_id,
1329 1330 comment,
1330 1331 pull_request=pull_request
1331 1332 )
1332 1333
1333 1334 Session().flush()
1334 1335 # this is somehow required to get access to some relationship
1335 1336 # loaded on comment
1336 1337 Session().refresh(comment)
1337 1338
1338 1339 events.trigger(
1339 1340 events.PullRequestCommentEvent(pull_request, comment))
1340 1341
1341 1342 # we now calculate the status of pull request, and based on that
1342 1343 # calculation we set the commits status
1343 1344 calculated_status = pull_request.calculated_review_status()
1344 1345 if old_calculated_status != calculated_status:
1345 1346 PullRequestModel()._trigger_pull_request_hook(
1346 1347 pull_request, self._rhodecode_user, 'review_status_change')
1347 1348
1348 1349 Session().commit()
1349 1350
1350 1351 data = {
1351 1352 'target_id': h.safeid(h.safe_unicode(
1352 1353 self.request.POST.get('f_path'))),
1353 1354 }
1354 1355 if comment:
1355 1356 c.co = comment
1356 1357 rendered_comment = render(
1357 1358 'rhodecode:templates/changeset/changeset_comment_block.mako',
1358 1359 self._get_template_context(c), self.request)
1359 1360
1360 1361 data.update(comment.get_dict())
1361 1362 data.update({'rendered_text': rendered_comment})
1362 1363
1363 1364 return data
1364 1365
1365 1366 @LoginRequired()
1366 1367 @NotAnonymous()
1367 1368 @HasRepoPermissionAnyDecorator(
1368 1369 'repository.read', 'repository.write', 'repository.admin')
1369 1370 @CSRFRequired()
1370 1371 @view_config(
1371 1372 route_name='pullrequest_comment_delete', request_method='POST',
1372 1373 renderer='json_ext')
1373 1374 def pull_request_comment_delete(self):
1374 1375 pull_request = PullRequest.get_or_404(
1375 1376 self.request.matchdict['pull_request_id'])
1376 1377
1377 1378 comment = ChangesetComment.get_or_404(
1378 1379 self.request.matchdict['comment_id'])
1379 1380 comment_id = comment.comment_id
1380 1381
1381 1382 if pull_request.is_closed():
1382 1383 log.debug('comment: forbidden because pull request is closed')
1383 1384 raise HTTPForbidden()
1384 1385
1385 1386 if not comment:
1386 1387 log.debug('Comment with id:%s not found, skipping', comment_id)
1387 1388 # comment already deleted in another call probably
1388 1389 return True
1389 1390
1390 1391 if comment.pull_request.is_closed():
1391 1392 # don't allow deleting comments on closed pull request
1392 1393 raise HTTPForbidden()
1393 1394
1394 1395 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1395 1396 super_admin = h.HasPermissionAny('hg.admin')()
1396 1397 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1397 1398 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1398 1399 comment_repo_admin = is_repo_admin and is_repo_comment
1399 1400
1400 1401 if super_admin or comment_owner or comment_repo_admin:
1401 1402 old_calculated_status = comment.pull_request.calculated_review_status()
1402 1403 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1403 1404 Session().commit()
1404 1405 calculated_status = comment.pull_request.calculated_review_status()
1405 1406 if old_calculated_status != calculated_status:
1406 1407 PullRequestModel()._trigger_pull_request_hook(
1407 1408 comment.pull_request, self._rhodecode_user, 'review_status_change')
1408 1409 return True
1409 1410 else:
1410 1411 log.warning('No permissions for user %s to delete comment_id: %s',
1411 1412 self._rhodecode_db_user, comment_id)
1412 1413 raise HTTPNotFound()
@@ -1,696 +1,696 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 %if c.rhodecode_name:
15 15 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
16 16 %endif
17 17 </div>
18 18 <!-- MENU BAR NAV -->
19 19 ${self.menu_bar_nav()}
20 20 <!-- END MENU BAR NAV -->
21 21 </div>
22 22 </div>
23 23 ${self.menu_bar_subnav()}
24 24 <!-- END HEADER -->
25 25
26 26 <!-- CONTENT -->
27 27 <div id="content" class="wrapper">
28 28
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 30
31 31 <div class="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 </div>
38 38 <!-- FOOTER -->
39 39 <div id="footer">
40 40 <div id="footer-inner" class="title wrapper">
41 41 <div>
42 42 <p class="footer-link-right">
43 43 % if c.visual.show_version:
44 44 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
45 45 % endif
46 46 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
47 47 % if c.visual.rhodecode_support_url:
48 48 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
49 49 % endif
50 50 </p>
51 51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52 52 <p class="server-instance" style="display:${sid}">
53 53 ## display hidden instance ID if specially defined
54 54 % if c.rhodecode_instanceid:
55 55 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
56 56 % endif
57 57 </p>
58 58 </div>
59 59 </div>
60 60 </div>
61 61
62 62 <!-- END FOOTER -->
63 63
64 64 ### MAKO DEFS ###
65 65
66 66 <%def name="menu_bar_subnav()">
67 67 </%def>
68 68
69 69 <%def name="breadcrumbs(class_='breadcrumbs')">
70 70 <div class="${class_}">
71 71 ${self.breadcrumbs_links()}
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="admin_menu()">
76 76 <ul class="admin_menu submenu">
77 77 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
78 78 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
79 79 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
80 80 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
81 81 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
82 82 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
83 83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
84 84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
85 85 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
86 86 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
87 87 </ul>
88 88 </%def>
89 89
90 90
91 91 <%def name="dt_info_panel(elements)">
92 92 <dl class="dl-horizontal">
93 93 %for dt, dd, title, show_items in elements:
94 94 <dt>${dt}:</dt>
95 95 <dd title="${h.tooltip(title)}">
96 96 %if callable(dd):
97 97 ## allow lazy evaluation of elements
98 98 ${dd()}
99 99 %else:
100 100 ${dd}
101 101 %endif
102 102 %if show_items:
103 103 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
104 104 %endif
105 105 </dd>
106 106
107 107 %if show_items:
108 108 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
109 109 %for item in show_items:
110 110 <dt></dt>
111 111 <dd>${item}</dd>
112 112 %endfor
113 113 </div>
114 114 %endif
115 115
116 116 %endfor
117 117 </dl>
118 118 </%def>
119 119
120 120
121 121 <%def name="gravatar(email, size=16)">
122 122 <%
123 123 if (size > 16):
124 124 gravatar_class = 'gravatar gravatar-large'
125 125 else:
126 126 gravatar_class = 'gravatar'
127 127 %>
128 128 <%doc>
129 129 TODO: johbo: For now we serve double size images to make it smooth
130 130 for retina. This is how it worked until now. Should be replaced
131 131 with a better solution at some point.
132 132 </%doc>
133 133 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
134 134 </%def>
135 135
136 136
137 137 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
138 138 <% email = h.email_or_none(contact) %>
139 139 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
140 140 ${self.gravatar(email, size)}
141 141 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
142 142 </div>
143 143 </%def>
144 144
145 145
146 146 ## admin menu used for people that have some admin resources
147 147 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
148 148 <ul class="submenu">
149 149 %if repositories:
150 150 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
151 151 %endif
152 152 %if repository_groups:
153 153 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
154 154 %endif
155 155 %if user_groups:
156 156 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
157 157 %endif
158 158 </ul>
159 159 </%def>
160 160
161 161 <%def name="repo_page_title(repo_instance)">
162 162 <div class="title-content">
163 163 <div class="title-main">
164 164 ## SVN/HG/GIT icons
165 165 %if h.is_hg(repo_instance):
166 166 <i class="icon-hg"></i>
167 167 %endif
168 168 %if h.is_git(repo_instance):
169 169 <i class="icon-git"></i>
170 170 %endif
171 171 %if h.is_svn(repo_instance):
172 172 <i class="icon-svn"></i>
173 173 %endif
174 174
175 175 ## public/private
176 176 %if repo_instance.private:
177 177 <i class="icon-repo-private"></i>
178 178 %else:
179 179 <i class="icon-repo-public"></i>
180 180 %endif
181 181
182 182 ## repo name with group name
183 183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
184 184
185 185 </div>
186 186
187 187 ## FORKED
188 188 %if repo_instance.fork:
189 189 <p>
190 190 <i class="icon-code-fork"></i> ${_('Fork of')}
191 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
191 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
192 192 </p>
193 193 %endif
194 194
195 195 ## IMPORTED FROM REMOTE
196 196 %if repo_instance.clone_uri:
197 197 <p>
198 198 <i class="icon-code-fork"></i> ${_('Clone from')}
199 199 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
200 200 </p>
201 201 %endif
202 202
203 203 ## LOCKING STATUS
204 204 %if repo_instance.locked[0]:
205 205 <p class="locking_locked">
206 206 <i class="icon-repo-lock"></i>
207 207 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
208 208 </p>
209 209 %elif repo_instance.enable_locking:
210 210 <p class="locking_unlocked">
211 211 <i class="icon-repo-unlock"></i>
212 212 ${_('Repository not locked. Pull repository to lock it.')}
213 213 </p>
214 214 %endif
215 215
216 216 </div>
217 217 </%def>
218 218
219 219 <%def name="repo_menu(active=None)">
220 220 <%
221 221 def is_active(selected):
222 222 if selected == active:
223 223 return "active"
224 224 %>
225 225
226 226 <!--- CONTEXT BAR -->
227 227 <div id="context-bar">
228 228 <div class="wrapper">
229 229 <ul id="context-pages" class="navigation horizontal-list">
230 230 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
231 231 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
232 232 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
233 233 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li>
255 255 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
256 256 href="${h.route_path('repo_compare',
257 257 repo_name=c.rhodecode_db_repo.fork.repo_name,
258 258 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
259 259 source_ref=c.rhodecode_db_repo.landing_rev[1],
260 260 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
261 261 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
262 262 _query=dict(merge=1))}"
263 263 >
264 264 ${_('Compare fork')}
265 265 </a>
266 266 </li>
267 267 %endif
268 268
269 269 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
270 270
271 271 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
272 272 %if c.rhodecode_db_repo.locked[0]:
273 273 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
274 274 %else:
275 275 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
276 276 %endif
277 277 %endif
278 278 %if c.rhodecode_user.username != h.DEFAULT_USER:
279 279 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
280 280 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
281 281 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
282 282 %endif
283 283 %endif
284 284 </ul>
285 285 </li>
286 286 </ul>
287 287 </div>
288 288 <div class="clear"></div>
289 289 </div>
290 290 % if c.rhodecode_db_repo.archived:
291 291 <div class="alert alert-warning text-center">
292 292 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
293 293 </div>
294 294 % endif
295 295 <!--- END CONTEXT BAR -->
296 296
297 297 </%def>
298 298
299 299 <%def name="usermenu(active=False)">
300 300 ## USER MENU
301 301 <li id="quick_login_li" class="${'active' if active else ''}">
302 302 <a id="quick_login_link" class="menulink childs">
303 303 ${gravatar(c.rhodecode_user.email, 20)}
304 304 <span class="user">
305 305 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 306 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
307 307 %else:
308 308 <span>${_('Sign in')}</span>
309 309 %endif
310 310 </span>
311 311 </a>
312 312
313 313 <div class="user-menu submenu">
314 314 <div id="quick_login">
315 315 %if c.rhodecode_user.username == h.DEFAULT_USER:
316 316 <h4>${_('Sign in to your account')}</h4>
317 317 ${h.form(h.route_path('login', _query={'came_from': h.current_route_path(request)}), needs_csrf_token=False)}
318 318 <div class="form form-vertical">
319 319 <div class="fields">
320 320 <div class="field">
321 321 <div class="label">
322 322 <label for="username">${_('Username')}:</label>
323 323 </div>
324 324 <div class="input">
325 325 ${h.text('username',class_='focus',tabindex=1)}
326 326 </div>
327 327
328 328 </div>
329 329 <div class="field">
330 330 <div class="label">
331 331 <label for="password">${_('Password')}:</label>
332 332 %if h.HasPermissionAny('hg.password_reset.enabled')():
333 333 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
334 334 %endif
335 335 </div>
336 336 <div class="input">
337 337 ${h.password('password',class_='focus',tabindex=2)}
338 338 </div>
339 339 </div>
340 340 <div class="buttons">
341 341 <div class="register">
342 342 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
343 343 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
344 344 %endif
345 345 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
346 346 </div>
347 347 <div class="submit">
348 348 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
349 349 </div>
350 350 </div>
351 351 </div>
352 352 </div>
353 353 ${h.end_form()}
354 354 %else:
355 355 <div class="">
356 356 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
357 357 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
358 358 <div class="email">${c.rhodecode_user.email}</div>
359 359 </div>
360 360 <div class="">
361 361 <ol class="links">
362 362 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
363 363 % if c.rhodecode_user.personal_repo_group:
364 364 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
365 365 % endif
366 366 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
367 367
368 368 <li class="logout">
369 369 ${h.secure_form(h.route_path('logout'), request=request)}
370 370 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
371 371 ${h.end_form()}
372 372 </li>
373 373 </ol>
374 374 </div>
375 375 %endif
376 376 </div>
377 377 </div>
378 378 %if c.rhodecode_user.username != h.DEFAULT_USER:
379 379 <div class="pill_container">
380 380 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
381 381 </div>
382 382 % endif
383 383 </li>
384 384 </%def>
385 385
386 386 <%def name="menu_items(active=None)">
387 387 <%
388 388 def is_active(selected):
389 389 if selected == active:
390 390 return "active"
391 391 return ""
392 392 %>
393 393
394 394 <ul id="quick" class="main_nav navigation horizontal-list">
395 395 ## notice box for important system messages
396 396 <li style="display: none">
397 397 <a class="notice-box" href="#openNotice" onclick="showNoticeBox(); return false">
398 398 <div class="menulabel-notice" >
399 399 0
400 400 </div>
401 401 </a>
402 402 </li>
403 403
404 404 ## Main filter
405 405 <li>
406 406 <div class="menulabel main_filter_box">
407 407 <div class="main_filter_input_box">
408 408 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/>
409 409 </div>
410 410 <div class="main_filter_help_box">
411 411 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
412 412 </div>
413 413 </div>
414 414
415 415 <div id="main_filter_help" style="display: none">
416 416 Use '/' key to quickly access this field.
417 417 Enter name of repository, or repository group for quick search.
418 418
419 419 Prefix query to allow special search:
420 420
421 421 user:admin, to search for usernames
422 422
423 423 user_group:devops, to search for user groups
424 424
425 425 commit:efced4, to search for commits
426 426
427 427 </div>
428 428 </li>
429 429
430 430 ## ROOT MENU
431 431 %if c.rhodecode_user.username != h.DEFAULT_USER:
432 432 <li class="${is_active('journal')}">
433 433 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
434 434 <div class="menulabel">${_('Journal')}</div>
435 435 </a>
436 436 </li>
437 437 %else:
438 438 <li class="${is_active('journal')}">
439 439 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
440 440 <div class="menulabel">${_('Public journal')}</div>
441 441 </a>
442 442 </li>
443 443 %endif
444 444 <li class="${is_active('gists')}">
445 445 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
446 446 <div class="menulabel">${_('Gists')}</div>
447 447 </a>
448 448 </li>
449 449 <li class="${is_active('search')}">
450 450 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
451 451 <div class="menulabel">${_('Search')}</div>
452 452 </a>
453 453 </li>
454 454 % if h.HasPermissionAll('hg.admin')('access admin main page'):
455 455 <li class="${is_active('admin')}">
456 456 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
457 457 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
458 458 </a>
459 459 ${admin_menu()}
460 460 </li>
461 461 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
462 462 <li class="${is_active('admin')}">
463 463 <a class="menulink childs" title="${_('Delegated Admin settings')}">
464 464 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
465 465 </a>
466 466 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
467 467 c.rhodecode_user.repository_groups_admin,
468 468 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
469 469 </li>
470 470 % endif
471 471 ## render extra user menu
472 472 ${usermenu(active=(active=='my_account'))}
473 473
474 474 % if c.debug_style:
475 475 <li>
476 476 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
477 477 <div class="menulabel">${_('[Style]')}</div>
478 478 </a>
479 479 </li>
480 480 % endif
481 481 </ul>
482 482
483 483 <script type="text/javascript">
484 484 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
485 485
486 486 var formatRepoResult = function(result, container, query, escapeMarkup) {
487 487 return function(data, escapeMarkup) {
488 488 if (!data.repo_id){
489 489 return data.text; // optgroup text Repositories
490 490 }
491 491
492 492 var tmpl = '';
493 493 var repoType = data['repo_type'];
494 494 var repoName = data['text'];
495 495
496 496 if(data && data.type == 'repo'){
497 497 if(repoType === 'hg'){
498 498 tmpl += '<i class="icon-hg"></i> ';
499 499 }
500 500 else if(repoType === 'git'){
501 501 tmpl += '<i class="icon-git"></i> ';
502 502 }
503 503 else if(repoType === 'svn'){
504 504 tmpl += '<i class="icon-svn"></i> ';
505 505 }
506 506 if(data['private']){
507 507 tmpl += '<i class="icon-lock" ></i> ';
508 508 }
509 509 else if(visualShowPublicIcon){
510 510 tmpl += '<i class="icon-unlock-alt"></i> ';
511 511 }
512 512 }
513 513 tmpl += escapeMarkup(repoName);
514 514 return tmpl;
515 515
516 516 }(result, escapeMarkup);
517 517 };
518 518
519 519
520 520 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
521 521
522 522 if (value.split(':').length === 2) {
523 523 value = value.split(':')[1]
524 524 }
525 525
526 526 var searchType = data['type'];
527 527 var valueDisplay = data['value_display'];
528 528
529 529 var escapeRegExChars = function (value) {
530 530 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
531 531 };
532 532 var pattern = '(' + escapeRegExChars(value) + ')';
533 533
534 534 // highlight match
535 535 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
536 536 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
537 537
538 538 var icon = '';
539 539
540 540 if (searchType === 'hint') {
541 541 icon += '<i class="icon-folder-close"></i> ';
542 542 }
543 543 else if (searchType === 'search') {
544 544 icon += '<i class="icon-more"></i> ';
545 545 }
546 546 else if (searchType === 'repo') {
547 547 if (data['repo_type'] === 'hg') {
548 548 icon += '<i class="icon-hg"></i> ';
549 549 }
550 550 else if (data['repo_type'] === 'git') {
551 551 icon += '<i class="icon-git"></i> ';
552 552 }
553 553 else if (data['repo_type'] === 'svn') {
554 554 icon += '<i class="icon-svn"></i> ';
555 555 }
556 556 if (data['private']) {
557 557 icon += '<i class="icon-lock" ></i> ';
558 558 }
559 559 else if (visualShowPublicIcon) {
560 560 icon += '<i class="icon-unlock-alt"></i> ';
561 561 }
562 562 }
563 563 else if (searchType === 'repo_group') {
564 564 icon += '<i class="icon-folder-close"></i> ';
565 565 }
566 566 else if (searchType === 'user_group') {
567 567 icon += '<i class="icon-group"></i> ';
568 568 }
569 569 else if (searchType === 'user') {
570 570 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
571 571 }
572 572 else if (searchType === 'commit') {
573 573 icon += '<i class="icon-tag"></i>';
574 574 }
575 575
576 576 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
577 577 return tmpl.format(icon, valueDisplay);
578 578 };
579 579
580 580 var handleSelect = function(element, suggestion) {
581 581 if (suggestion.type === "hint") {
582 582 // we skip action
583 583 $('#main_filter').focus();
584 584 } else {
585 585 window.location = suggestion['url'];
586 586 }
587 587 };
588 588 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
589 589 if (queryLowerCase.split(':').length === 2) {
590 590 queryLowerCase = queryLowerCase.split(':')[1]
591 591 }
592 592 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
593 593 };
594 594
595 595 $('#main_filter').autocomplete({
596 596 serviceUrl: pyroutes.url('goto_switcher_data'),
597 597 params: {"search_context": templateContext.search_context},
598 598 minChars:2,
599 599 maxHeight:400,
600 600 deferRequestBy: 300, //miliseconds
601 601 tabDisabled: true,
602 602 autoSelectFirst: true,
603 603 formatResult: autocompleteMainFilterFormatResult,
604 604 lookupFilter: autocompleteMainFilterResult,
605 605 onSelect: function (element, suggestion) {
606 606 handleSelect(element, suggestion);
607 607 return false;
608 608 },
609 609 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
610 610 if (jqXHR !== 'abort') {
611 611 alert("Error during search.\nError code: {0}".format(textStatus));
612 612 window.location = '';
613 613 }
614 614 }
615 615 });
616 616
617 617 showMainFilterBox = function () {
618 618 $('#main_filter_help').toggle();
619 619 }
620 620
621 621 </script>
622 622 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
623 623 </%def>
624 624
625 625 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
626 626 <div class="modal-dialog">
627 627 <div class="modal-content">
628 628 <div class="modal-header">
629 629 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
630 630 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
631 631 </div>
632 632 <div class="modal-body">
633 633 <div class="block-left">
634 634 <table class="keyboard-mappings">
635 635 <tbody>
636 636 <tr>
637 637 <th></th>
638 638 <th>${_('Site-wide shortcuts')}</th>
639 639 </tr>
640 640 <%
641 641 elems = [
642 642 ('/', 'Use quick search box'),
643 643 ('g h', 'Goto home page'),
644 644 ('g g', 'Goto my private gists page'),
645 645 ('g G', 'Goto my public gists page'),
646 646 ('n r', 'New repository page'),
647 647 ('n g', 'New gist page'),
648 648 ]
649 649 %>
650 650 %for key, desc in elems:
651 651 <tr>
652 652 <td class="keys">
653 653 <span class="key tag">${key}</span>
654 654 </td>
655 655 <td>${desc}</td>
656 656 </tr>
657 657 %endfor
658 658 </tbody>
659 659 </table>
660 660 </div>
661 661 <div class="block-left">
662 662 <table class="keyboard-mappings">
663 663 <tbody>
664 664 <tr>
665 665 <th></th>
666 666 <th>${_('Repositories')}</th>
667 667 </tr>
668 668 <%
669 669 elems = [
670 670 ('g s', 'Goto summary page'),
671 671 ('g c', 'Goto changelog page'),
672 672 ('g f', 'Goto files page'),
673 673 ('g F', 'Goto files page with file search activated'),
674 674 ('g p', 'Goto pull requests page'),
675 675 ('g o', 'Goto repository settings'),
676 676 ('g O', 'Goto repository permissions settings'),
677 677 ]
678 678 %>
679 679 %for key, desc in elems:
680 680 <tr>
681 681 <td class="keys">
682 682 <span class="key tag">${key}</span>
683 683 </td>
684 684 <td>${desc}</td>
685 685 </tr>
686 686 %endfor
687 687 </tbody>
688 688 </table>
689 689 </div>
690 690 </div>
691 691 <div class="modal-footer">
692 692 </div>
693 693 </div><!-- /.modal-content -->
694 694 </div><!-- /.modal-dialog -->
695 695 </div><!-- /.modal -->
696 696
General Comments 0
You need to be logged in to leave comments. Login now