##// END OF EJS Templates
apps: modernize for python3
super-admin -
r5093:525812a8 default
parent child Browse files
Show More

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

@@ -1,19 +1,17 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,860 +1,858 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import time
22 20 import logging
23 21 import operator
24 22
25 23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 24
27 25 from rhodecode.lib import helpers as h, diffs, rc_cache
28 26 from rhodecode.lib.str_utils import safe_str
29 27 from rhodecode.lib.utils import repo_name_slug
30 28 from rhodecode.lib.utils2 import (
31 29 StrictAttributeDict, str2bool, safe_int, datetime_to_time)
32 30 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
33 31 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 32 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
35 33 from rhodecode.model import repo
36 34 from rhodecode.model import repo_group
37 35 from rhodecode.model import user_group
38 36 from rhodecode.model import user
39 37 from rhodecode.model.db import User
40 38 from rhodecode.model.scm import ScmModel
41 39 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
42 40 from rhodecode.model.repo import ReadmeFinder
43 41
44 42 log = logging.getLogger(__name__)
45 43
46 44
47 45 ADMIN_PREFIX = '/_admin'
48 46 STATIC_FILE_PREFIX = '/_static'
49 47
50 48 URL_NAME_REQUIREMENTS = {
51 49 # group name can have a slash in them, but they must not end with a slash
52 50 'group_name': r'.*?[^/]',
53 51 'repo_group_name': r'.*?[^/]',
54 52 # repo names can have a slash in them, but they must not end with a slash
55 53 'repo_name': r'.*?[^/]',
56 54 # file path eats up everything at the end
57 55 'f_path': r'.*',
58 56 # reference types
59 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
60 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
57 'source_ref_type': r'(branch|book|tag|rev|\%\(source_ref_type\)s)',
58 'target_ref_type': r'(branch|book|tag|rev|\%\(target_ref_type\)s)',
61 59 }
62 60
63 61
64 62 def add_route_with_slash(config,name, pattern, **kw):
65 63 config.add_route(name, pattern, **kw)
66 64 if not pattern.endswith('/'):
67 65 config.add_route(name + '_slash', pattern + '/', **kw)
68 66
69 67
70 68 def add_route_requirements(route_path, requirements=None):
71 69 """
72 70 Adds regex requirements to pyramid routes using a mapping dict
73 71 e.g::
74 72 add_route_requirements('{repo_name}/settings')
75 73 """
76 74 requirements = requirements or URL_NAME_REQUIREMENTS
77 75 for key, regex in list(requirements.items()):
78 76 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
79 77 return route_path
80 78
81 79
82 80 def get_format_ref_id(repo):
83 81 """Returns a `repo` specific reference formatter function"""
84 82 if h.is_svn(repo):
85 83 return _format_ref_id_svn
86 84 else:
87 85 return _format_ref_id
88 86
89 87
90 88 def _format_ref_id(name, raw_id):
91 89 """Default formatting of a given reference `name`"""
92 90 return name
93 91
94 92
95 93 def _format_ref_id_svn(name, raw_id):
96 94 """Special way of formatting a reference for Subversion including path"""
97 return '%s@%s' % (name, raw_id)
95 return '{}@{}'.format(name, raw_id)
98 96
99 97
100 98 class TemplateArgs(StrictAttributeDict):
101 99 pass
102 100
103 101
104 102 class BaseAppView(object):
105 103
106 104 def __init__(self, context, request):
107 105 self.request = request
108 106 self.context = context
109 107 self.session = request.session
110 108 if not hasattr(request, 'user'):
111 109 # NOTE(marcink): edge case, we ended up in matched route
112 110 # but probably of web-app context, e.g API CALL/VCS CALL
113 111 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
114 112 log.warning('Unable to process request `%s` in this scope', request)
115 113 raise HTTPBadRequest()
116 114
117 115 self._rhodecode_user = request.user # auth user
118 116 self._rhodecode_db_user = self._rhodecode_user.get_instance()
119 117 self._maybe_needs_password_change(
120 118 request.matched_route.name, self._rhodecode_db_user)
121 119
122 120 def _maybe_needs_password_change(self, view_name, user_obj):
123 121
124 122 dont_check_views = [
125 123 'channelstream_connect',
126 124 'ops_ping'
127 125 ]
128 126 if view_name in dont_check_views:
129 127 return
130 128
131 129 log.debug('Checking if user %s needs password change on view %s',
132 130 user_obj, view_name)
133 131
134 132 skip_user_views = [
135 133 'logout', 'login',
136 134 'my_account_password', 'my_account_password_update'
137 135 ]
138 136
139 137 if not user_obj:
140 138 return
141 139
142 140 if user_obj.username == User.DEFAULT_USER:
143 141 return
144 142
145 143 now = time.time()
146 144 should_change = user_obj.user_data.get('force_password_change')
147 145 change_after = safe_int(should_change) or 0
148 146 if should_change and now > change_after:
149 147 log.debug('User %s requires password change', user_obj)
150 148 h.flash('You are required to change your password', 'warning',
151 149 ignore_duplicate=True)
152 150
153 151 if view_name not in skip_user_views:
154 152 raise HTTPFound(
155 153 self.request.route_path('my_account_password'))
156 154
157 155 def _log_creation_exception(self, e, repo_name):
158 156 _ = self.request.translate
159 157 reason = None
160 158 if len(e.args) == 2:
161 159 reason = e.args[1]
162 160
163 161 if reason == 'INVALID_CERTIFICATE':
164 162 log.exception(
165 163 'Exception creating a repository: invalid certificate')
166 164 msg = (_('Error creating repository %s: invalid certificate')
167 165 % repo_name)
168 166 else:
169 167 log.exception("Exception creating a repository")
170 168 msg = (_('Error creating repository %s')
171 169 % repo_name)
172 170 return msg
173 171
174 172 def _get_local_tmpl_context(self, include_app_defaults=True):
175 173 c = TemplateArgs()
176 174 c.auth_user = self.request.user
177 175 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
178 176 c.rhodecode_user = self.request.user
179 177
180 178 if include_app_defaults:
181 179 from rhodecode.lib.base import attach_context_attributes
182 180 attach_context_attributes(c, self.request, self.request.user.user_id)
183 181
184 182 c.is_super_admin = c.auth_user.is_admin
185 183
186 184 c.can_create_repo = c.is_super_admin
187 185 c.can_create_repo_group = c.is_super_admin
188 186 c.can_create_user_group = c.is_super_admin
189 187
190 188 c.is_delegated_admin = False
191 189
192 190 if not c.auth_user.is_default and not c.is_super_admin:
193 191 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
194 192 user=self.request.user)
195 193 repositories = c.auth_user.repositories_admin or c.can_create_repo
196 194
197 195 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
198 196 user=self.request.user)
199 197 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
200 198
201 199 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
202 200 user=self.request.user)
203 201 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
204 202 # delegated admin can create, or manage some objects
205 203 c.is_delegated_admin = repositories or repository_groups or user_groups
206 204 return c
207 205
208 206 def _get_template_context(self, tmpl_args, **kwargs):
209 207
210 208 local_tmpl_args = {
211 209 'defaults': {},
212 210 'errors': {},
213 211 'c': tmpl_args
214 212 }
215 213 local_tmpl_args.update(kwargs)
216 214 return local_tmpl_args
217 215
218 216 def load_default_context(self):
219 217 """
220 218 example:
221 219
222 220 def load_default_context(self):
223 221 c = self._get_local_tmpl_context()
224 222 c.custom_var = 'foobar'
225 223
226 224 return c
227 225 """
228 226 raise NotImplementedError('Needs implementation in view class')
229 227
230 228
231 229 class RepoAppView(BaseAppView):
232 230
233 231 def __init__(self, context, request):
234 super(RepoAppView, self).__init__(context, request)
232 super().__init__(context, request)
235 233 self.db_repo = request.db_repo
236 234 self.db_repo_name = self.db_repo.repo_name
237 235 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
238 236 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
239 237 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
240 238
241 239 def _handle_missing_requirements(self, error):
242 240 log.error(
243 241 'Requirements are missing for repository %s: %s',
244 242 self.db_repo_name, safe_str(error))
245 243
246 244 def _prepare_and_set_clone_url(self, c):
247 245 username = ''
248 246 if self._rhodecode_user.username != User.DEFAULT_USER:
249 247 username = self._rhodecode_user.username
250 248
251 249 _def_clone_uri = c.clone_uri_tmpl
252 250 _def_clone_uri_id = c.clone_uri_id_tmpl
253 251 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
254 252
255 253 c.clone_repo_url = self.db_repo.clone_url(
256 254 user=username, uri_tmpl=_def_clone_uri)
257 255 c.clone_repo_url_id = self.db_repo.clone_url(
258 256 user=username, uri_tmpl=_def_clone_uri_id)
259 257 c.clone_repo_url_ssh = self.db_repo.clone_url(
260 258 uri_tmpl=_def_clone_uri_ssh, ssh=True)
261 259
262 260 def _get_local_tmpl_context(self, include_app_defaults=True):
263 261 _ = self.request.translate
264 c = super(RepoAppView, self)._get_local_tmpl_context(
262 c = super()._get_local_tmpl_context(
265 263 include_app_defaults=include_app_defaults)
266 264
267 265 # register common vars for this type of view
268 266 c.rhodecode_db_repo = self.db_repo
269 267 c.repo_name = self.db_repo_name
270 268 c.repository_pull_requests = self.db_repo_pull_requests
271 269 c.repository_artifacts = self.db_repo_artifacts
272 270 c.repository_is_user_following = ScmModel().is_following_repo(
273 271 self.db_repo_name, self._rhodecode_user.user_id)
274 272 self.path_filter = PathFilter(None)
275 273
276 274 c.repository_requirements_missing = {}
277 275 try:
278 276 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
279 277 # NOTE(marcink):
280 278 # comparison to None since if it's an object __bool__ is expensive to
281 279 # calculate
282 280 if self.rhodecode_vcs_repo is not None:
283 281 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
284 282 c.auth_user.username)
285 283 self.path_filter = PathFilter(path_perms)
286 284 except RepositoryRequirementError as e:
287 285 c.repository_requirements_missing = {'error': str(e)}
288 286 self._handle_missing_requirements(e)
289 287 self.rhodecode_vcs_repo = None
290 288
291 289 c.path_filter = self.path_filter # used by atom_feed_entry.mako
292 290
293 291 if self.rhodecode_vcs_repo is None:
294 292 # unable to fetch this repo as vcs instance, report back to user
295 293 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
296 294 h.flash(_(
297 295 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
298 296 "Please check if it exist, or is not damaged.") %
299 297 {'repo_name': c.repo_name},
300 298 category='error', ignore_duplicate=True)
301 299 if c.repository_requirements_missing:
302 300 route = self.request.matched_route.name
303 301 if route.startswith(('edit_repo', 'repo_summary')):
304 302 # allow summary and edit repo on missing requirements
305 303 return c
306 304
307 305 raise HTTPFound(
308 306 h.route_path('repo_summary', repo_name=self.db_repo_name))
309 307
310 308 else: # redirect if we don't show missing requirements
311 309 raise HTTPFound(h.route_path('home'))
312 310
313 311 c.has_origin_repo_read_perm = False
314 312 if self.db_repo.fork:
315 313 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
316 314 'repository.write', 'repository.read', 'repository.admin')(
317 315 self.db_repo.fork.repo_name, 'summary fork link')
318 316
319 317 return c
320 318
321 319 def _get_f_path_unchecked(self, matchdict, default=None):
322 320 """
323 321 Should only be used by redirects, everything else should call _get_f_path
324 322 """
325 323 f_path = matchdict.get('f_path')
326 324 if f_path:
327 325 # fix for multiple initial slashes that causes errors for GIT
328 326 return f_path.lstrip('/')
329 327
330 328 return default
331 329
332 330 def _get_f_path(self, matchdict, default=None):
333 331 f_path_match = self._get_f_path_unchecked(matchdict, default)
334 332 return self.path_filter.assert_path_permissions(f_path_match)
335 333
336 334 def _get_general_setting(self, target_repo, settings_key, default=False):
337 335 settings_model = VcsSettingsModel(repo=target_repo)
338 336 settings = settings_model.get_general_settings()
339 337 return settings.get(settings_key, default)
340 338
341 339 def _get_repo_setting(self, target_repo, settings_key, default=False):
342 340 settings_model = VcsSettingsModel(repo=target_repo)
343 341 settings = settings_model.get_repo_settings_inherited()
344 342 return settings.get(settings_key, default)
345 343
346 344 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
347 345 log.debug('Looking for README file at path %s', path)
348 346 if commit_id:
349 347 landing_commit_id = commit_id
350 348 else:
351 349 landing_commit = db_repo.get_landing_commit()
352 350 if isinstance(landing_commit, EmptyCommit):
353 351 return None, None
354 352 landing_commit_id = landing_commit.raw_id
355 353
356 cache_namespace_uid = 'repo.{}'.format(db_repo.repo_id)
354 cache_namespace_uid = f'repo.{db_repo.repo_id}'
357 355 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid, use_async_runner=True)
358 356 start = time.time()
359 357
360 358 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
361 359 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
362 360 readme_data = None
363 361 readme_filename = None
364 362
365 363 commit = db_repo.get_commit(_commit_id)
366 364 log.debug("Searching for a README file at commit %s.", _commit_id)
367 365 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
368 366
369 367 if readme_node:
370 368 log.debug('Found README node: %s', readme_node)
371 369 relative_urls = {
372 370 'raw': h.route_path(
373 371 'repo_file_raw', repo_name=_repo_name,
374 372 commit_id=commit.raw_id, f_path=readme_node.path),
375 373 'standard': h.route_path(
376 374 'repo_files', repo_name=_repo_name,
377 375 commit_id=commit.raw_id, f_path=readme_node.path),
378 376 }
379 377
380 378 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
381 379 readme_filename = readme_node.str_path
382 380
383 381 return readme_data, readme_filename
384 382
385 383 readme_data, readme_filename = generate_repo_readme(
386 384 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
387 385
388 386 compute_time = time.time() - start
389 387 log.debug('Repo README for path %s generated and computed in %.4fs',
390 388 path, compute_time)
391 389 return readme_data, readme_filename
392 390
393 391 def _render_readme_or_none(self, commit, readme_node, relative_urls):
394 392 log.debug('Found README file `%s` rendering...', readme_node.path)
395 393 renderer = MarkupRenderer()
396 394 try:
397 395 html_source = renderer.render(
398 396 readme_node.str_content, filename=readme_node.path)
399 397 if relative_urls:
400 398 return relative_links(html_source, relative_urls)
401 399 return html_source
402 400 except Exception:
403 401 log.exception("Exception while trying to render the README")
404 402
405 403 def get_recache_flag(self):
406 404 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
407 405 flag_val = self.request.GET.get(flag_name)
408 406 if str2bool(flag_val):
409 407 return True
410 408 return False
411 409
412 410 def get_commit_preload_attrs(cls):
413 411 pre_load = ['author', 'branch', 'date', 'message', 'parents',
414 412 'obsolete', 'phase', 'hidden']
415 413 return pre_load
416 414
417 415
418 416 class PathFilter(object):
419 417
420 418 # Expects and instance of BasePathPermissionChecker or None
421 419 def __init__(self, permission_checker):
422 420 self.permission_checker = permission_checker
423 421
424 422 def assert_path_permissions(self, path):
425 423 if self.path_access_allowed(path):
426 424 return path
427 425 raise HTTPForbidden()
428 426
429 427 def path_access_allowed(self, path):
430 428 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
431 429 if self.permission_checker:
432 430 has_access = path and self.permission_checker.has_access(path)
433 431 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
434 432 return has_access
435 433
436 434 log.debug('ACL permissions checker not enabled, skipping...')
437 435 return True
438 436
439 437 def filter_patchset(self, patchset):
440 438 if not self.permission_checker or not patchset:
441 439 return patchset, False
442 440 had_filtered = False
443 441 filtered_patchset = []
444 442 for patch in patchset:
445 443 filename = patch.get('filename', None)
446 444 if not filename or self.permission_checker.has_access(filename):
447 445 filtered_patchset.append(patch)
448 446 else:
449 447 had_filtered = True
450 448 if had_filtered:
451 449 if isinstance(patchset, diffs.LimitedDiffContainer):
452 450 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
453 451 return filtered_patchset, True
454 452 else:
455 453 return patchset, False
456 454
457 455 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
458 456
459 457 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
460 458 result = diffset.render_patchset(
461 459 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
462 460 result.has_hidden_changes = has_hidden_changes
463 461 return result
464 462
465 463 def get_raw_patch(self, diff_processor):
466 464 if self.permission_checker is None:
467 465 return diff_processor.as_raw()
468 466 elif self.permission_checker.has_full_access:
469 467 return diff_processor.as_raw()
470 468 else:
471 469 return '# Repository has user-specific filters, raw patch generation is disabled.'
472 470
473 471 @property
474 472 def is_enabled(self):
475 473 return self.permission_checker is not None
476 474
477 475
478 476 class RepoGroupAppView(BaseAppView):
479 477 def __init__(self, context, request):
480 super(RepoGroupAppView, self).__init__(context, request)
478 super().__init__(context, request)
481 479 self.db_repo_group = request.db_repo_group
482 480 self.db_repo_group_name = self.db_repo_group.group_name
483 481
484 482 def _get_local_tmpl_context(self, include_app_defaults=True):
485 483 _ = self.request.translate
486 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
484 c = super()._get_local_tmpl_context(
487 485 include_app_defaults=include_app_defaults)
488 486 c.repo_group = self.db_repo_group
489 487 return c
490 488
491 489 def _revoke_perms_on_yourself(self, form_result):
492 490 _updates = [u for u in form_result['perm_updates'] if self._rhodecode_user.user_id == int(u[0])]
493 491 _additions = [u for u in form_result['perm_additions'] if self._rhodecode_user.user_id == int(u[0])]
494 492 _deletions = [u for u in form_result['perm_deletions'] if self._rhodecode_user.user_id == int(u[0])]
495 493 admin_perm = 'group.admin'
496 494 if _updates and _updates[0][1] != admin_perm or \
497 495 _additions and _additions[0][1] != admin_perm or \
498 496 _deletions and _deletions[0][1] != admin_perm:
499 497 return True
500 498 return False
501 499
502 500
503 501 class UserGroupAppView(BaseAppView):
504 502 def __init__(self, context, request):
505 super(UserGroupAppView, self).__init__(context, request)
503 super().__init__(context, request)
506 504 self.db_user_group = request.db_user_group
507 505 self.db_user_group_name = self.db_user_group.users_group_name
508 506
509 507
510 508 class UserAppView(BaseAppView):
511 509 def __init__(self, context, request):
512 super(UserAppView, self).__init__(context, request)
510 super().__init__(context, request)
513 511 self.db_user = request.db_user
514 512 self.db_user_id = self.db_user.user_id
515 513
516 514 _ = self.request.translate
517 515 if not request.db_user_supports_default:
518 516 if self.db_user.username == User.DEFAULT_USER:
519 517 h.flash(_("Editing user `{}` is disabled.".format(
520 518 User.DEFAULT_USER)), category='warning')
521 519 raise HTTPFound(h.route_path('users'))
522 520
523 521
524 522 class DataGridAppView(object):
525 523 """
526 524 Common class to have re-usable grid rendering components
527 525 """
528 526
529 527 def _extract_ordering(self, request, column_map=None):
530 528 column_map = column_map or {}
531 529 column_index = safe_int(request.GET.get('order[0][column]'))
532 530 order_dir = request.GET.get(
533 531 'order[0][dir]', 'desc')
534 532 order_by = request.GET.get(
535 533 'columns[%s][data][sort]' % column_index, 'name_raw')
536 534
537 535 # translate datatable to DB columns
538 536 order_by = column_map.get(order_by) or order_by
539 537
540 538 search_q = request.GET.get('search[value]')
541 539 return search_q, order_by, order_dir
542 540
543 541 def _extract_chunk(self, request):
544 542 start = safe_int(request.GET.get('start'), 0)
545 543 length = safe_int(request.GET.get('length'), 25)
546 544 draw = safe_int(request.GET.get('draw'))
547 545 return draw, start, length
548 546
549 547 def _get_order_col(self, order_by, model):
550 548 if isinstance(order_by, str):
551 549 try:
552 550 return operator.attrgetter(order_by)(model)
553 551 except AttributeError:
554 552 return None
555 553 else:
556 554 return order_by
557 555
558 556
559 557 class BaseReferencesView(RepoAppView):
560 558 """
561 559 Base for reference view for branches, tags and bookmarks.
562 560 """
563 561 def load_default_context(self):
564 562 c = self._get_local_tmpl_context()
565 563 return c
566 564
567 565 def load_refs_context(self, ref_items, partials_template):
568 566 _render = self.request.get_partial_renderer(partials_template)
569 567 pre_load = ["author", "date", "message", "parents"]
570 568
571 569 is_svn = h.is_svn(self.rhodecode_vcs_repo)
572 570 is_hg = h.is_hg(self.rhodecode_vcs_repo)
573 571
574 572 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
575 573
576 574 closed_refs = {}
577 575 if is_hg:
578 576 closed_refs = self.rhodecode_vcs_repo.branches_closed
579 577
580 578 data = []
581 579 for ref_name, commit_id in ref_items:
582 580 commit = self.rhodecode_vcs_repo.get_commit(
583 581 commit_id=commit_id, pre_load=pre_load)
584 582 closed = ref_name in closed_refs
585 583
586 584 # TODO: johbo: Unify generation of reference links
587 585 use_commit_id = '/' in ref_name or is_svn
588 586
589 587 if use_commit_id:
590 588 files_url = h.route_path(
591 589 'repo_files',
592 590 repo_name=self.db_repo_name,
593 591 f_path=ref_name if is_svn else '',
594 592 commit_id=commit_id,
595 593 _query=dict(at=ref_name)
596 594 )
597 595
598 596 else:
599 597 files_url = h.route_path(
600 598 'repo_files',
601 599 repo_name=self.db_repo_name,
602 600 f_path=ref_name if is_svn else '',
603 601 commit_id=ref_name,
604 602 _query=dict(at=ref_name)
605 603 )
606 604
607 605 data.append({
608 606 "name": _render('name', ref_name, files_url, closed),
609 607 "name_raw": ref_name,
610 608 "date": _render('date', commit.date),
611 609 "date_raw": datetime_to_time(commit.date),
612 610 "author": _render('author', commit.author),
613 611 "commit": _render(
614 612 'commit', commit.message, commit.raw_id, commit.idx),
615 613 "commit_raw": commit.idx,
616 614 "compare": _render(
617 615 'compare', format_ref_id(ref_name, commit.raw_id)),
618 616 })
619 617
620 618 return data
621 619
622 620
623 621 class RepoRoutePredicate(object):
624 622 def __init__(self, val, config):
625 623 self.val = val
626 624
627 625 def text(self):
628 626 return f'repo_route = {self.val}'
629 627
630 628 phash = text
631 629
632 630 def __call__(self, info, request):
633 631 if hasattr(request, 'vcs_call'):
634 632 # skip vcs calls
635 633 return
636 634
637 635 repo_name = info['match']['repo_name']
638 636
639 637 repo_name_parts = repo_name.split('/')
640 638 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
641 639
642 640 if repo_name_parts != repo_slugs:
643 641 # short-skip if the repo-name doesn't follow slug rule
644 642 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
645 643 return False
646 644
647 645 repo_model = repo.RepoModel()
648 646
649 647 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
650 648
651 649 def redirect_if_creating(route_info, db_repo):
652 650 skip_views = ['edit_repo_advanced_delete']
653 651 route = route_info['route']
654 652 # we should skip delete view so we can actually "remove" repositories
655 653 # if they get stuck in creating state.
656 654 if route.name in skip_views:
657 655 return
658 656
659 657 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
660 658 repo_creating_url = request.route_path(
661 659 'repo_creating', repo_name=db_repo.repo_name)
662 660 raise HTTPFound(repo_creating_url)
663 661
664 662 if by_name_match:
665 663 # register this as request object we can re-use later
666 664 request.db_repo = by_name_match
667 665 request.db_repo_name = request.db_repo.repo_name
668 666
669 667 redirect_if_creating(info, by_name_match)
670 668 return True
671 669
672 670 by_id_match = repo_model.get_repo_by_id(repo_name)
673 671 if by_id_match:
674 672 request.db_repo = by_id_match
675 673 request.db_repo_name = request.db_repo.repo_name
676 674 redirect_if_creating(info, by_id_match)
677 675 return True
678 676
679 677 return False
680 678
681 679
682 680 class RepoForbidArchivedRoutePredicate(object):
683 681 def __init__(self, val, config):
684 682 self.val = val
685 683
686 684 def text(self):
687 685 return f'repo_forbid_archived = {self.val}'
688 686
689 687 phash = text
690 688
691 689 def __call__(self, info, request):
692 690 _ = request.translate
693 691 rhodecode_db_repo = request.db_repo
694 692
695 693 log.debug(
696 694 '%s checking if archived flag for repo for %s',
697 695 self.__class__.__name__, rhodecode_db_repo.repo_name)
698 696
699 697 if rhodecode_db_repo.archived:
700 698 log.warning('Current view is not supported for archived repo:%s',
701 699 rhodecode_db_repo.repo_name)
702 700
703 701 h.flash(
704 702 h.literal(_('Action not supported for archived repository.')),
705 703 category='warning')
706 704 summary_url = request.route_path(
707 705 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
708 706 raise HTTPFound(summary_url)
709 707 return True
710 708
711 709
712 710 class RepoTypeRoutePredicate(object):
713 711 def __init__(self, val, config):
714 712 self.val = val or ['hg', 'git', 'svn']
715 713
716 714 def text(self):
717 715 return f'repo_accepted_type = {self.val}'
718 716
719 717 phash = text
720 718
721 719 def __call__(self, info, request):
722 720 if hasattr(request, 'vcs_call'):
723 721 # skip vcs calls
724 722 return
725 723
726 724 rhodecode_db_repo = request.db_repo
727 725
728 726 log.debug(
729 727 '%s checking repo type for %s in %s',
730 728 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
731 729
732 730 if rhodecode_db_repo.repo_type in self.val:
733 731 return True
734 732 else:
735 733 log.warning('Current view is not supported for repo type:%s',
736 734 rhodecode_db_repo.repo_type)
737 735 return False
738 736
739 737
740 738 class RepoGroupRoutePredicate(object):
741 739 def __init__(self, val, config):
742 740 self.val = val
743 741
744 742 def text(self):
745 743 return f'repo_group_route = {self.val}'
746 744
747 745 phash = text
748 746
749 747 def __call__(self, info, request):
750 748 if hasattr(request, 'vcs_call'):
751 749 # skip vcs calls
752 750 return
753 751
754 752 repo_group_name = info['match']['repo_group_name']
755 753
756 754 repo_group_name_parts = repo_group_name.split('/')
757 755 repo_group_slugs = [x for x in [repo_name_slug(x) for x in repo_group_name_parts]]
758 756 if repo_group_name_parts != repo_group_slugs:
759 757 # short-skip if the repo-name doesn't follow slug rule
760 758 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
761 759 return False
762 760
763 761 repo_group_model = repo_group.RepoGroupModel()
764 762 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
765 763
766 764 if by_name_match:
767 765 # register this as request object we can re-use later
768 766 request.db_repo_group = by_name_match
769 767 request.db_repo_group_name = request.db_repo_group.group_name
770 768 return True
771 769
772 770 return False
773 771
774 772
775 773 class UserGroupRoutePredicate(object):
776 774 def __init__(self, val, config):
777 775 self.val = val
778 776
779 777 def text(self):
780 778 return f'user_group_route = {self.val}'
781 779
782 780 phash = text
783 781
784 782 def __call__(self, info, request):
785 783 if hasattr(request, 'vcs_call'):
786 784 # skip vcs calls
787 785 return
788 786
789 787 user_group_id = info['match']['user_group_id']
790 788 user_group_model = user_group.UserGroup()
791 789 by_id_match = user_group_model.get(user_group_id, cache=False)
792 790
793 791 if by_id_match:
794 792 # register this as request object we can re-use later
795 793 request.db_user_group = by_id_match
796 794 return True
797 795
798 796 return False
799 797
800 798
801 799 class UserRoutePredicateBase(object):
802 800 supports_default = None
803 801
804 802 def __init__(self, val, config):
805 803 self.val = val
806 804
807 805 def text(self):
808 806 raise NotImplementedError()
809 807
810 808 def __call__(self, info, request):
811 809 if hasattr(request, 'vcs_call'):
812 810 # skip vcs calls
813 811 return
814 812
815 813 user_id = info['match']['user_id']
816 814 user_model = user.User()
817 815 by_id_match = user_model.get(user_id, cache=False)
818 816
819 817 if by_id_match:
820 818 # register this as request object we can re-use later
821 819 request.db_user = by_id_match
822 820 request.db_user_supports_default = self.supports_default
823 821 return True
824 822
825 823 return False
826 824
827 825
828 826 class UserRoutePredicate(UserRoutePredicateBase):
829 827 supports_default = False
830 828
831 829 def text(self):
832 830 return f'user_route = {self.val}'
833 831
834 832 phash = text
835 833
836 834
837 835 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
838 836 supports_default = True
839 837
840 838 def text(self):
841 839 return f'user_with_default_route = {self.val}'
842 840
843 841 phash = text
844 842
845 843
846 844 def includeme(config):
847 845 config.add_route_predicate(
848 846 'repo_route', RepoRoutePredicate)
849 847 config.add_route_predicate(
850 848 'repo_accepted_types', RepoTypeRoutePredicate)
851 849 config.add_route_predicate(
852 850 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
853 851 config.add_route_predicate(
854 852 'repo_group_route', RepoGroupRoutePredicate)
855 853 config.add_route_predicate(
856 854 'user_group_route', UserGroupRoutePredicate)
857 855 config.add_route_predicate(
858 856 'user_route_with_default', UserRouteWithDefaultPredicate)
859 857 config.add_route_predicate(
860 858 'user_route', UserRoutePredicate)
@@ -1,29 +1,27 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 from zope.interface import Interface
22 20
23 21
24 22 class IAdminNavigationRegistry(Interface):
25 23 """
26 24 Interface for the admin navigation registry. Currently this is only
27 25 used to register and retrieve it via pyramids registry.
28 26 """
29 27 pass
@@ -1,55 +1,53 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from rhodecode import events
24 22 from rhodecode.lib import rc_cache
25 23
26 24 log = logging.getLogger(__name__)
27 25
28 26 # names of namespaces used for different permission related cached
29 27 # during flush operation we need to take care of all those
30 28 cache_namespaces = [
31 29 'cache_user_auth.{}',
32 30 'cache_user_repo_acl_ids.{}',
33 31 'cache_user_user_group_acl_ids.{}',
34 32 'cache_user_repo_group_acl_ids.{}'
35 33 ]
36 34
37 35
38 36 def trigger_user_permission_flush(event):
39 37 """
40 38 Subscriber to the `UserPermissionsChange`. This triggers the
41 39 automatic flush of permission caches, so the users affected receive new permissions
42 40 Right Away
43 41 """
44 42
45 43 affected_user_ids = set(event.user_ids)
46 44 for user_id in affected_user_ids:
47 45 for cache_namespace_uid_tmpl in cache_namespaces:
48 46 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
49 47 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
50 48 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
51 49 del_keys, user_id, cache_namespace_uid)
52 50
53 51
54 52 def includeme(config):
55 53 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
@@ -1,19 +1,17 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,40 +1,38 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from rhodecode.apps._base import BaseAppView, DataGridAppView
24 22 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
25 23
26 24 log = logging.getLogger(__name__)
27 25
28 26
29 27 class AdminArtifactsView(BaseAppView, DataGridAppView):
30 28
31 29 def load_default_context(self):
32 30 c = self._get_local_tmpl_context()
33 31 return c
34 32
35 33 @LoginRequired()
36 34 @HasPermissionAllDecorator('hg.admin')
37 35 def artifacts(self):
38 36 c = self.load_default_context()
39 37 c.active = 'artifacts'
40 38 return self._get_template_context(c)
@@ -1,87 +1,85 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPNotFound
24 22
25 23 from rhodecode.apps._base import BaseAppView
26 24 from rhodecode.model.db import joinedload, UserLog
27 25 from rhodecode.lib.user_log_filter import user_log_filter
28 26 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
29 27 from rhodecode.lib.utils2 import safe_int
30 28 from rhodecode.lib.helpers import SqlPage
31 29
32 30 log = logging.getLogger(__name__)
33 31
34 32
35 33 class AdminAuditLogsView(BaseAppView):
36 34
37 35 def load_default_context(self):
38 36 c = self._get_local_tmpl_context()
39 37 return c
40 38
41 39 @LoginRequired()
42 40 @HasPermissionAllDecorator('hg.admin')
43 41 def admin_audit_logs(self):
44 42 c = self.load_default_context()
45 43
46 44 users_log = UserLog.query()\
47 45 .options(joinedload(UserLog.user))\
48 46 .options(joinedload(UserLog.repository))
49 47
50 48 # FILTERING
51 49 c.search_term = self.request.GET.get('filter')
52 50 try:
53 51 users_log = user_log_filter(users_log, c.search_term)
54 52 except Exception:
55 53 # we want this to crash for now
56 54 raise
57 55
58 56 users_log = users_log.order_by(UserLog.action_date.desc())
59 57
60 58 p = safe_int(self.request.GET.get('page', 1), 1)
61 59
62 60 def url_generator(page_num):
63 61 query_params = {
64 62 'page': page_num
65 63 }
66 64 if c.search_term:
67 65 query_params['filter'] = c.search_term
68 66 return self.request.current_route_path(_query=query_params)
69 67
70 68 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
71 69 url_maker=url_generator)
72 70 return self._get_template_context(c)
73 71
74 72 @LoginRequired()
75 73 @HasPermissionAllDecorator('hg.admin')
76 74 def admin_audit_log_entry(self):
77 75 c = self.load_default_context()
78 76 audit_log_id = self.request.matchdict['audit_log_id']
79 77
80 78 c.audit_log_entry = UserLog.query()\
81 79 .options(joinedload(UserLog.user))\
82 80 .options(joinedload(UserLog.repository))\
83 81 .filter(UserLog.user_log_id == audit_log_id).scalar()
84 82 if not c.audit_log_entry:
85 83 raise HTTPNotFound()
86 84
87 85 return self._get_template_context(c)
@@ -1,103 +1,101 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 import formencode
24 22 import formencode.htmlfill
25 23
26 24 from pyramid.httpexceptions import HTTPFound
27 25 from pyramid.renderers import render
28 26 from pyramid.response import Response
29 27
30 28 from rhodecode.apps._base import BaseAppView
31 29 from rhodecode.lib.auth import (
32 30 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 31 from rhodecode.lib import helpers as h
34 32 from rhodecode.model.forms import DefaultsForm
35 33 from rhodecode.model.meta import Session
36 34 from rhodecode import BACKENDS
37 35 from rhodecode.model.settings import SettingsModel
38 36
39 37 log = logging.getLogger(__name__)
40 38
41 39
42 40 class AdminDefaultSettingsView(BaseAppView):
43 41
44 42 def load_default_context(self):
45 43 c = self._get_local_tmpl_context()
46 44 return c
47 45
48 46 @LoginRequired()
49 47 @HasPermissionAllDecorator('hg.admin')
50 48 def defaults_repository_show(self):
51 49 c = self.load_default_context()
52 50 c.backends = BACKENDS.keys()
53 51 c.active = 'repositories'
54 52 defaults = SettingsModel().get_default_repo_settings()
55 53
56 54 data = render(
57 55 'rhodecode:templates/admin/defaults/defaults.mako',
58 56 self._get_template_context(c), self.request)
59 57 html = formencode.htmlfill.render(
60 58 data,
61 59 defaults=defaults,
62 60 encoding="UTF-8",
63 61 force_defaults=False
64 62 )
65 63 return Response(html)
66 64
67 65 @LoginRequired()
68 66 @HasPermissionAllDecorator('hg.admin')
69 67 @CSRFRequired()
70 68 def defaults_repository_update(self):
71 69 _ = self.request.translate
72 70 c = self.load_default_context()
73 71 c.active = 'repositories'
74 72 form = DefaultsForm(self.request.translate)()
75 73
76 74 try:
77 75 form_result = form.to_python(dict(self.request.POST))
78 76 for k, v in form_result.items():
79 77 setting = SettingsModel().create_or_update_setting(k, v)
80 78 Session().add(setting)
81 79 Session().commit()
82 80 h.flash(_('Default settings updated successfully'),
83 81 category='success')
84 82
85 83 except formencode.Invalid as errors:
86 84 data = render(
87 85 'rhodecode:templates/admin/defaults/defaults.mako',
88 86 self._get_template_context(c), self.request)
89 87 html = formencode.htmlfill.render(
90 88 data,
91 89 defaults=errors.value,
92 90 errors=errors.unpack_errors() or {},
93 91 prefix_error=False,
94 92 encoding="UTF-8",
95 93 force_defaults=False
96 94 )
97 95 return Response(html)
98 96 except Exception:
99 97 log.exception('Exception in update action')
100 98 h.flash(_('Error occurred during update of default values'),
101 99 category='error')
102 100
103 101 raise HTTPFound(h.route_path('admin_defaults_repositories'))
@@ -1,161 +1,159 b''
1
2
3 1 # Copyright (C) 2018-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18 import os
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPFound
24 22
25 23 from rhodecode.apps._base import BaseAppView
26 24 from rhodecode.apps._base.navigation import navigation_list
27 25 from rhodecode.lib import helpers as h
28 26 from rhodecode.lib.auth import (
29 27 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 28 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
31 29 from rhodecode.lib import exc_tracking
32 30
33 31 log = logging.getLogger(__name__)
34 32
35 33
36 34 class ExceptionsTrackerView(BaseAppView):
37 35 def load_default_context(self):
38 36 c = self._get_local_tmpl_context()
39 37 c.navlist = navigation_list(self.request)
40 38 return c
41 39
42 40 def count_all_exceptions(self):
43 41 exc_store_path = exc_tracking.get_exc_store()
44 42 count = 0
45 43 for fname in os.listdir(exc_store_path):
46 44 parts = fname.split('_', 2)
47 45 if not len(parts) == 3:
48 46 continue
49 47 count +=1
50 48 return count
51 49
52 50 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
53 51 exc_store_path = exc_tracking.get_exc_store()
54 52 exception_list = []
55 53
56 54 def key_sorter(val):
57 55 try:
58 56 return val.split('_')[-1]
59 57 except Exception:
60 58 return 0
61 59
62 60 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
63 61
64 62 parts = fname.split('_', 2)
65 63 if not len(parts) == 3:
66 64 continue
67 65
68 66 exc_id, app_type, exc_timestamp = parts
69 67
70 68 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
71 69 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
72 70
73 71 if read_metadata:
74 72 full_path = os.path.join(exc_store_path, fname)
75 73 if not os.path.isfile(full_path):
76 74 continue
77 75 try:
78 76 # we can read our metadata
79 77 with open(full_path, 'rb') as f:
80 78 exc_metadata = exc_tracking.exc_unserialize(f.read())
81 79 exc.update(exc_metadata)
82 80 except Exception:
83 log.exception('Failed to read exc data from:{}'.format(full_path))
81 log.exception(f'Failed to read exc data from:{full_path}')
84 82 pass
85 83 # convert our timestamp to a date obj, for nicer representation
86 84 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
87 85
88 86 type_present = exc.get('exc_type')
89 87 if type_filter:
90 88 if type_present and type_present == type_filter:
91 89 exception_list.append(exc)
92 90 else:
93 91 exception_list.append(exc)
94 92
95 93 if limit and len(exception_list) >= limit:
96 94 break
97 95 return exception_list
98 96
99 97 @LoginRequired()
100 98 @HasPermissionAllDecorator('hg.admin')
101 99 def browse_exceptions(self):
102 100 _ = self.request.translate
103 101 c = self.load_default_context()
104 102 c.active = 'exceptions_browse'
105 103 c.limit = safe_int(self.request.GET.get('limit')) or 50
106 104 c.type_filter = self.request.GET.get('type_filter')
107 105 c.next_limit = c.limit + 50
108 106 c.exception_list = self.get_all_exceptions(
109 107 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
110 108 c.exception_list_count = self.count_all_exceptions()
111 109 c.exception_store_dir = exc_tracking.get_exc_store()
112 110 return self._get_template_context(c)
113 111
114 112 @LoginRequired()
115 113 @HasPermissionAllDecorator('hg.admin')
116 114 def exception_show(self):
117 115 _ = self.request.translate
118 116 c = self.load_default_context()
119 117
120 118 c.active = 'exceptions'
121 119 c.exception_id = self.request.matchdict['exception_id']
122 120 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
123 121 return self._get_template_context(c)
124 122
125 123 @LoginRequired()
126 124 @HasPermissionAllDecorator('hg.admin')
127 125 @CSRFRequired()
128 126 def exception_delete_all(self):
129 127 _ = self.request.translate
130 128 c = self.load_default_context()
131 129 type_filter = self.request.POST.get('type_filter')
132 130
133 131 c.active = 'exceptions'
134 132 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
135 133 exc_count = 0
136 134
137 135 for exc in all_exc:
138 136 if type_filter:
139 137 if exc.get('exc_type') == type_filter:
140 138 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
141 139 exc_count += 1
142 140 else:
143 141 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
144 142 exc_count += 1
145 143
146 144 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
147 145 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
148 146
149 147 @LoginRequired()
150 148 @HasPermissionAllDecorator('hg.admin')
151 149 @CSRFRequired()
152 150 def exception_delete(self):
153 151 _ = self.request.translate
154 152 c = self.load_default_context()
155 153
156 154 c.active = 'exceptions'
157 155 c.exception_id = self.request.matchdict['exception_id']
158 156 exc_tracking.delete_exception(c.exception_id, prefix=None)
159 157
160 158 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
161 159 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
@@ -1,72 +1,70 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 22
25 23 from rhodecode.apps._base import BaseAppView
26 24 from rhodecode.lib import helpers as h
27 25 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
28 26 from rhodecode.model.db import PullRequest
29 27
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class AdminMainView(BaseAppView):
35 33 def load_default_context(self):
36 34 c = self._get_local_tmpl_context()
37 35 return c
38 36
39 37 @LoginRequired()
40 38 @NotAnonymous()
41 39 def admin_main(self):
42 40 c = self.load_default_context()
43 41 c.active = 'admin'
44 42
45 43 if not (c.is_super_admin or c.is_delegated_admin):
46 44 raise HTTPNotFound()
47 45
48 46 return self._get_template_context(c)
49 47
50 48 @LoginRequired()
51 49 def pull_requests(self):
52 50 """
53 51 Global redirect for Pull Requests
54 52 pull_request_id: id of pull requests in the system
55 53 """
56 54
57 55 pull_request = PullRequest.get_or_404(
58 56 self.request.matchdict['pull_request_id'])
59 57 pull_request_id = pull_request.pull_request_id
60 58
61 59 repo_name = pull_request.target_repo.repo_name
62 60 # NOTE(marcink):
63 61 # check permissions so we don't redirect to repo that we don't have access to
64 62 # exposing it's name
65 63 target_repo_perm = HasRepoPermissionAny(
66 64 'repository.read', 'repository.write', 'repository.admin')(repo_name)
67 65 if not target_repo_perm:
68 66 raise HTTPNotFound()
69 67
70 68 raise HTTPFound(
71 69 h.route_path('pullrequest_show', repo_name=repo_name,
72 70 pull_request_id=pull_request_id))
@@ -1,46 +1,44 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import collections
22 20 import logging
23 21
24 22 from rhodecode.apps._base import BaseAppView
25 23 from rhodecode.apps._base.navigation import navigation_list
26 24 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
27 25 from rhodecode.lib.utils import read_opensource_licenses
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 class OpenSourceLicensesAdminSettingsView(BaseAppView):
33 31
34 32 def load_default_context(self):
35 33 c = self._get_local_tmpl_context()
36 34 return c
37 35
38 36 @LoginRequired()
39 37 @HasPermissionAllDecorator('hg.admin')
40 38 def open_source_licenses(self):
41 39 c = self.load_default_context()
42 40 c.active = 'open_source'
43 41 c.navlist = navigation_list(self.request)
44 42 c.opensource_licenses = sorted(
45 43 read_opensource_licenses(), key=lambda d: d["name"])
46 44 return self._get_template_context(c)
@@ -1,479 +1,477 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import re
22 20 import logging
23 21 import formencode
24 22 import formencode.htmlfill
25 23 import datetime
26 24 from pyramid.interfaces import IRoutesMapper
27 25
28 26 from pyramid.httpexceptions import HTTPFound
29 27 from pyramid.renderers import render
30 28 from pyramid.response import Response
31 29
32 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 31 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 32 from rhodecode import events
35 33
36 34 from rhodecode.lib import helpers as h
37 35 from rhodecode.lib.auth import (
38 36 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 37 from rhodecode.lib.utils2 import aslist, safe_str
40 38 from rhodecode.model.db import (
41 39 or_, coalesce, User, UserIpMap, UserSshKeys)
42 40 from rhodecode.model.forms import (
43 41 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 42 from rhodecode.model.meta import Session
45 43 from rhodecode.model.permission import PermissionModel
46 44 from rhodecode.model.settings import SettingsModel
47 45
48 46
49 47 log = logging.getLogger(__name__)
50 48
51 49
52 50 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 51 def load_default_context(self):
54 52 c = self._get_local_tmpl_context()
55 53 PermissionModel().set_global_permission_choices(
56 54 c, gettext_translator=self.request.translate)
57 55 return c
58 56
59 57 @LoginRequired()
60 58 @HasPermissionAllDecorator('hg.admin')
61 59 def permissions_application(self):
62 60 c = self.load_default_context()
63 61 c.active = 'application'
64 62
65 63 c.user = User.get_default_user(refresh=True)
66 64
67 65 app_settings = c.rc_config
68 66
69 67 defaults = {
70 68 'anonymous': c.user.active,
71 69 'default_register_message': app_settings.get(
72 70 'rhodecode_register_message')
73 71 }
74 72 defaults.update(c.user.get_default_perms())
75 73
76 74 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 75 self._get_template_context(c), self.request)
78 76 html = formencode.htmlfill.render(
79 77 data,
80 78 defaults=defaults,
81 79 encoding="UTF-8",
82 80 force_defaults=False
83 81 )
84 82 return Response(html)
85 83
86 84 @LoginRequired()
87 85 @HasPermissionAllDecorator('hg.admin')
88 86 @CSRFRequired()
89 87 def permissions_application_update(self):
90 88 _ = self.request.translate
91 89 c = self.load_default_context()
92 90 c.active = 'application'
93 91
94 92 _form = ApplicationPermissionsForm(
95 93 self.request.translate,
96 94 [x[0] for x in c.register_choices],
97 95 [x[0] for x in c.password_reset_choices],
98 96 [x[0] for x in c.extern_activate_choices])()
99 97
100 98 try:
101 99 form_result = _form.to_python(dict(self.request.POST))
102 100 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 101 PermissionModel().update_application_permissions(form_result)
104 102
105 103 settings = [
106 104 ('register_message', 'default_register_message'),
107 105 ]
108 106 for setting, form_key in settings:
109 107 sett = SettingsModel().create_or_update_setting(
110 108 setting, form_result[form_key])
111 109 Session().add(sett)
112 110
113 111 Session().commit()
114 112 h.flash(_('Application permissions updated successfully'),
115 113 category='success')
116 114
117 115 except formencode.Invalid as errors:
118 116 defaults = errors.value
119 117
120 118 data = render(
121 119 'rhodecode:templates/admin/permissions/permissions.mako',
122 120 self._get_template_context(c), self.request)
123 121 html = formencode.htmlfill.render(
124 122 data,
125 123 defaults=defaults,
126 124 errors=errors.unpack_errors() or {},
127 125 prefix_error=False,
128 126 encoding="UTF-8",
129 127 force_defaults=False
130 128 )
131 129 return Response(html)
132 130
133 131 except Exception:
134 132 log.exception("Exception during update of permissions")
135 133 h.flash(_('Error occurred during update of permissions'),
136 134 category='error')
137 135
138 136 affected_user_ids = [User.get_default_user_id()]
139 137 PermissionModel().trigger_permission_flush(affected_user_ids)
140 138
141 139 raise HTTPFound(h.route_path('admin_permissions_application'))
142 140
143 141 @LoginRequired()
144 142 @HasPermissionAllDecorator('hg.admin')
145 143 def permissions_objects(self):
146 144 c = self.load_default_context()
147 145 c.active = 'objects'
148 146
149 147 c.user = User.get_default_user(refresh=True)
150 148 defaults = {}
151 149 defaults.update(c.user.get_default_perms())
152 150
153 151 data = render(
154 152 'rhodecode:templates/admin/permissions/permissions.mako',
155 153 self._get_template_context(c), self.request)
156 154 html = formencode.htmlfill.render(
157 155 data,
158 156 defaults=defaults,
159 157 encoding="UTF-8",
160 158 force_defaults=False
161 159 )
162 160 return Response(html)
163 161
164 162 @LoginRequired()
165 163 @HasPermissionAllDecorator('hg.admin')
166 164 @CSRFRequired()
167 165 def permissions_objects_update(self):
168 166 _ = self.request.translate
169 167 c = self.load_default_context()
170 168 c.active = 'objects'
171 169
172 170 _form = ObjectPermissionsForm(
173 171 self.request.translate,
174 172 [x[0] for x in c.repo_perms_choices],
175 173 [x[0] for x in c.group_perms_choices],
176 174 [x[0] for x in c.user_group_perms_choices],
177 175 )()
178 176
179 177 try:
180 178 form_result = _form.to_python(dict(self.request.POST))
181 179 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 180 PermissionModel().update_object_permissions(form_result)
183 181
184 182 Session().commit()
185 183 h.flash(_('Object permissions updated successfully'),
186 184 category='success')
187 185
188 186 except formencode.Invalid as errors:
189 187 defaults = errors.value
190 188
191 189 data = render(
192 190 'rhodecode:templates/admin/permissions/permissions.mako',
193 191 self._get_template_context(c), self.request)
194 192 html = formencode.htmlfill.render(
195 193 data,
196 194 defaults=defaults,
197 195 errors=errors.unpack_errors() or {},
198 196 prefix_error=False,
199 197 encoding="UTF-8",
200 198 force_defaults=False
201 199 )
202 200 return Response(html)
203 201 except Exception:
204 202 log.exception("Exception during update of permissions")
205 203 h.flash(_('Error occurred during update of permissions'),
206 204 category='error')
207 205
208 206 affected_user_ids = [User.get_default_user_id()]
209 207 PermissionModel().trigger_permission_flush(affected_user_ids)
210 208
211 209 raise HTTPFound(h.route_path('admin_permissions_object'))
212 210
213 211 @LoginRequired()
214 212 @HasPermissionAllDecorator('hg.admin')
215 213 def permissions_branch(self):
216 214 c = self.load_default_context()
217 215 c.active = 'branch'
218 216
219 217 c.user = User.get_default_user(refresh=True)
220 218 defaults = {}
221 219 defaults.update(c.user.get_default_perms())
222 220
223 221 data = render(
224 222 'rhodecode:templates/admin/permissions/permissions.mako',
225 223 self._get_template_context(c), self.request)
226 224 html = formencode.htmlfill.render(
227 225 data,
228 226 defaults=defaults,
229 227 encoding="UTF-8",
230 228 force_defaults=False
231 229 )
232 230 return Response(html)
233 231
234 232 @LoginRequired()
235 233 @HasPermissionAllDecorator('hg.admin')
236 234 def permissions_global(self):
237 235 c = self.load_default_context()
238 236 c.active = 'global'
239 237
240 238 c.user = User.get_default_user(refresh=True)
241 239 defaults = {}
242 240 defaults.update(c.user.get_default_perms())
243 241
244 242 data = render(
245 243 'rhodecode:templates/admin/permissions/permissions.mako',
246 244 self._get_template_context(c), self.request)
247 245 html = formencode.htmlfill.render(
248 246 data,
249 247 defaults=defaults,
250 248 encoding="UTF-8",
251 249 force_defaults=False
252 250 )
253 251 return Response(html)
254 252
255 253 @LoginRequired()
256 254 @HasPermissionAllDecorator('hg.admin')
257 255 @CSRFRequired()
258 256 def permissions_global_update(self):
259 257 _ = self.request.translate
260 258 c = self.load_default_context()
261 259 c.active = 'global'
262 260
263 261 _form = UserPermissionsForm(
264 262 self.request.translate,
265 263 [x[0] for x in c.repo_create_choices],
266 264 [x[0] for x in c.repo_create_on_write_choices],
267 265 [x[0] for x in c.repo_group_create_choices],
268 266 [x[0] for x in c.user_group_create_choices],
269 267 [x[0] for x in c.fork_choices],
270 268 [x[0] for x in c.inherit_default_permission_choices])()
271 269
272 270 try:
273 271 form_result = _form.to_python(dict(self.request.POST))
274 272 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 273 PermissionModel().update_user_permissions(form_result)
276 274
277 275 Session().commit()
278 276 h.flash(_('Global permissions updated successfully'),
279 277 category='success')
280 278
281 279 except formencode.Invalid as errors:
282 280 defaults = errors.value
283 281
284 282 data = render(
285 283 'rhodecode:templates/admin/permissions/permissions.mako',
286 284 self._get_template_context(c), self.request)
287 285 html = formencode.htmlfill.render(
288 286 data,
289 287 defaults=defaults,
290 288 errors=errors.unpack_errors() or {},
291 289 prefix_error=False,
292 290 encoding="UTF-8",
293 291 force_defaults=False
294 292 )
295 293 return Response(html)
296 294 except Exception:
297 295 log.exception("Exception during update of permissions")
298 296 h.flash(_('Error occurred during update of permissions'),
299 297 category='error')
300 298
301 299 affected_user_ids = [User.get_default_user_id()]
302 300 PermissionModel().trigger_permission_flush(affected_user_ids)
303 301
304 302 raise HTTPFound(h.route_path('admin_permissions_global'))
305 303
306 304 @LoginRequired()
307 305 @HasPermissionAllDecorator('hg.admin')
308 306 def permissions_ips(self):
309 307 c = self.load_default_context()
310 308 c.active = 'ips'
311 309
312 310 c.user = User.get_default_user(refresh=True)
313 311 c.user_ip_map = (
314 312 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315 313
316 314 return self._get_template_context(c)
317 315
318 316 @LoginRequired()
319 317 @HasPermissionAllDecorator('hg.admin')
320 318 def permissions_overview(self):
321 319 c = self.load_default_context()
322 320 c.active = 'perms'
323 321
324 322 c.user = User.get_default_user(refresh=True)
325 323 c.perm_user = c.user.AuthUser()
326 324 return self._get_template_context(c)
327 325
328 326 @LoginRequired()
329 327 @HasPermissionAllDecorator('hg.admin')
330 328 def auth_token_access(self):
331 329 from rhodecode import CONFIG
332 330
333 331 c = self.load_default_context()
334 332 c.active = 'auth_token_access'
335 333
336 334 c.user = User.get_default_user(refresh=True)
337 335 c.perm_user = c.user.AuthUser()
338 336
339 337 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 338 c.view_data = []
341 339
342 340 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 341 introspector = self.request.registry.introspector
344 342
345 343 view_intr = {}
346 344 for view_data in introspector.get_category('views'):
347 345 intr = view_data['introspectable']
348 346
349 347 if 'route_name' in intr and intr['attr']:
350 348 view_intr[intr['route_name']] = '{}:{}'.format(
351 349 str(intr['derived_callable'].__name__), intr['attr']
352 350 )
353 351
354 352 c.whitelist_key = 'api_access_controllers_whitelist'
355 353 c.whitelist_file = CONFIG.get('__file__')
356 354 whitelist_views = aslist(
357 355 CONFIG.get(c.whitelist_key), sep=',')
358 356
359 357 for route_info in mapper.get_routes():
360 358 if not route_info.name.startswith('__'):
361 359 routepath = route_info.pattern
362 360
363 361 def replace(matchobj):
364 362 if matchobj.group(1):
365 363 return "{%s}" % matchobj.group(1).split(':')[0]
366 364 else:
367 365 return "{%s}" % matchobj.group(2)
368 366
369 367 routepath = _argument_prog.sub(replace, routepath)
370 368
371 369 if not routepath.startswith('/'):
372 370 routepath = '/' + routepath
373 371
374 372 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 373 active = view_fqn in whitelist_views
376 374 c.view_data.append((route_info.name, view_fqn, routepath, active))
377 375
378 376 c.whitelist_views = whitelist_views
379 377 return self._get_template_context(c)
380 378
381 379 def ssh_enabled(self):
382 380 return self.request.registry.settings.get(
383 381 'ssh.generate_authorized_keyfile')
384 382
385 383 @LoginRequired()
386 384 @HasPermissionAllDecorator('hg.admin')
387 385 def ssh_keys(self):
388 386 c = self.load_default_context()
389 387 c.active = 'ssh_keys'
390 388 c.ssh_enabled = self.ssh_enabled()
391 389 return self._get_template_context(c)
392 390
393 391 @LoginRequired()
394 392 @HasPermissionAllDecorator('hg.admin')
395 393 def ssh_keys_data(self):
396 394 _ = self.request.translate
397 395 self.load_default_context()
398 396 column_map = {
399 397 'fingerprint': 'ssh_key_fingerprint',
400 398 'username': User.username
401 399 }
402 400 draw, start, limit = self._extract_chunk(self.request)
403 401 search_q, order_by, order_dir = self._extract_ordering(
404 402 self.request, column_map=column_map)
405 403
406 404 ssh_keys_data_total_count = UserSshKeys.query()\
407 405 .count()
408 406
409 407 # json generate
410 408 base_q = UserSshKeys.query().join(UserSshKeys.user)
411 409
412 410 if search_q:
413 like_expression = u'%{}%'.format(safe_str(search_q))
411 like_expression = f'%{safe_str(search_q)}%'
414 412 base_q = base_q.filter(or_(
415 413 User.username.ilike(like_expression),
416 414 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 415 ))
418 416
419 417 users_data_total_filtered_count = base_q.count()
420 418
421 419 sort_col = self._get_order_col(order_by, UserSshKeys)
422 420 if sort_col:
423 421 if order_dir == 'asc':
424 422 # handle null values properly to order by NULL last
425 423 if order_by in ['created_on']:
426 424 sort_col = coalesce(sort_col, datetime.date.max)
427 425 sort_col = sort_col.asc()
428 426 else:
429 427 # handle null values properly to order by NULL last
430 428 if order_by in ['created_on']:
431 429 sort_col = coalesce(sort_col, datetime.date.min)
432 430 sort_col = sort_col.desc()
433 431
434 432 base_q = base_q.order_by(sort_col)
435 433 base_q = base_q.offset(start).limit(limit)
436 434
437 435 ssh_keys = base_q.all()
438 436
439 437 ssh_keys_data = []
440 438 for ssh_key in ssh_keys:
441 439 ssh_keys_data.append({
442 440 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 441 "fingerprint": ssh_key.ssh_key_fingerprint,
444 442 "description": ssh_key.description,
445 443 "created_on": h.format_date(ssh_key.created_on),
446 444 "accessed_on": h.format_date(ssh_key.accessed_on),
447 445 "action": h.link_to(
448 446 _('Edit'), h.route_path('edit_user_ssh_keys',
449 447 user_id=ssh_key.user.user_id))
450 448 })
451 449
452 450 data = ({
453 451 'draw': draw,
454 452 'data': ssh_keys_data,
455 453 'recordsTotal': ssh_keys_data_total_count,
456 454 'recordsFiltered': users_data_total_filtered_count,
457 455 })
458 456
459 457 return data
460 458
461 459 @LoginRequired()
462 460 @HasPermissionAllDecorator('hg.admin')
463 461 @CSRFRequired()
464 462 def ssh_keys_update(self):
465 463 _ = self.request.translate
466 464 self.load_default_context()
467 465
468 466 ssh_enabled = self.ssh_enabled()
469 467 key_file = self.request.registry.settings.get(
470 468 'ssh.authorized_keys_file_path')
471 469 if ssh_enabled:
472 470 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 471 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 472 category='success')
475 473 else:
476 474 h.flash(_('SSH key support is disabled in .ini file'),
477 475 category='warning')
478 476
479 477 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,170 +1,168 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 import psutil
24 22 import signal
25 23
26 24
27 25 from rhodecode.apps._base import BaseAppView
28 26 from rhodecode.apps._base.navigation import navigation_list
29 27 from rhodecode.lib import system_info
30 28 from rhodecode.lib.auth import (
31 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 30 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
33 31
34 32 log = logging.getLogger(__name__)
35 33
36 34
37 35 class AdminProcessManagementView(BaseAppView):
38 36 def load_default_context(self):
39 37 c = self._get_local_tmpl_context()
40 38 return c
41 39
42 40 def _format_proc(self, proc, with_children=False):
43 41 try:
44 42 mem = proc.memory_info()
45 43 proc_formatted = StrictAttributeDict({
46 44 'pid': proc.pid,
47 45 'name': proc.name(),
48 46 'mem_rss': mem.rss,
49 47 'mem_vms': mem.vms,
50 48 'cpu_percent': proc.cpu_percent(interval=0.1),
51 49 'create_time': proc.create_time(),
52 50 'cmd': ' '.join(proc.cmdline()),
53 51 })
54 52
55 53 if with_children:
56 54 proc_formatted.update({
57 55 'children': [self._format_proc(x)
58 56 for x in proc.children(recursive=True)]
59 57 })
60 58 except Exception:
61 59 log.exception('Failed to load proc')
62 60 proc_formatted = None
63 61 return proc_formatted
64 62
65 63 def get_processes(self):
66 64 proc_list = []
67 65 for p in psutil.process_iter():
68 66 if 'gunicorn' in p.name():
69 67 proc = self._format_proc(p, with_children=True)
70 68 if proc:
71 69 proc_list.append(proc)
72 70
73 71 return proc_list
74 72
75 73 def get_workers(self):
76 74 workers = None
77 75 try:
78 76 rc_config = system_info.rhodecode_config().value['config']
79 77 workers = rc_config['server:main'].get('workers')
80 78 except Exception:
81 79 pass
82 80
83 81 return workers or '?'
84 82
85 83 @LoginRequired()
86 84 @HasPermissionAllDecorator('hg.admin')
87 85 def process_management(self):
88 86 _ = self.request.translate
89 87 c = self.load_default_context()
90 88
91 89 c.active = 'process_management'
92 90 c.navlist = navigation_list(self.request)
93 91 c.gunicorn_processes = self.get_processes()
94 92 c.gunicorn_workers = self.get_workers()
95 93 return self._get_template_context(c)
96 94
97 95 @LoginRequired()
98 96 @HasPermissionAllDecorator('hg.admin')
99 97 def process_management_data(self):
100 98 _ = self.request.translate
101 99 c = self.load_default_context()
102 100 c.gunicorn_processes = self.get_processes()
103 101 return self._get_template_context(c)
104 102
105 103 @LoginRequired()
106 104 @HasPermissionAllDecorator('hg.admin')
107 105 @CSRFRequired()
108 106 def process_management_signal(self):
109 107 pids = self.request.json.get('pids', [])
110 108 result = []
111 109
112 110 def on_terminate(proc):
113 111 msg = "terminated"
114 112 result.append(msg)
115 113
116 114 procs = []
117 115 for pid in pids:
118 116 pid = safe_int(pid)
119 117 if pid:
120 118 try:
121 119 proc = psutil.Process(pid)
122 120 except psutil.NoSuchProcess:
123 121 continue
124 122
125 123 children = proc.children(recursive=True)
126 124 if children:
127 125 log.warning('Wont kill Master Process')
128 126 else:
129 127 procs.append(proc)
130 128
131 129 for p in procs:
132 130 try:
133 131 p.terminate()
134 132 except psutil.AccessDenied as e:
135 log.warning('Access denied: {}'.format(e))
133 log.warning(f'Access denied: {e}')
136 134
137 135 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
138 136 for p in alive:
139 137 try:
140 138 p.kill()
141 139 except psutil.AccessDenied as e:
142 log.warning('Access denied: {}'.format(e))
140 log.warning(f'Access denied: {e}')
143 141
144 142 return {'result': result}
145 143
146 144 @LoginRequired()
147 145 @HasPermissionAllDecorator('hg.admin')
148 146 @CSRFRequired()
149 147 def process_management_master_signal(self):
150 148 pid_data = self.request.json.get('pid_data', {})
151 149 pid = safe_int(pid_data['pid'])
152 150 action = pid_data['action']
153 151 if pid:
154 152 try:
155 153 proc = psutil.Process(pid)
156 154 except psutil.NoSuchProcess:
157 155 return {'result': 'failure_no_such_process'}
158 156
159 157 children = proc.children(recursive=True)
160 158 if children:
161 159 # master process
162 160 if action == '+' and len(children) <= 20:
163 161 proc.send_signal(signal.SIGTTIN)
164 162 elif action == '-' and len(children) >= 2:
165 163 proc.send_signal(signal.SIGTTOU)
166 164 else:
167 165 return {'result': 'failure_wrong_action'}
168 166 return {'result': 'success'}
169 167
170 168 return {'result': 'failure_not_master'}
@@ -1,358 +1,356 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18 import datetime
21 19 import logging
22 20 import time
23 21
24 22 import formencode
25 23 import formencode.htmlfill
26 24
27 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 26
29 27 from pyramid.renderers import render
30 28 from pyramid.response import Response
31 29 from sqlalchemy.orm import aliased
32 30
33 31 from rhodecode import events
34 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
35 33
36 34 from rhodecode.lib.auth import (
37 35 LoginRequired, CSRFRequired, NotAnonymous,
38 36 HasPermissionAny, HasRepoGroupPermissionAny)
39 37 from rhodecode.lib import helpers as h, audit_logger
40 38 from rhodecode.lib.str_utils import safe_int, safe_str
41 39 from rhodecode.model.forms import RepoGroupForm
42 40 from rhodecode.model.permission import PermissionModel
43 41 from rhodecode.model.repo_group import RepoGroupModel
44 42 from rhodecode.model.scm import RepoGroupList
45 43 from rhodecode.model.db import (
46 44 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
47 45
48 46 log = logging.getLogger(__name__)
49 47
50 48
51 49 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
52 50
53 51 def load_default_context(self):
54 52 c = self._get_local_tmpl_context()
55 53
56 54 return c
57 55
58 56 def _load_form_data(self, c):
59 57 allow_empty_group = False
60 58
61 59 if self._can_create_repo_group():
62 60 # we're global admin, we're ok and we can create TOP level groups
63 61 allow_empty_group = True
64 62
65 63 # override the choices for this form, we need to filter choices
66 64 # and display only those we have ADMIN right
67 65 groups_with_admin_rights = RepoGroupList(
68 66 RepoGroup.query().all(),
69 67 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
70 68 c.repo_groups = RepoGroup.groups_choices(
71 69 groups=groups_with_admin_rights,
72 70 show_empty_group=allow_empty_group)
73 71 c.personal_repo_group = self._rhodecode_user.personal_repo_group
74 72
75 73 def _can_create_repo_group(self, parent_group_id=None):
76 74 is_admin = HasPermissionAny('hg.admin')('group create controller')
77 75 create_repo_group = HasPermissionAny(
78 76 'hg.repogroup.create.true')('group create controller')
79 77 if is_admin or (create_repo_group and not parent_group_id):
80 78 # we're global admin, or we have global repo group create
81 79 # permission
82 80 # we're ok and we can create TOP level groups
83 81 return True
84 82 elif parent_group_id:
85 83 # we check the permission if we can write to parent group
86 84 group = RepoGroup.get(parent_group_id)
87 85 group_name = group.group_name if group else None
88 86 if HasRepoGroupPermissionAny('group.admin')(
89 87 group_name, 'check if user is an admin of group'):
90 88 # we're an admin of passed in group, we're ok.
91 89 return True
92 90 else:
93 91 return False
94 92 return False
95 93
96 94 # permission check in data loading of
97 95 # `repo_group_list_data` via RepoGroupList
98 96 @LoginRequired()
99 97 @NotAnonymous()
100 98 def repo_group_list(self):
101 99 c = self.load_default_context()
102 100 return self._get_template_context(c)
103 101
104 102 # permission check inside
105 103 @LoginRequired()
106 104 @NotAnonymous()
107 105 def repo_group_list_data(self):
108 106 self.load_default_context()
109 107 column_map = {
110 108 'name': 'group_name_hash',
111 109 'desc': 'group_description',
112 110 'last_change': 'updated_on',
113 111 'top_level_repos': 'repos_total',
114 112 'owner': 'user_username',
115 113 }
116 114 draw, start, limit = self._extract_chunk(self.request)
117 115 search_q, order_by, order_dir = self._extract_ordering(
118 116 self.request, column_map=column_map)
119 117
120 118 _render = self.request.get_partial_renderer(
121 119 'rhodecode:templates/data_table/_dt_elements.mako')
122 120 c = _render.get_call_context()
123 121
124 122 def quick_menu(repo_group_name):
125 123 return _render('quick_repo_group_menu', repo_group_name)
126 124
127 125 def repo_group_lnk(repo_group_name):
128 126 return _render('repo_group_name', repo_group_name)
129 127
130 128 def last_change(last_change):
131 129 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
132 130 ts = time.time()
133 131 utc_offset = (datetime.datetime.fromtimestamp(ts)
134 132 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
135 133 last_change = last_change + datetime.timedelta(seconds=utc_offset)
136 134 return _render("last_change", last_change)
137 135
138 136 def desc(desc, personal):
139 137 return _render(
140 138 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
141 139
142 140 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
143 141 return _render(
144 142 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
145 143
146 144 def user_profile(username):
147 145 return _render('user_profile', username)
148 146
149 147 _perms = ['group.admin']
150 148 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
151 149
152 150 repo_groups_data_total_count = RepoGroup.query()\
153 151 .filter(or_(
154 152 # generate multiple IN to fix limitation problems
155 153 *in_filter_generator(RepoGroup.group_id, allowed_ids)
156 154 )) \
157 155 .count()
158 156
159 157 repo_groups_data_total_inactive_count = RepoGroup.query()\
160 158 .filter(RepoGroup.group_id.in_(allowed_ids))\
161 159 .count()
162 160
163 161 repo_count = count(Repository.repo_id)
164 162 OwnerUser = aliased(User)
165 163 base_q = Session.query(
166 164 RepoGroup.group_name,
167 165 RepoGroup.group_name_hash,
168 166 RepoGroup.group_description,
169 167 RepoGroup.group_id,
170 168 RepoGroup.personal,
171 169 RepoGroup.updated_on,
172 170 OwnerUser.username.label('owner_username'),
173 171 repo_count.label('repos_count')
174 172 ) \
175 173 .filter(or_(
176 174 # generate multiple IN to fix limitation problems
177 175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
178 176 )) \
179 177 .outerjoin(Repository, RepoGroup.group_id == Repository.group_id) \
180 178 .join(OwnerUser, RepoGroup.user_id == OwnerUser.user_id)
181 179
182 180 base_q = base_q.group_by(RepoGroup, OwnerUser)
183 181
184 182 if search_q:
185 like_expression = u'%{}%'.format(safe_str(search_q))
183 like_expression = f'%{safe_str(search_q)}%'
186 184 base_q = base_q.filter(or_(
187 185 RepoGroup.group_name.ilike(like_expression),
188 186 ))
189 187
190 188 repo_groups_data_total_filtered_count = base_q.count()
191 189 # the inactive isn't really used, but we still make it same as other data grids
192 190 # which use inactive (users,user groups)
193 191 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
194 192
195 193 sort_defined = False
196 194 if order_by == 'group_name':
197 195 sort_col = func.lower(RepoGroup.group_name)
198 196 sort_defined = True
199 197 elif order_by == 'repos_total':
200 198 sort_col = repo_count
201 199 sort_defined = True
202 200 elif order_by == 'user_username':
203 201 sort_col = OwnerUser.username
204 202 else:
205 203 sort_col = getattr(RepoGroup, order_by, None)
206 204
207 205 if sort_defined or sort_col:
208 206 if order_dir == 'asc':
209 207 sort_col = sort_col.asc()
210 208 else:
211 209 sort_col = sort_col.desc()
212 210
213 211 base_q = base_q.order_by(sort_col)
214 212 base_q = base_q.offset(start).limit(limit)
215 213
216 214 # authenticated access to user groups
217 215 auth_repo_group_list = base_q.all()
218 216
219 217 repo_groups_data = []
220 218 for repo_gr in auth_repo_group_list:
221 219 row = {
222 220 "menu": quick_menu(repo_gr.group_name),
223 221 "name": repo_group_lnk(repo_gr.group_name),
224 222
225 223 "last_change": last_change(repo_gr.updated_on),
226 224
227 225 "last_changeset": "",
228 226 "last_changeset_raw": "",
229 227
230 228 "desc": desc(repo_gr.group_description, repo_gr.personal),
231 229 "owner": user_profile(repo_gr.owner_username),
232 230 "top_level_repos": repo_gr.repos_count,
233 231 "action": repo_group_actions(
234 232 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
235 233 }
236 234
237 235 repo_groups_data.append(row)
238 236
239 237 data = ({
240 238 'draw': draw,
241 239 'data': repo_groups_data,
242 240 'recordsTotal': repo_groups_data_total_count,
243 241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
244 242 'recordsFiltered': repo_groups_data_total_filtered_count,
245 243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
246 244 })
247 245
248 246 return data
249 247
250 248 @LoginRequired()
251 249 @NotAnonymous()
252 250 # perm checks inside
253 251 def repo_group_new(self):
254 252 c = self.load_default_context()
255 253
256 254 # perm check for admin, create_group perm or admin of parent_group
257 255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
258 256 _gr = RepoGroup.get(parent_group_id)
259 257 if not self._can_create_repo_group(parent_group_id):
260 258 raise HTTPForbidden()
261 259
262 260 self._load_form_data(c)
263 261
264 262 defaults = {} # Future proof for default of repo group
265 263
266 264 parent_group_choice = '-1'
267 265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
268 266 parent_group_choice = self._rhodecode_user.personal_repo_group
269 267
270 268 if parent_group_id and _gr:
271 269 if parent_group_id in [x[0] for x in c.repo_groups]:
272 270 parent_group_choice = safe_str(parent_group_id)
273 271
274 272 defaults.update({'group_parent_id': parent_group_choice})
275 273
276 274 data = render(
277 275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
278 276 self._get_template_context(c), self.request)
279 277
280 278 html = formencode.htmlfill.render(
281 279 data,
282 280 defaults=defaults,
283 281 encoding="UTF-8",
284 282 force_defaults=False
285 283 )
286 284 return Response(html)
287 285
288 286 @LoginRequired()
289 287 @NotAnonymous()
290 288 @CSRFRequired()
291 289 # perm checks inside
292 290 def repo_group_create(self):
293 291 c = self.load_default_context()
294 292 _ = self.request.translate
295 293
296 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
297 295 can_create = self._can_create_repo_group(parent_group_id)
298 296
299 297 self._load_form_data(c)
300 298 # permissions for can create group based on parent_id are checked
301 299 # here in the Form
302 300 available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
303 301 repo_group_form = RepoGroupForm(
304 302 self.request.translate, available_groups=available_groups,
305 303 can_create_in_root=can_create)()
306 304
307 305 repo_group_name = self.request.POST.get('group_name')
308 306 try:
309 307 owner = self._rhodecode_user
310 308 form_result = repo_group_form.to_python(dict(self.request.POST))
311 309 copy_permissions = form_result.get('group_copy_permissions')
312 310 repo_group = RepoGroupModel().create(
313 311 group_name=form_result['group_name_full'],
314 312 group_description=form_result['group_description'],
315 313 owner=owner.user_id,
316 314 copy_permissions=form_result['group_copy_permissions']
317 315 )
318 316 Session().flush()
319 317
320 318 repo_group_data = repo_group.get_api_data()
321 319 audit_logger.store_web(
322 320 'repo_group.create', action_data={'data': repo_group_data},
323 321 user=self._rhodecode_user)
324 322
325 323 Session().commit()
326 324
327 325 _new_group_name = form_result['group_name_full']
328 326
329 327 repo_group_url = h.link_to(
330 328 _new_group_name,
331 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
332 330 h.flash(h.literal(_('Created repository group %s')
333 331 % repo_group_url), category='success')
334 332
335 333 except formencode.Invalid as errors:
336 334 data = render(
337 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
338 336 self._get_template_context(c), self.request)
339 337 html = formencode.htmlfill.render(
340 338 data,
341 339 defaults=errors.value,
342 340 errors=errors.unpack_errors() or {},
343 341 prefix_error=False,
344 342 encoding="UTF-8",
345 343 force_defaults=False
346 344 )
347 345 return Response(html)
348 346 except Exception:
349 347 log.exception("Exception during creation of repository group")
350 348 h.flash(_('Error occurred during creation of repository group %s')
351 349 % repo_group_name, category='error')
352 350 raise HTTPFound(h.route_path('home'))
353 351
354 352 PermissionModel().trigger_permission_flush()
355 353
356 354 raise HTTPFound(
357 355 h.route_path('repo_group_home',
358 356 repo_group_name=form_result['group_name_full']))
@@ -1,256 +1,254 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20 import formencode
23 21 import formencode.htmlfill
24 22
25 23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 24
27 25 from pyramid.renderers import render
28 26 from pyramid.response import Response
29 27 from sqlalchemy.orm import aliased
30 28
31 29 from rhodecode import events
32 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 31 from rhodecode.lib.celerylib.utils import get_task_id
34 32
35 33 from rhodecode.lib.auth import (
36 34 LoginRequired, CSRFRequired, NotAnonymous,
37 35 HasPermissionAny, HasRepoGroupPermissionAny)
38 36 from rhodecode.lib import helpers as h
39 37 from rhodecode.lib.utils import repo_name_slug
40 38 from rhodecode.lib.utils2 import safe_int, safe_str
41 39 from rhodecode.model.forms import RepoForm
42 40 from rhodecode.model.permission import PermissionModel
43 41 from rhodecode.model.repo import RepoModel
44 42 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
45 43 from rhodecode.model.settings import SettingsModel
46 44 from rhodecode.model.db import (
47 45 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
48 46
49 47 log = logging.getLogger(__name__)
50 48
51 49
52 50 class AdminReposView(BaseAppView, DataGridAppView):
53 51
54 52 def load_default_context(self):
55 53 c = self._get_local_tmpl_context()
56 54 return c
57 55
58 56 def _load_form_data(self, c):
59 57 acl_groups = RepoGroupList(RepoGroup.query().all(),
60 58 perm_set=['group.write', 'group.admin'])
61 59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
62 60 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
63 61 c.personal_repo_group = self._rhodecode_user.personal_repo_group
64 62
65 63 @LoginRequired()
66 64 @NotAnonymous()
67 65 # perms check inside
68 66 def repository_list(self):
69 67 c = self.load_default_context()
70 68 return self._get_template_context(c)
71 69
72 70 @LoginRequired()
73 71 @NotAnonymous()
74 72 # perms check inside
75 73 def repository_list_data(self):
76 74 self.load_default_context()
77 75 column_map = {
78 76 'name': 'repo_name',
79 77 'desc': 'description',
80 78 'last_change': 'updated_on',
81 79 'owner': 'user_username',
82 80 }
83 81 draw, start, limit = self._extract_chunk(self.request)
84 82 search_q, order_by, order_dir = self._extract_ordering(
85 83 self.request, column_map=column_map)
86 84
87 85 _perms = ['repository.admin']
88 86 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
89 87
90 88 repos_data_total_count = Repository.query() \
91 89 .filter(or_(
92 90 # generate multiple IN to fix limitation problems
93 91 *in_filter_generator(Repository.repo_id, allowed_ids))
94 92 ) \
95 93 .count()
96 94
97 95 RepoFork = aliased(Repository)
98 96 OwnerUser = aliased(User)
99 97 base_q = Session.query(
100 98 Repository.repo_id,
101 99 Repository.repo_name,
102 100 Repository.description,
103 101 Repository.repo_type,
104 102 Repository.repo_state,
105 103 Repository.private,
106 104 Repository.archived,
107 105 Repository.updated_on,
108 106 Repository._changeset_cache,
109 107 RepoFork.repo_name.label('fork_repo_name'),
110 108 OwnerUser.username.label('owner_username'),
111 109 ) \
112 110 .filter(or_(
113 111 # generate multiple IN to fix limitation problems
114 112 *in_filter_generator(Repository.repo_id, allowed_ids))
115 113 ) \
116 114 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
117 115 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
118 116
119 117 if search_q:
120 like_expression = u'%{}%'.format(safe_str(search_q))
118 like_expression = f'%{safe_str(search_q)}%'
121 119 base_q = base_q.filter(or_(
122 120 Repository.repo_name.ilike(like_expression),
123 121 ))
124 122
125 123 #TODO: check if we need group_by here ?
126 124 #base_q = base_q.group_by(Repository, User)
127 125
128 126 repos_data_total_filtered_count = base_q.count()
129 127
130 128 sort_defined = False
131 129 if order_by == 'repo_name':
132 130 sort_col = func.lower(Repository.repo_name)
133 131 sort_defined = True
134 132 elif order_by == 'user_username':
135 133 sort_col = OwnerUser.username
136 134 else:
137 135 sort_col = getattr(Repository, order_by, None)
138 136
139 137 if sort_defined or sort_col:
140 138 if order_dir == 'asc':
141 139 sort_col = sort_col.asc()
142 140 else:
143 141 sort_col = sort_col.desc()
144 142
145 143 base_q = base_q.order_by(sort_col)
146 144 base_q = base_q.offset(start).limit(limit)
147 145
148 146 repos_list = base_q.all()
149 147
150 148 repos_data = RepoModel().get_repos_as_dict(
151 149 repo_list=repos_list, admin=True, super_user_actions=True)
152 150
153 151 data = ({
154 152 'draw': draw,
155 153 'data': repos_data,
156 154 'recordsTotal': repos_data_total_count,
157 155 'recordsFiltered': repos_data_total_filtered_count,
158 156 })
159 157 return data
160 158
161 159 @LoginRequired()
162 160 @NotAnonymous()
163 161 # perms check inside
164 162 def repository_new(self):
165 163 c = self.load_default_context()
166 164
167 165 new_repo = self.request.GET.get('repo', '')
168 166 parent_group_id = safe_int(self.request.GET.get('parent_group'))
169 167 _gr = RepoGroup.get(parent_group_id)
170 168
171 169 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
172 170 # you're not super admin nor have global create permissions,
173 171 # but maybe you have at least write permission to a parent group ?
174 172
175 173 gr_name = _gr.group_name if _gr else None
176 174 # create repositories with write permission on group is set to true
177 175 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
178 176 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
179 177 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
180 178 if not (group_admin or (group_write and create_on_write)):
181 179 raise HTTPForbidden()
182 180
183 181 self._load_form_data(c)
184 182 c.new_repo = repo_name_slug(new_repo)
185 183
186 184 # apply the defaults from defaults page
187 185 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
188 186 # set checkbox to autochecked
189 187 defaults['repo_copy_permissions'] = True
190 188
191 189 parent_group_choice = '-1'
192 190 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
193 191 parent_group_choice = self._rhodecode_user.personal_repo_group
194 192
195 193 if parent_group_id and _gr:
196 194 if parent_group_id in [x[0] for x in c.repo_groups]:
197 195 parent_group_choice = safe_str(parent_group_id)
198 196
199 197 defaults.update({'repo_group': parent_group_choice})
200 198
201 199 data = render('rhodecode:templates/admin/repos/repo_add.mako',
202 200 self._get_template_context(c), self.request)
203 201 html = formencode.htmlfill.render(
204 202 data,
205 203 defaults=defaults,
206 204 encoding="UTF-8",
207 205 force_defaults=False
208 206 )
209 207 return Response(html)
210 208
211 209 @LoginRequired()
212 210 @NotAnonymous()
213 211 @CSRFRequired()
214 212 # perms check inside
215 213 def repository_create(self):
216 214 c = self.load_default_context()
217 215
218 216 form_result = {}
219 217 self._load_form_data(c)
220 218
221 219 try:
222 220 # CanWriteToGroup validators checks permissions of this POST
223 221 form = RepoForm(
224 222 self.request.translate, repo_groups=c.repo_groups_choices)()
225 223 form_result = form.to_python(dict(self.request.POST))
226 224 copy_permissions = form_result.get('repo_copy_permissions')
227 225 # create is done sometimes async on celery, db transaction
228 226 # management is handled there.
229 227 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
230 228 task_id = get_task_id(task)
231 229 except formencode.Invalid as errors:
232 230 data = render('rhodecode:templates/admin/repos/repo_add.mako',
233 231 self._get_template_context(c), self.request)
234 232 html = formencode.htmlfill.render(
235 233 data,
236 234 defaults=errors.value,
237 235 errors=errors.unpack_errors() or {},
238 236 prefix_error=False,
239 237 encoding="UTF-8",
240 238 force_defaults=False
241 239 )
242 240 return Response(html)
243 241
244 242 except Exception as e:
245 243 msg = self._log_creation_exception(e, form_result.get('repo_name'))
246 244 h.flash(msg, category='error')
247 245 raise HTTPFound(h.route_path('home'))
248 246
249 247 repo_name = form_result.get('repo_name_full')
250 248
251 249 affected_user_ids = [self._rhodecode_user.user_id]
252 250 PermissionModel().trigger_permission_flush(affected_user_ids)
253 251
254 252 raise HTTPFound(
255 253 h.route_path('repo_creating', repo_name=repo_name,
256 254 _query=dict(task_id=task_id)))
@@ -1,95 +1,93 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22 from pyramid.httpexceptions import HTTPFound
25 23
26 24 from rhodecode.apps._base import BaseAppView
27 25 from rhodecode.apps._base.navigation import navigation_list
28 26 from rhodecode.lib.auth import (
29 27 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 28 from rhodecode.lib.utils2 import safe_int
31 29 from rhodecode.lib import system_info
32 30 from rhodecode.lib import user_sessions
33 31 from rhodecode.lib import helpers as h
34 32
35 33
36 34 log = logging.getLogger(__name__)
37 35
38 36
39 37 class AdminSessionSettingsView(BaseAppView):
40 38
41 39 def load_default_context(self):
42 40 c = self._get_local_tmpl_context()
43 41 return c
44 42
45 43 @LoginRequired()
46 44 @HasPermissionAllDecorator('hg.admin')
47 45 def settings_sessions(self):
48 46 c = self.load_default_context()
49 47
50 48 c.active = 'sessions'
51 49 c.navlist = navigation_list(self.request)
52 50
53 51 c.cleanup_older_days = 60
54 52 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
55 53
56 54 config = system_info.rhodecode_config().get_value()['value']['config']
57 55 c.session_model = user_sessions.get_session_handler(
58 56 config.get('beaker.session.type', 'memory'))(config)
59 57
60 58 c.session_conf = c.session_model.config
61 59 c.session_count = c.session_model.get_count()
62 60 c.session_expired_count = c.session_model.get_expired_count(
63 61 older_than_seconds)
64 62
65 63 return self._get_template_context(c)
66 64
67 65 @LoginRequired()
68 66 @HasPermissionAllDecorator('hg.admin')
69 67 @CSRFRequired()
70 68 def settings_sessions_cleanup(self):
71 69 _ = self.request.translate
72 70 expire_days = safe_int(self.request.params.get('expire_days'))
73 71
74 72 if expire_days is None:
75 73 expire_days = 60
76 74
77 75 older_than_seconds = 60 * 60 * 24 * expire_days
78 76
79 77 config = system_info.rhodecode_config().get_value()['value']['config']
80 78 session_model = user_sessions.get_session_handler(
81 79 config.get('beaker.session.type', 'memory'))(config)
82 80
83 81 try:
84 82 session_model.clean_sessions(
85 83 older_than_seconds=older_than_seconds)
86 84 h.flash(_('Cleaned up old sessions'), category='success')
87 85 except user_sessions.CleanupCommand as msg:
88 86 h.flash(msg.message, category='warning')
89 87 except Exception as e:
90 88 log.exception('Failed session cleanup')
91 89 h.flash(_('Failed to cleanup up old sessions'), category='error')
92 90
93 91 redirect_to = self.request.resource_path(
94 92 self.context, route_name='admin_settings_sessions')
95 93 return HTTPFound(redirect_to)
@@ -1,722 +1,721 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19
21 20 import logging
22 21 import collections
23 22
24 23 import datetime
25 24 import formencode
26 25 import formencode.htmlfill
27 26
28 27 import rhodecode
29 28
30 29 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 30 from pyramid.renderers import render
32 31 from pyramid.response import Response
33 32
34 33 from rhodecode.apps._base import BaseAppView
35 34 from rhodecode.apps._base.navigation import navigation_list
36 35 from rhodecode.apps.svn_support.config_keys import generate_config
37 36 from rhodecode.lib import helpers as h
38 37 from rhodecode.lib.auth import (
39 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 39 from rhodecode.lib.celerylib import tasks, run_task
41 40 from rhodecode.lib.str_utils import safe_str
42 41 from rhodecode.lib.utils import repo2db_mapper
43 42 from rhodecode.lib.utils2 import str2bool, AttributeDict
44 43 from rhodecode.lib.index import searcher_from_config
45 44
46 45 from rhodecode.model.db import RhodeCodeUi, Repository
47 46 from rhodecode.model.forms import (ApplicationSettingsForm,
48 47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 48 LabsSettingsForm, IssueTrackerPatternsForm)
50 49 from rhodecode.model.permission import PermissionModel
51 50 from rhodecode.model.repo_group import RepoGroupModel
52 51
53 52 from rhodecode.model.scm import ScmModel
54 53 from rhodecode.model.notification import EmailNotificationModel
55 54 from rhodecode.model.meta import Session
56 55 from rhodecode.model.settings import (
57 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 57 SettingsModel)
59 58
60 59
61 60 log = logging.getLogger(__name__)
62 61
63 62
64 63 class AdminSettingsView(BaseAppView):
65 64
66 65 def load_default_context(self):
67 66 c = self._get_local_tmpl_context()
68 67 c.labs_active = str2bool(
69 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 69 c.navlist = navigation_list(self.request)
71 70 return c
72 71
73 72 @classmethod
74 73 def _get_ui_settings(cls):
75 74 ret = RhodeCodeUi.query().all()
76 75
77 76 if not ret:
78 77 raise Exception('Could not get application ui settings !')
79 78 settings = {}
80 79 for each in ret:
81 80 k = each.ui_key
82 81 v = each.ui_value
83 82 if k == '/':
84 83 k = 'root_path'
85 84
86 85 if k in ['push_ssl', 'publish', 'enabled']:
87 86 v = str2bool(v)
88 87
89 88 if k.find('.') != -1:
90 89 k = k.replace('.', '_')
91 90
92 91 if each.ui_section in ['hooks', 'extensions']:
93 92 v = each.ui_active
94 93
95 94 settings[each.ui_section + '_' + k] = v
96 95 return settings
97 96
98 97 @classmethod
99 98 def _form_defaults(cls):
100 99 defaults = SettingsModel().get_all_settings()
101 100 defaults.update(cls._get_ui_settings())
102 101
103 102 defaults.update({
104 103 'new_svn_branch': '',
105 104 'new_svn_tag': '',
106 105 })
107 106 return defaults
108 107
109 108 @LoginRequired()
110 109 @HasPermissionAllDecorator('hg.admin')
111 110 def settings_vcs(self):
112 111 c = self.load_default_context()
113 112 c.active = 'vcs'
114 113 model = VcsSettingsModel()
115 114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
116 115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
117 116
118 117 settings = self.request.registry.settings
119 118 c.svn_proxy_generate_config = settings[generate_config]
120 119
121 120 defaults = self._form_defaults()
122 121
123 122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
124 123
125 124 data = render('rhodecode:templates/admin/settings/settings.mako',
126 125 self._get_template_context(c), self.request)
127 126 html = formencode.htmlfill.render(
128 127 data,
129 128 defaults=defaults,
130 129 encoding="UTF-8",
131 130 force_defaults=False
132 131 )
133 132 return Response(html)
134 133
135 134 @LoginRequired()
136 135 @HasPermissionAllDecorator('hg.admin')
137 136 @CSRFRequired()
138 137 def settings_vcs_update(self):
139 138 _ = self.request.translate
140 139 c = self.load_default_context()
141 140 c.active = 'vcs'
142 141
143 142 model = VcsSettingsModel()
144 143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
145 144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
146 145
147 146 settings = self.request.registry.settings
148 147 c.svn_proxy_generate_config = settings[generate_config]
149 148
150 149 application_form = ApplicationUiSettingsForm(self.request.translate)()
151 150
152 151 try:
153 152 form_result = application_form.to_python(dict(self.request.POST))
154 153 except formencode.Invalid as errors:
155 154 h.flash(
156 155 _("Some form inputs contain invalid data."),
157 156 category='error')
158 157 data = render('rhodecode:templates/admin/settings/settings.mako',
159 158 self._get_template_context(c), self.request)
160 159 html = formencode.htmlfill.render(
161 160 data,
162 161 defaults=errors.value,
163 162 errors=errors.unpack_errors() or {},
164 163 prefix_error=False,
165 164 encoding="UTF-8",
166 165 force_defaults=False
167 166 )
168 167 return Response(html)
169 168
170 169 try:
171 170 if c.visual.allow_repo_location_change:
172 171 model.update_global_path_setting(form_result['paths_root_path'])
173 172
174 173 model.update_global_ssl_setting(form_result['web_push_ssl'])
175 174 model.update_global_hook_settings(form_result)
176 175
177 176 model.create_or_update_global_svn_settings(form_result)
178 177 model.create_or_update_global_hg_settings(form_result)
179 178 model.create_or_update_global_git_settings(form_result)
180 179 model.create_or_update_global_pr_settings(form_result)
181 180 except Exception:
182 181 log.exception("Exception while updating settings")
183 182 h.flash(_('Error occurred during updating '
184 183 'application settings'), category='error')
185 184 else:
186 185 Session().commit()
187 186 h.flash(_('Updated VCS settings'), category='success')
188 187 raise HTTPFound(h.route_path('admin_settings_vcs'))
189 188
190 189 data = render('rhodecode:templates/admin/settings/settings.mako',
191 190 self._get_template_context(c), self.request)
192 191 html = formencode.htmlfill.render(
193 192 data,
194 193 defaults=self._form_defaults(),
195 194 encoding="UTF-8",
196 195 force_defaults=False
197 196 )
198 197 return Response(html)
199 198
200 199 @LoginRequired()
201 200 @HasPermissionAllDecorator('hg.admin')
202 201 @CSRFRequired()
203 202 def settings_vcs_delete_svn_pattern(self):
204 203 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
205 204 model = VcsSettingsModel()
206 205 try:
207 206 model.delete_global_svn_pattern(delete_pattern_id)
208 207 except SettingNotFound:
209 208 log.exception(
210 209 'Failed to delete svn_pattern with id %s', delete_pattern_id)
211 210 raise HTTPNotFound()
212 211
213 212 Session().commit()
214 213 return True
215 214
216 215 @LoginRequired()
217 216 @HasPermissionAllDecorator('hg.admin')
218 217 def settings_mapping(self):
219 218 c = self.load_default_context()
220 219 c.active = 'mapping'
221 220
222 221 data = render('rhodecode:templates/admin/settings/settings.mako',
223 222 self._get_template_context(c), self.request)
224 223 html = formencode.htmlfill.render(
225 224 data,
226 225 defaults=self._form_defaults(),
227 226 encoding="UTF-8",
228 227 force_defaults=False
229 228 )
230 229 return Response(html)
231 230
232 231 @LoginRequired()
233 232 @HasPermissionAllDecorator('hg.admin')
234 233 @CSRFRequired()
235 234 def settings_mapping_update(self):
236 235 _ = self.request.translate
237 236 c = self.load_default_context()
238 237 c.active = 'mapping'
239 238 rm_obsolete = self.request.POST.get('destroy', False)
240 239 invalidate_cache = self.request.POST.get('invalidate', False)
241 240 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
242 241
243 242 if invalidate_cache:
244 243 log.debug('invalidating all repositories cache')
245 244 for repo in Repository.get_all():
246 245 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
247 246
248 247 filesystem_repos = ScmModel().repo_scan()
249 248 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
250 249 PermissionModel().trigger_permission_flush()
251 250
252 251 def _repr(l):
253 252 return ', '.join(map(safe_str, l)) or '-'
254 253 h.flash(_('Repositories successfully '
255 254 'rescanned added: %s ; removed: %s') %
256 255 (_repr(added), _repr(removed)),
257 256 category='success')
258 257 raise HTTPFound(h.route_path('admin_settings_mapping'))
259 258
260 259 @LoginRequired()
261 260 @HasPermissionAllDecorator('hg.admin')
262 261 def settings_global(self):
263 262 c = self.load_default_context()
264 263 c.active = 'global'
265 264 c.personal_repo_group_default_pattern = RepoGroupModel()\
266 265 .get_personal_group_name_pattern()
267 266
268 267 data = render('rhodecode:templates/admin/settings/settings.mako',
269 268 self._get_template_context(c), self.request)
270 269 html = formencode.htmlfill.render(
271 270 data,
272 271 defaults=self._form_defaults(),
273 272 encoding="UTF-8",
274 273 force_defaults=False
275 274 )
276 275 return Response(html)
277 276
278 277 @LoginRequired()
279 278 @HasPermissionAllDecorator('hg.admin')
280 279 @CSRFRequired()
281 280 def settings_global_update(self):
282 281 _ = self.request.translate
283 282 c = self.load_default_context()
284 283 c.active = 'global'
285 284 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 285 .get_personal_group_name_pattern()
287 286 application_form = ApplicationSettingsForm(self.request.translate)()
288 287 try:
289 288 form_result = application_form.to_python(dict(self.request.POST))
290 289 except formencode.Invalid as errors:
291 290 h.flash(
292 291 _("Some form inputs contain invalid data."),
293 292 category='error')
294 293 data = render('rhodecode:templates/admin/settings/settings.mako',
295 294 self._get_template_context(c), self.request)
296 295 html = formencode.htmlfill.render(
297 296 data,
298 297 defaults=errors.value,
299 298 errors=errors.unpack_errors() or {},
300 299 prefix_error=False,
301 300 encoding="UTF-8",
302 301 force_defaults=False
303 302 )
304 303 return Response(html)
305 304
306 305 settings = [
307 306 ('title', 'rhodecode_title', 'unicode'),
308 307 ('realm', 'rhodecode_realm', 'unicode'),
309 308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
310 309 ('post_code', 'rhodecode_post_code', 'unicode'),
311 310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
312 311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
313 312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
314 313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
315 314 ]
316 315
317 316 try:
318 317 for setting, form_key, type_ in settings:
319 318 sett = SettingsModel().create_or_update_setting(
320 319 setting, form_result[form_key], type_)
321 320 Session().add(sett)
322 321
323 322 Session().commit()
324 323 SettingsModel().invalidate_settings_cache()
325 324 h.flash(_('Updated application settings'), category='success')
326 325 except Exception:
327 326 log.exception("Exception while updating application settings")
328 327 h.flash(
329 328 _('Error occurred during updating application settings'),
330 329 category='error')
331 330
332 331 raise HTTPFound(h.route_path('admin_settings_global'))
333 332
334 333 @LoginRequired()
335 334 @HasPermissionAllDecorator('hg.admin')
336 335 def settings_visual(self):
337 336 c = self.load_default_context()
338 337 c.active = 'visual'
339 338
340 339 data = render('rhodecode:templates/admin/settings/settings.mako',
341 340 self._get_template_context(c), self.request)
342 341 html = formencode.htmlfill.render(
343 342 data,
344 343 defaults=self._form_defaults(),
345 344 encoding="UTF-8",
346 345 force_defaults=False
347 346 )
348 347 return Response(html)
349 348
350 349 @LoginRequired()
351 350 @HasPermissionAllDecorator('hg.admin')
352 351 @CSRFRequired()
353 352 def settings_visual_update(self):
354 353 _ = self.request.translate
355 354 c = self.load_default_context()
356 355 c.active = 'visual'
357 356 application_form = ApplicationVisualisationForm(self.request.translate)()
358 357 try:
359 358 form_result = application_form.to_python(dict(self.request.POST))
360 359 except formencode.Invalid as errors:
361 360 h.flash(
362 361 _("Some form inputs contain invalid data."),
363 362 category='error')
364 363 data = render('rhodecode:templates/admin/settings/settings.mako',
365 364 self._get_template_context(c), self.request)
366 365 html = formencode.htmlfill.render(
367 366 data,
368 367 defaults=errors.value,
369 368 errors=errors.unpack_errors() or {},
370 369 prefix_error=False,
371 370 encoding="UTF-8",
372 371 force_defaults=False
373 372 )
374 373 return Response(html)
375 374
376 375 try:
377 376 settings = [
378 377 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
379 378 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
380 379 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
381 380 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
382 381 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
383 382 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
384 383 ('show_version', 'rhodecode_show_version', 'bool'),
385 384 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
386 385 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
387 386 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
388 387 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
389 388 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
390 389 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
391 390 ('support_url', 'rhodecode_support_url', 'unicode'),
392 391 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
393 392 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
394 393 ]
395 394 for setting, form_key, type_ in settings:
396 395 sett = SettingsModel().create_or_update_setting(
397 396 setting, form_result[form_key], type_)
398 397 Session().add(sett)
399 398
400 399 Session().commit()
401 400 SettingsModel().invalidate_settings_cache()
402 401 h.flash(_('Updated visualisation settings'), category='success')
403 402 except Exception:
404 403 log.exception("Exception updating visualization settings")
405 404 h.flash(_('Error occurred during updating '
406 405 'visualisation settings'),
407 406 category='error')
408 407
409 408 raise HTTPFound(h.route_path('admin_settings_visual'))
410 409
411 410 @LoginRequired()
412 411 @HasPermissionAllDecorator('hg.admin')
413 412 def settings_issuetracker(self):
414 413 c = self.load_default_context()
415 414 c.active = 'issuetracker'
416 415 defaults = c.rc_config
417 416
418 417 entry_key = 'rhodecode_issuetracker_pat_'
419 418
420 419 c.issuetracker_entries = {}
421 420 for k, v in defaults.items():
422 421 if k.startswith(entry_key):
423 422 uid = k[len(entry_key):]
424 423 c.issuetracker_entries[uid] = None
425 424
426 425 for uid in c.issuetracker_entries:
427 426 c.issuetracker_entries[uid] = AttributeDict({
428 427 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
429 428 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
430 429 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
431 430 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
432 431 })
433 432
434 433 return self._get_template_context(c)
435 434
436 435 @LoginRequired()
437 436 @HasPermissionAllDecorator('hg.admin')
438 437 @CSRFRequired()
439 438 def settings_issuetracker_test(self):
440 439 error_container = []
441 440
442 441 urlified_commit = h.urlify_commit_message(
443 442 self.request.POST.get('test_text', ''),
444 443 'repo_group/test_repo1', error_container=error_container)
445 444 if error_container:
446 445 def converter(inp):
447 446 return h.html_escape(inp)
448 447
449 448 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
450 449
451 450 return urlified_commit
452 451
453 452 @LoginRequired()
454 453 @HasPermissionAllDecorator('hg.admin')
455 454 @CSRFRequired()
456 455 def settings_issuetracker_update(self):
457 456 _ = self.request.translate
458 457 self.load_default_context()
459 458 settings_model = IssueTrackerSettingsModel()
460 459
461 460 try:
462 461 form = IssueTrackerPatternsForm(self.request.translate)()
463 462 data = form.to_python(self.request.POST)
464 463 except formencode.Invalid as errors:
465 464 log.exception('Failed to add new pattern')
466 465 error = errors
467 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
466 h.flash(_(f'Invalid issue tracker pattern: {error}'),
468 467 category='error')
469 468 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
470 469
471 470 if data:
472 471 for uid in data.get('delete_patterns', []):
473 472 settings_model.delete_entries(uid)
474 473
475 474 for pattern in data.get('patterns', []):
476 475 for setting, value, type_ in pattern:
477 476 sett = settings_model.create_or_update_setting(
478 477 setting, value, type_)
479 478 Session().add(sett)
480 479
481 480 Session().commit()
482 481
483 482 SettingsModel().invalidate_settings_cache()
484 483 h.flash(_('Updated issue tracker entries'), category='success')
485 484 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
486 485
487 486 @LoginRequired()
488 487 @HasPermissionAllDecorator('hg.admin')
489 488 @CSRFRequired()
490 489 def settings_issuetracker_delete(self):
491 490 _ = self.request.translate
492 491 self.load_default_context()
493 492 uid = self.request.POST.get('uid')
494 493 try:
495 494 IssueTrackerSettingsModel().delete_entries(uid)
496 495 except Exception:
497 496 log.exception('Failed to delete issue tracker setting %s', uid)
498 497 raise HTTPNotFound()
499 498
500 499 SettingsModel().invalidate_settings_cache()
501 500 h.flash(_('Removed issue tracker entry.'), category='success')
502 501
503 502 return {'deleted': uid}
504 503
505 504 @LoginRequired()
506 505 @HasPermissionAllDecorator('hg.admin')
507 506 def settings_email(self):
508 507 c = self.load_default_context()
509 508 c.active = 'email'
510 509 c.rhodecode_ini = rhodecode.CONFIG
511 510
512 511 data = render('rhodecode:templates/admin/settings/settings.mako',
513 512 self._get_template_context(c), self.request)
514 513 html = formencode.htmlfill.render(
515 514 data,
516 515 defaults=self._form_defaults(),
517 516 encoding="UTF-8",
518 517 force_defaults=False
519 518 )
520 519 return Response(html)
521 520
522 521 @LoginRequired()
523 522 @HasPermissionAllDecorator('hg.admin')
524 523 @CSRFRequired()
525 524 def settings_email_update(self):
526 525 _ = self.request.translate
527 526 c = self.load_default_context()
528 527 c.active = 'email'
529 528
530 529 test_email = self.request.POST.get('test_email')
531 530
532 531 if not test_email:
533 532 h.flash(_('Please enter email address'), category='error')
534 533 raise HTTPFound(h.route_path('admin_settings_email'))
535 534
536 535 email_kwargs = {
537 536 'date': datetime.datetime.now(),
538 537 'user': self._rhodecode_db_user
539 538 }
540 539
541 540 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
542 541 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
543 542
544 543 recipients = [test_email] if test_email else None
545 544
546 545 run_task(tasks.send_email, recipients, subject,
547 546 email_body_plaintext, email_body)
548 547
549 548 h.flash(_('Send email task created'), category='success')
550 549 raise HTTPFound(h.route_path('admin_settings_email'))
551 550
552 551 @LoginRequired()
553 552 @HasPermissionAllDecorator('hg.admin')
554 553 def settings_hooks(self):
555 554 c = self.load_default_context()
556 555 c.active = 'hooks'
557 556
558 557 model = SettingsModel()
559 558 c.hooks = model.get_builtin_hooks()
560 559 c.custom_hooks = model.get_custom_hooks()
561 560
562 561 data = render('rhodecode:templates/admin/settings/settings.mako',
563 562 self._get_template_context(c), self.request)
564 563 html = formencode.htmlfill.render(
565 564 data,
566 565 defaults=self._form_defaults(),
567 566 encoding="UTF-8",
568 567 force_defaults=False
569 568 )
570 569 return Response(html)
571 570
572 571 @LoginRequired()
573 572 @HasPermissionAllDecorator('hg.admin')
574 573 @CSRFRequired()
575 574 def settings_hooks_update(self):
576 575 _ = self.request.translate
577 576 c = self.load_default_context()
578 577 c.active = 'hooks'
579 578 if c.visual.allow_custom_hooks_settings:
580 579 ui_key = self.request.POST.get('new_hook_ui_key')
581 580 ui_value = self.request.POST.get('new_hook_ui_value')
582 581
583 582 hook_id = self.request.POST.get('hook_id')
584 583 new_hook = False
585 584
586 585 model = SettingsModel()
587 586 try:
588 587 if ui_value and ui_key:
589 588 model.create_or_update_hook(ui_key, ui_value)
590 589 h.flash(_('Added new hook'), category='success')
591 590 new_hook = True
592 591 elif hook_id:
593 592 RhodeCodeUi.delete(hook_id)
594 593 Session().commit()
595 594
596 595 # check for edits
597 596 update = False
598 597 _d = self.request.POST.dict_of_lists()
599 598 for k, v in zip(_d.get('hook_ui_key', []),
600 599 _d.get('hook_ui_value_new', [])):
601 600 model.create_or_update_hook(k, v)
602 601 update = True
603 602
604 603 if update and not new_hook:
605 604 h.flash(_('Updated hooks'), category='success')
606 605 Session().commit()
607 606 except Exception:
608 607 log.exception("Exception during hook creation")
609 608 h.flash(_('Error occurred during hook creation'),
610 609 category='error')
611 610
612 611 raise HTTPFound(h.route_path('admin_settings_hooks'))
613 612
614 613 @LoginRequired()
615 614 @HasPermissionAllDecorator('hg.admin')
616 615 def settings_search(self):
617 616 c = self.load_default_context()
618 617 c.active = 'search'
619 618
620 619 c.searcher = searcher_from_config(self.request.registry.settings)
621 620 c.statistics = c.searcher.statistics(self.request.translate)
622 621
623 622 return self._get_template_context(c)
624 623
625 624 @LoginRequired()
626 625 @HasPermissionAllDecorator('hg.admin')
627 626 def settings_automation(self):
628 627 c = self.load_default_context()
629 628 c.active = 'automation'
630 629
631 630 return self._get_template_context(c)
632 631
633 632 @LoginRequired()
634 633 @HasPermissionAllDecorator('hg.admin')
635 634 def settings_labs(self):
636 635 c = self.load_default_context()
637 636 if not c.labs_active:
638 637 raise HTTPFound(h.route_path('admin_settings'))
639 638
640 639 c.active = 'labs'
641 640 c.lab_settings = _LAB_SETTINGS
642 641
643 642 data = render('rhodecode:templates/admin/settings/settings.mako',
644 643 self._get_template_context(c), self.request)
645 644 html = formencode.htmlfill.render(
646 645 data,
647 646 defaults=self._form_defaults(),
648 647 encoding="UTF-8",
649 648 force_defaults=False
650 649 )
651 650 return Response(html)
652 651
653 652 @LoginRequired()
654 653 @HasPermissionAllDecorator('hg.admin')
655 654 @CSRFRequired()
656 655 def settings_labs_update(self):
657 656 _ = self.request.translate
658 657 c = self.load_default_context()
659 658 c.active = 'labs'
660 659
661 660 application_form = LabsSettingsForm(self.request.translate)()
662 661 try:
663 662 form_result = application_form.to_python(dict(self.request.POST))
664 663 except formencode.Invalid as errors:
665 664 h.flash(
666 665 _("Some form inputs contain invalid data."),
667 666 category='error')
668 667 data = render('rhodecode:templates/admin/settings/settings.mako',
669 668 self._get_template_context(c), self.request)
670 669 html = formencode.htmlfill.render(
671 670 data,
672 671 defaults=errors.value,
673 672 errors=errors.unpack_errors() or {},
674 673 prefix_error=False,
675 674 encoding="UTF-8",
676 675 force_defaults=False
677 676 )
678 677 return Response(html)
679 678
680 679 try:
681 680 session = Session()
682 681 for setting in _LAB_SETTINGS:
683 682 setting_name = setting.key[len('rhodecode_'):]
684 683 sett = SettingsModel().create_or_update_setting(
685 684 setting_name, form_result[setting.key], setting.type)
686 685 session.add(sett)
687 686
688 687 except Exception:
689 688 log.exception('Exception while updating lab settings')
690 689 h.flash(_('Error occurred during updating labs settings'),
691 690 category='error')
692 691 else:
693 692 Session().commit()
694 693 SettingsModel().invalidate_settings_cache()
695 694 h.flash(_('Updated Labs settings'), category='success')
696 695 raise HTTPFound(h.route_path('admin_settings_labs'))
697 696
698 697 data = render('rhodecode:templates/admin/settings/settings.mako',
699 698 self._get_template_context(c), self.request)
700 699 html = formencode.htmlfill.render(
701 700 data,
702 701 defaults=self._form_defaults(),
703 702 encoding="UTF-8",
704 703 force_defaults=False
705 704 )
706 705 return Response(html)
707 706
708 707
709 708 # :param key: name of the setting including the 'rhodecode_' prefix
710 709 # :param type: the RhodeCodeSetting type to use.
711 710 # :param group: the i18ned group in which we should dispaly this setting
712 711 # :param label: the i18ned label we should display for this setting
713 712 # :param help: the i18ned help we should dispaly for this setting
714 713 LabSetting = collections.namedtuple(
715 714 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
716 715
717 716
718 717 # This list has to be kept in sync with the form
719 718 # rhodecode.model.forms.LabsSettingsForm.
720 719 _LAB_SETTINGS = [
721 720
722 721 ]
@@ -1,56 +1,54 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22
25 23 from rhodecode.apps._base import BaseAppView
26 24 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
27 25 from rhodecode.lib.auth import (
28 26 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 27
30 28 log = logging.getLogger(__name__)
31 29
32 30
33 31 class AdminSvnConfigView(BaseAppView):
34 32
35 33 @LoginRequired()
36 34 @HasPermissionAllDecorator('hg.admin')
37 35 @CSRFRequired()
38 36 def vcs_svn_generate_config(self):
39 37 _ = self.request.translate
40 38 try:
41 39 file_path = generate_mod_dav_svn_config(self.request.registry)
42 40 msg = {
43 41 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
44 42 'level': 'success',
45 43 }
46 44 except Exception:
47 45 log.exception(
48 46 'Exception while generating the Apache '
49 47 'configuration for Subversion.')
50 48 msg = {
51 49 'message': _('Failed to generate the Apache configuration for Subversion.'),
52 50 'level': 'error',
53 51 }
54 52
55 53 data = {'message': msg}
56 54 return data
@@ -1,19 +1,17 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,784 +1,782 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20 import datetime
23 21 import string
24 22
25 23 import formencode
26 24 import formencode.htmlfill
27 25 import peppercorn
28 26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 27
30 28 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 29 from rhodecode import forms
32 30 from rhodecode.lib import helpers as h
33 31 from rhodecode.lib import audit_logger
34 32 from rhodecode.lib import ext_json
35 33 from rhodecode.lib.auth import (
36 34 LoginRequired, NotAnonymous, CSRFRequired,
37 35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 36 from rhodecode.lib.channelstream import (
39 37 channelstream_request, ChannelstreamException)
40 38 from rhodecode.lib.hash_utils import md5_safe
41 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 40 from rhodecode.model.auth_token import AuthTokenModel
43 41 from rhodecode.model.comment import CommentsModel
44 42 from rhodecode.model.db import (
45 43 IntegrityError, or_, in_filter_generator,
46 44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
48 46 from rhodecode.model.meta import Session
49 47 from rhodecode.model.pull_request import PullRequestModel
50 48 from rhodecode.model.user import UserModel
51 49 from rhodecode.model.user_group import UserGroupModel
52 50 from rhodecode.model.validation_schema.schemas import user_schema
53 51
54 52 log = logging.getLogger(__name__)
55 53
56 54
57 55 class MyAccountView(BaseAppView, DataGridAppView):
58 56 ALLOW_SCOPED_TOKENS = False
59 57 """
60 58 This view has alternative version inside EE, if modified please take a look
61 59 in there as well.
62 60 """
63 61
64 62 def load_default_context(self):
65 63 c = self._get_local_tmpl_context()
66 64 c.user = c.auth_user.get_instance()
67 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 66 return c
69 67
70 68 @LoginRequired()
71 69 @NotAnonymous()
72 70 def my_account_profile(self):
73 71 c = self.load_default_context()
74 72 c.active = 'profile'
75 73 c.extern_type = c.user.extern_type
76 74 return self._get_template_context(c)
77 75
78 76 @LoginRequired()
79 77 @NotAnonymous()
80 78 def my_account_edit(self):
81 79 c = self.load_default_context()
82 80 c.active = 'profile_edit'
83 81 c.extern_type = c.user.extern_type
84 82 c.extern_name = c.user.extern_name
85 83
86 84 schema = user_schema.UserProfileSchema().bind(
87 85 username=c.user.username, user_emails=c.user.emails)
88 86 appstruct = {
89 87 'username': c.user.username,
90 88 'email': c.user.email,
91 89 'firstname': c.user.firstname,
92 90 'lastname': c.user.lastname,
93 91 'description': c.user.description,
94 92 }
95 93 c.form = forms.RcForm(
96 94 schema, appstruct=appstruct,
97 95 action=h.route_path('my_account_update'),
98 96 buttons=(forms.buttons.save, forms.buttons.reset))
99 97
100 98 return self._get_template_context(c)
101 99
102 100 @LoginRequired()
103 101 @NotAnonymous()
104 102 @CSRFRequired()
105 103 def my_account_update(self):
106 104 _ = self.request.translate
107 105 c = self.load_default_context()
108 106 c.active = 'profile_edit'
109 107 c.perm_user = c.auth_user
110 108 c.extern_type = c.user.extern_type
111 109 c.extern_name = c.user.extern_name
112 110
113 111 schema = user_schema.UserProfileSchema().bind(
114 112 username=c.user.username, user_emails=c.user.emails)
115 113 form = forms.RcForm(
116 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 115
118 116 controls = list(self.request.POST.items())
119 117 try:
120 118 valid_data = form.validate(controls)
121 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
122 120 'new_password', 'password_confirmation']
123 121 if c.extern_type != "rhodecode":
124 122 # forbid updating username for external accounts
125 123 skip_attrs.append('username')
126 124 old_email = c.user.email
127 125 UserModel().update_user(
128 126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
129 127 **valid_data)
130 128 if old_email != valid_data['email']:
131 129 old = UserEmailMap.query() \
132 130 .filter(UserEmailMap.user == c.user)\
133 131 .filter(UserEmailMap.email == valid_data['email'])\
134 132 .first()
135 133 old.email = old_email
136 134 h.flash(_('Your account was updated successfully'), category='success')
137 135 Session().commit()
138 136 except forms.ValidationFailure as e:
139 137 c.form = e
140 138 return self._get_template_context(c)
141 139 except Exception:
142 140 log.exception("Exception updating user")
143 141 h.flash(_('Error occurred during update of user'),
144 142 category='error')
145 143 raise HTTPFound(h.route_path('my_account_profile'))
146 144
147 145 @LoginRequired()
148 146 @NotAnonymous()
149 147 def my_account_password(self):
150 148 c = self.load_default_context()
151 149 c.active = 'password'
152 150 c.extern_type = c.user.extern_type
153 151
154 152 schema = user_schema.ChangePasswordSchema().bind(
155 153 username=c.user.username)
156 154
157 155 form = forms.Form(
158 156 schema,
159 157 action=h.route_path('my_account_password_update'),
160 158 buttons=(forms.buttons.save, forms.buttons.reset))
161 159
162 160 c.form = form
163 161 return self._get_template_context(c)
164 162
165 163 @LoginRequired()
166 164 @NotAnonymous()
167 165 @CSRFRequired()
168 166 def my_account_password_update(self):
169 167 _ = self.request.translate
170 168 c = self.load_default_context()
171 169 c.active = 'password'
172 170 c.extern_type = c.user.extern_type
173 171
174 172 schema = user_schema.ChangePasswordSchema().bind(
175 173 username=c.user.username)
176 174
177 175 form = forms.Form(
178 176 schema, buttons=(forms.buttons.save, forms.buttons.reset))
179 177
180 178 if c.extern_type != 'rhodecode':
181 179 raise HTTPFound(self.request.route_path('my_account_password'))
182 180
183 181 controls = list(self.request.POST.items())
184 182 try:
185 183 valid_data = form.validate(controls)
186 184 UserModel().update_user(c.user.user_id, **valid_data)
187 185 c.user.update_userdata(force_password_change=False)
188 186 Session().commit()
189 187 except forms.ValidationFailure as e:
190 188 c.form = e
191 189 return self._get_template_context(c)
192 190
193 191 except Exception:
194 192 log.exception("Exception updating password")
195 193 h.flash(_('Error occurred during update of user password'),
196 194 category='error')
197 195 else:
198 196 instance = c.auth_user.get_instance()
199 197 self.session.setdefault('rhodecode_user', {}).update(
200 198 {'password': md5_safe(instance.password)})
201 199 self.session.save()
202 200 h.flash(_("Successfully updated password"), category='success')
203 201
204 202 raise HTTPFound(self.request.route_path('my_account_password'))
205 203
206 204 @LoginRequired()
207 205 @NotAnonymous()
208 206 def my_account_auth_tokens(self):
209 207 _ = self.request.translate
210 208
211 209 c = self.load_default_context()
212 210 c.active = 'auth_tokens'
213 211 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
214 212 c.role_values = [
215 213 (x, AuthTokenModel.cls._get_role_name(x))
216 214 for x in AuthTokenModel.cls.ROLES]
217 215 c.role_options = [(c.role_values, _("Role"))]
218 216 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
219 217 c.user.user_id, show_expired=True)
220 218 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
221 219 return self._get_template_context(c)
222 220
223 221 @LoginRequired()
224 222 @NotAnonymous()
225 223 @CSRFRequired()
226 224 def my_account_auth_tokens_view(self):
227 225 _ = self.request.translate
228 226 c = self.load_default_context()
229 227
230 228 auth_token_id = self.request.POST.get('auth_token_id')
231 229
232 230 if auth_token_id:
233 231 token = UserApiKeys.get_or_404(auth_token_id)
234 232 if token.user.user_id != c.user.user_id:
235 233 raise HTTPNotFound()
236 234
237 235 return {
238 236 'auth_token': token.api_key
239 237 }
240 238
241 239 def maybe_attach_token_scope(self, token):
242 240 # implemented in EE edition
243 241 pass
244 242
245 243 @LoginRequired()
246 244 @NotAnonymous()
247 245 @CSRFRequired()
248 246 def my_account_auth_tokens_add(self):
249 247 _ = self.request.translate
250 248 c = self.load_default_context()
251 249
252 250 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
253 251 description = self.request.POST.get('description')
254 252 role = self.request.POST.get('role')
255 253
256 254 token = UserModel().add_auth_token(
257 255 user=c.user.user_id,
258 256 lifetime_minutes=lifetime, role=role, description=description,
259 257 scope_callback=self.maybe_attach_token_scope)
260 258 token_data = token.get_api_data()
261 259
262 260 audit_logger.store_web(
263 261 'user.edit.token.add', action_data={
264 262 'data': {'token': token_data, 'user': 'self'}},
265 263 user=self._rhodecode_user, )
266 264 Session().commit()
267 265
268 266 h.flash(_("Auth token successfully created"), category='success')
269 267 return HTTPFound(h.route_path('my_account_auth_tokens'))
270 268
271 269 @LoginRequired()
272 270 @NotAnonymous()
273 271 @CSRFRequired()
274 272 def my_account_auth_tokens_delete(self):
275 273 _ = self.request.translate
276 274 c = self.load_default_context()
277 275
278 276 del_auth_token = self.request.POST.get('del_auth_token')
279 277
280 278 if del_auth_token:
281 279 token = UserApiKeys.get_or_404(del_auth_token)
282 280 token_data = token.get_api_data()
283 281
284 282 AuthTokenModel().delete(del_auth_token, c.user.user_id)
285 283 audit_logger.store_web(
286 284 'user.edit.token.delete', action_data={
287 285 'data': {'token': token_data, 'user': 'self'}},
288 286 user=self._rhodecode_user,)
289 287 Session().commit()
290 288 h.flash(_("Auth token successfully deleted"), category='success')
291 289
292 290 return HTTPFound(h.route_path('my_account_auth_tokens'))
293 291
294 292 @LoginRequired()
295 293 @NotAnonymous()
296 294 def my_account_emails(self):
297 295 _ = self.request.translate
298 296
299 297 c = self.load_default_context()
300 298 c.active = 'emails'
301 299
302 300 c.user_email_map = UserEmailMap.query()\
303 301 .filter(UserEmailMap.user == c.user).all()
304 302
305 303 schema = user_schema.AddEmailSchema().bind(
306 304 username=c.user.username, user_emails=c.user.emails)
307 305
308 306 form = forms.RcForm(schema,
309 307 action=h.route_path('my_account_emails_add'),
310 308 buttons=(forms.buttons.save, forms.buttons.reset))
311 309
312 310 c.form = form
313 311 return self._get_template_context(c)
314 312
315 313 @LoginRequired()
316 314 @NotAnonymous()
317 315 @CSRFRequired()
318 316 def my_account_emails_add(self):
319 317 _ = self.request.translate
320 318 c = self.load_default_context()
321 319 c.active = 'emails'
322 320
323 321 schema = user_schema.AddEmailSchema().bind(
324 322 username=c.user.username, user_emails=c.user.emails)
325 323
326 324 form = forms.RcForm(
327 325 schema, action=h.route_path('my_account_emails_add'),
328 326 buttons=(forms.buttons.save, forms.buttons.reset))
329 327
330 328 controls = list(self.request.POST.items())
331 329 try:
332 330 valid_data = form.validate(controls)
333 331 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
334 332 audit_logger.store_web(
335 333 'user.edit.email.add', action_data={
336 334 'data': {'email': valid_data['email'], 'user': 'self'}},
337 335 user=self._rhodecode_user,)
338 336 Session().commit()
339 337 except formencode.Invalid as error:
340 338 h.flash(h.escape(error.error_dict['email']), category='error')
341 339 except forms.ValidationFailure as e:
342 340 c.user_email_map = UserEmailMap.query() \
343 341 .filter(UserEmailMap.user == c.user).all()
344 342 c.form = e
345 343 return self._get_template_context(c)
346 344 except Exception:
347 345 log.exception("Exception adding email")
348 346 h.flash(_('Error occurred during adding email'),
349 347 category='error')
350 348 else:
351 349 h.flash(_("Successfully added email"), category='success')
352 350
353 351 raise HTTPFound(self.request.route_path('my_account_emails'))
354 352
355 353 @LoginRequired()
356 354 @NotAnonymous()
357 355 @CSRFRequired()
358 356 def my_account_emails_delete(self):
359 357 _ = self.request.translate
360 358 c = self.load_default_context()
361 359
362 360 del_email_id = self.request.POST.get('del_email_id')
363 361 if del_email_id:
364 362 email = UserEmailMap.get_or_404(del_email_id).email
365 363 UserModel().delete_extra_email(c.user.user_id, del_email_id)
366 364 audit_logger.store_web(
367 365 'user.edit.email.delete', action_data={
368 366 'data': {'email': email, 'user': 'self'}},
369 367 user=self._rhodecode_user,)
370 368 Session().commit()
371 369 h.flash(_("Email successfully deleted"),
372 370 category='success')
373 371 return HTTPFound(h.route_path('my_account_emails'))
374 372
375 373 @LoginRequired()
376 374 @NotAnonymous()
377 375 @CSRFRequired()
378 376 def my_account_notifications_test_channelstream(self):
379 377 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
380 378 self._rhodecode_user.username, datetime.datetime.now())
381 379 payload = {
382 380 # 'channel': 'broadcast',
383 381 'type': 'message',
384 382 'timestamp': datetime.datetime.utcnow(),
385 383 'user': 'system',
386 384 'pm_users': [self._rhodecode_user.username],
387 385 'message': {
388 386 'message': message,
389 387 'level': 'info',
390 388 'topic': '/notifications'
391 389 }
392 390 }
393 391
394 392 registry = self.request.registry
395 393 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
396 394 channelstream_config = rhodecode_plugins.get('channelstream', {})
397 395
398 396 try:
399 397 channelstream_request(channelstream_config, [payload], '/message')
400 398 except ChannelstreamException as e:
401 399 log.exception('Failed to send channelstream data')
402 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
400 return {"response": f'ERROR: {e.__class__.__name__}'}
403 401 return {"response": 'Channelstream data sent. '
404 402 'You should see a new live message now.'}
405 403
406 404 def _load_my_repos_data(self, watched=False):
407 405
408 406 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
409 407
410 408 if watched:
411 409 # repos user watch
412 410 repo_list = Session().query(
413 411 Repository
414 412 ) \
415 413 .join(
416 414 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
417 415 ) \
418 416 .filter(
419 417 UserFollowing.user_id == self._rhodecode_user.user_id
420 418 ) \
421 419 .filter(or_(
422 420 # generate multiple IN to fix limitation problems
423 421 *in_filter_generator(Repository.repo_id, allowed_ids))
424 422 ) \
425 423 .order_by(Repository.repo_name) \
426 424 .all()
427 425
428 426 else:
429 427 # repos user is owner of
430 428 repo_list = Session().query(
431 429 Repository
432 430 ) \
433 431 .filter(
434 432 Repository.user_id == self._rhodecode_user.user_id
435 433 ) \
436 434 .filter(or_(
437 435 # generate multiple IN to fix limitation problems
438 436 *in_filter_generator(Repository.repo_id, allowed_ids))
439 437 ) \
440 438 .order_by(Repository.repo_name) \
441 439 .all()
442 440
443 441 _render = self.request.get_partial_renderer(
444 442 'rhodecode:templates/data_table/_dt_elements.mako')
445 443
446 444 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
447 445 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
448 446 short_name=False, admin=False)
449 447
450 448 repos_data = []
451 449 for repo in repo_list:
452 450 row = {
453 451 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
454 452 repo.private, repo.archived, repo.fork),
455 453 "name_raw": repo.repo_name.lower(),
456 454 }
457 455
458 456 repos_data.append(row)
459 457
460 458 # json used to render the grid
461 459 return ext_json.str_json(repos_data)
462 460
463 461 @LoginRequired()
464 462 @NotAnonymous()
465 463 def my_account_repos(self):
466 464 c = self.load_default_context()
467 465 c.active = 'repos'
468 466
469 467 # json used to render the grid
470 468 c.data = self._load_my_repos_data()
471 469 return self._get_template_context(c)
472 470
473 471 @LoginRequired()
474 472 @NotAnonymous()
475 473 def my_account_watched(self):
476 474 c = self.load_default_context()
477 475 c.active = 'watched'
478 476
479 477 # json used to render the grid
480 478 c.data = self._load_my_repos_data(watched=True)
481 479 return self._get_template_context(c)
482 480
483 481 @LoginRequired()
484 482 @NotAnonymous()
485 483 def my_account_bookmarks(self):
486 484 c = self.load_default_context()
487 485 c.active = 'bookmarks'
488 486 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
489 487 self._rhodecode_db_user.user_id, cache=False)
490 488 return self._get_template_context(c)
491 489
492 490 def _process_bookmark_entry(self, entry, user_id):
493 491 position = safe_int(entry.get('position'))
494 492 cur_position = safe_int(entry.get('cur_position'))
495 493 if position is None:
496 494 return
497 495
498 496 # check if this is an existing entry
499 497 is_new = False
500 498 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
501 499
502 500 if db_entry and str2bool(entry.get('remove')):
503 501 log.debug('Marked bookmark %s for deletion', db_entry)
504 502 Session().delete(db_entry)
505 503 return
506 504
507 505 if not db_entry:
508 506 # new
509 507 db_entry = UserBookmark()
510 508 is_new = True
511 509
512 510 should_save = False
513 511 default_redirect_url = ''
514 512
515 513 # save repo
516 514 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
517 515 repo = Repository.get(entry['bookmark_repo'])
518 516 perm_check = HasRepoPermissionAny(
519 517 'repository.read', 'repository.write', 'repository.admin')
520 518 if repo and perm_check(repo_name=repo.repo_name):
521 519 db_entry.repository = repo
522 520 should_save = True
523 521 default_redirect_url = '${repo_url}'
524 522 # save repo group
525 523 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
526 524 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
527 525 perm_check = HasRepoGroupPermissionAny(
528 526 'group.read', 'group.write', 'group.admin')
529 527
530 528 if repo_group and perm_check(group_name=repo_group.group_name):
531 529 db_entry.repository_group = repo_group
532 530 should_save = True
533 531 default_redirect_url = '${repo_group_url}'
534 532 # save generic info
535 533 elif entry.get('title') and entry.get('redirect_url'):
536 534 should_save = True
537 535
538 536 if should_save:
539 537 # mark user and position
540 538 db_entry.user_id = user_id
541 539 db_entry.position = position
542 540 db_entry.title = entry.get('title')
543 541 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
544 542 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
545 543
546 544 Session().add(db_entry)
547 545
548 546 @LoginRequired()
549 547 @NotAnonymous()
550 548 @CSRFRequired()
551 549 def my_account_bookmarks_update(self):
552 550 _ = self.request.translate
553 551 c = self.load_default_context()
554 552 c.active = 'bookmarks'
555 553
556 554 controls = peppercorn.parse(self.request.POST.items())
557 555 user_id = c.user.user_id
558 556
559 557 # validate positions
560 558 positions = {}
561 559 for entry in controls.get('bookmarks', []):
562 560 position = safe_int(entry['position'])
563 561 if position is None:
564 562 continue
565 563
566 564 if position in positions:
567 565 h.flash(_("Position {} is defined twice. "
568 566 "Please correct this error.").format(position), category='error')
569 567 return HTTPFound(h.route_path('my_account_bookmarks'))
570 568
571 569 entry['position'] = position
572 570 entry['cur_position'] = safe_int(entry.get('cur_position'))
573 571 positions[position] = entry
574 572
575 573 try:
576 574 for entry in positions.values():
577 575 self._process_bookmark_entry(entry, user_id)
578 576
579 577 Session().commit()
580 578 h.flash(_("Update Bookmarks"), category='success')
581 579 except IntegrityError:
582 580 h.flash(_("Failed to update bookmarks. "
583 581 "Make sure an unique position is used."), category='error')
584 582
585 583 return HTTPFound(h.route_path('my_account_bookmarks'))
586 584
587 585 @LoginRequired()
588 586 @NotAnonymous()
589 587 def my_account_goto_bookmark(self):
590 588
591 589 bookmark_id = self.request.matchdict['bookmark_id']
592 590 user_bookmark = UserBookmark().query()\
593 591 .filter(UserBookmark.user_id == self.request.user.user_id) \
594 592 .filter(UserBookmark.position == bookmark_id).scalar()
595 593
596 594 redirect_url = h.route_path('my_account_bookmarks')
597 595 if not user_bookmark:
598 596 raise HTTPFound(redirect_url)
599 597
600 598 # repository set
601 599 if user_bookmark.repository:
602 600 repo_name = user_bookmark.repository.repo_name
603 601 base_redirect_url = h.route_path(
604 602 'repo_summary', repo_name=repo_name)
605 603 if user_bookmark.redirect_url and \
606 604 '${repo_url}' in user_bookmark.redirect_url:
607 605 redirect_url = string.Template(user_bookmark.redirect_url)\
608 606 .safe_substitute({'repo_url': base_redirect_url})
609 607 else:
610 608 redirect_url = base_redirect_url
611 609 # repository group set
612 610 elif user_bookmark.repository_group:
613 611 repo_group_name = user_bookmark.repository_group.group_name
614 612 base_redirect_url = h.route_path(
615 613 'repo_group_home', repo_group_name=repo_group_name)
616 614 if user_bookmark.redirect_url and \
617 615 '${repo_group_url}' in user_bookmark.redirect_url:
618 616 redirect_url = string.Template(user_bookmark.redirect_url)\
619 617 .safe_substitute({'repo_group_url': base_redirect_url})
620 618 else:
621 619 redirect_url = base_redirect_url
622 620 # custom URL set
623 621 elif user_bookmark.redirect_url:
624 622 server_url = h.route_url('home').rstrip('/')
625 623 redirect_url = string.Template(user_bookmark.redirect_url) \
626 624 .safe_substitute({'server_url': server_url})
627 625
628 626 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
629 627 raise HTTPFound(redirect_url)
630 628
631 629 @LoginRequired()
632 630 @NotAnonymous()
633 631 def my_account_perms(self):
634 632 c = self.load_default_context()
635 633 c.active = 'perms'
636 634
637 635 c.perm_user = c.auth_user
638 636 return self._get_template_context(c)
639 637
640 638 @LoginRequired()
641 639 @NotAnonymous()
642 640 def my_notifications(self):
643 641 c = self.load_default_context()
644 642 c.active = 'notifications'
645 643
646 644 return self._get_template_context(c)
647 645
648 646 @LoginRequired()
649 647 @NotAnonymous()
650 648 @CSRFRequired()
651 649 def my_notifications_toggle_visibility(self):
652 650 user = self._rhodecode_db_user
653 651 new_status = not user.user_data.get('notification_status', True)
654 652 user.update_userdata(notification_status=new_status)
655 653 Session().commit()
656 654 return user.user_data['notification_status']
657 655
658 656 def _get_pull_requests_list(self, statuses, filter_type=None):
659 657 draw, start, limit = self._extract_chunk(self.request)
660 658 search_q, order_by, order_dir = self._extract_ordering(self.request)
661 659
662 660 _render = self.request.get_partial_renderer(
663 661 'rhodecode:templates/data_table/_dt_elements.mako')
664 662
665 663 if filter_type == 'awaiting_my_review':
666 664 pull_requests = PullRequestModel().get_im_participating_in_for_review(
667 665 user_id=self._rhodecode_user.user_id,
668 666 statuses=statuses, query=search_q,
669 667 offset=start, length=limit, order_by=order_by,
670 668 order_dir=order_dir)
671 669
672 670 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
673 671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
674 672 else:
675 673 pull_requests = PullRequestModel().get_im_participating_in(
676 674 user_id=self._rhodecode_user.user_id,
677 675 statuses=statuses, query=search_q,
678 676 offset=start, length=limit, order_by=order_by,
679 677 order_dir=order_dir)
680 678
681 679 pull_requests_total_count = PullRequestModel().count_im_participating_in(
682 680 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
683 681
684 682 data = []
685 683 comments_model = CommentsModel()
686 684 for pr in pull_requests:
687 685 repo_id = pr.target_repo_id
688 686 comments_count = comments_model.get_all_comments(
689 687 repo_id, pull_request=pr, include_drafts=False, count_only=True)
690 688 owned = pr.user_id == self._rhodecode_user.user_id
691 689
692 690 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
693 691 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
694 692 if review_statuses and review_statuses[4]:
695 693 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
696 694 my_review_status = statuses[0][1].status
697 695
698 696 data.append({
699 697 'target_repo': _render('pullrequest_target_repo',
700 698 pr.target_repo.repo_name),
701 699 'name': _render('pullrequest_name',
702 700 pr.pull_request_id, pr.pull_request_state,
703 701 pr.work_in_progress, pr.target_repo.repo_name,
704 702 short=True),
705 703 'name_raw': pr.pull_request_id,
706 704 'status': _render('pullrequest_status',
707 705 pr.calculated_review_status()),
708 706 'my_status': _render('pullrequest_status',
709 707 my_review_status),
710 708 'title': _render('pullrequest_title', pr.title, pr.description),
711 709 'description': h.escape(pr.description),
712 710 'updated_on': _render('pullrequest_updated_on',
713 711 h.datetime_to_time(pr.updated_on),
714 712 pr.versions_count),
715 713 'updated_on_raw': h.datetime_to_time(pr.updated_on),
716 714 'created_on': _render('pullrequest_updated_on',
717 715 h.datetime_to_time(pr.created_on)),
718 716 'created_on_raw': h.datetime_to_time(pr.created_on),
719 717 'state': pr.pull_request_state,
720 718 'author': _render('pullrequest_author',
721 719 pr.author.full_contact, ),
722 720 'author_raw': pr.author.full_name,
723 721 'comments': _render('pullrequest_comments', comments_count),
724 722 'comments_raw': comments_count,
725 723 'closed': pr.is_closed(),
726 724 'owned': owned
727 725 })
728 726
729 727 # json used to render the grid
730 728 data = ({
731 729 'draw': draw,
732 730 'data': data,
733 731 'recordsTotal': pull_requests_total_count,
734 732 'recordsFiltered': pull_requests_total_count,
735 733 })
736 734 return data
737 735
738 736 @LoginRequired()
739 737 @NotAnonymous()
740 738 def my_account_pullrequests(self):
741 739 c = self.load_default_context()
742 740 c.active = 'pullrequests'
743 741 req_get = self.request.GET
744 742
745 743 c.closed = str2bool(req_get.get('closed'))
746 744 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
747 745
748 746 c.selected_filter = 'all'
749 747 if c.closed:
750 748 c.selected_filter = 'all_closed'
751 749 if c.awaiting_my_review:
752 750 c.selected_filter = 'awaiting_my_review'
753 751
754 752 return self._get_template_context(c)
755 753
756 754 @LoginRequired()
757 755 @NotAnonymous()
758 756 def my_account_pullrequests_data(self):
759 757 self.load_default_context()
760 758 req_get = self.request.GET
761 759
762 760 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
763 761 closed = str2bool(req_get.get('closed'))
764 762
765 763 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
766 764 if closed:
767 765 statuses += [PullRequest.STATUS_CLOSED]
768 766
769 767 filter_type = \
770 768 'awaiting_my_review' if awaiting_my_review \
771 769 else None
772 770
773 771 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
774 772 return data
775 773
776 774 @LoginRequired()
777 775 @NotAnonymous()
778 776 def my_account_user_group_membership(self):
779 777 c = self.load_default_context()
780 778 c.active = 'user_group_membership'
781 779 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
782 780 for group in self._rhodecode_db_user.group_member]
783 781 c.user_groups = ext_json.str_json(groups)
784 782 return self._get_template_context(c)
@@ -1,184 +1,183 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import logging
21 20
22 21 from pyramid.httpexceptions import (
23 22 HTTPFound, HTTPNotFound, HTTPInternalServerError)
24 23
25 24 from rhodecode.apps._base import BaseAppView
26 25 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
27 26
28 27 from rhodecode.lib import helpers as h
29 28 from rhodecode.lib.helpers import SqlPage
30 29 from rhodecode.lib.utils2 import safe_int
31 30 from rhodecode.model.db import Notification
32 31 from rhodecode.model.notification import NotificationModel
33 32 from rhodecode.model.meta import Session
34 33
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37
39 38 class MyAccountNotificationsView(BaseAppView):
40 39
41 40 def load_default_context(self):
42 41 c = self._get_local_tmpl_context()
43 42 c.user = c.auth_user.get_instance()
44 43
45 44 return c
46 45
47 46 def _has_permissions(self, notification):
48 47 def is_owner():
49 48 user_id = self._rhodecode_db_user.user_id
50 49 for user_notification in notification.notifications_to_users:
51 50 if user_notification.user.user_id == user_id:
52 51 return True
53 52 return False
54 53 return h.HasPermissionAny('hg.admin')() or is_owner()
55 54
56 55 @LoginRequired()
57 56 @NotAnonymous()
58 57 def notifications_show_all(self):
59 58 c = self.load_default_context()
60 59
61 60 c.unread_count = NotificationModel().get_unread_cnt_for_user(
62 61 self._rhodecode_db_user.user_id)
63 62
64 63 _current_filter = self.request.GET.getall('type') or ['unread']
65 64
66 65 notifications = NotificationModel().get_for_user(
67 66 self._rhodecode_db_user.user_id,
68 67 filter_=_current_filter)
69 68
70 69 p = safe_int(self.request.GET.get('page', 1), 1)
71 70
72 71 def url_generator(page_num):
73 72 query_params = {
74 73 'page': page_num
75 74 }
76 75 _query = self.request.GET.mixed()
77 76 query_params.update(_query)
78 77 return self.request.current_route_path(_query=query_params)
79 78
80 79 c.notifications = SqlPage(notifications, page=p, items_per_page=10,
81 80 url_maker=url_generator)
82 81
83 82 c.unread_type = 'unread'
84 83 c.all_type = 'all'
85 84 c.pull_request_type = Notification.TYPE_PULL_REQUEST
86 85 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
87 86 Notification.TYPE_PULL_REQUEST_COMMENT]
88 87
89 88 c.current_filter = 'unread' # default filter
90 89
91 90 if _current_filter == [c.pull_request_type]:
92 91 c.current_filter = 'pull_request'
93 92 elif _current_filter == c.comment_type:
94 93 c.current_filter = 'comment'
95 94 elif _current_filter == [c.unread_type]:
96 95 c.current_filter = 'unread'
97 96 elif _current_filter == [c.all_type]:
98 97 c.current_filter = 'all'
99 98 return self._get_template_context(c)
100 99
101 100 @LoginRequired()
102 101 @NotAnonymous()
103 102 def notifications_show(self):
104 103 c = self.load_default_context()
105 104 notification_id = self.request.matchdict['notification_id']
106 105 notification = Notification.get_or_404(notification_id)
107 106
108 107 if not self._has_permissions(notification):
109 108 log.debug('User %s does not have permission to access notification',
110 109 self._rhodecode_user)
111 110 raise HTTPNotFound()
112 111
113 112 u_notification = NotificationModel().get_user_notification(
114 113 self._rhodecode_db_user.user_id, notification)
115 114 if not u_notification:
116 115 log.debug('User %s notification does not exist',
117 116 self._rhodecode_user)
118 117 raise HTTPNotFound()
119 118
120 119 # when opening this notification, mark it as read for this use
121 120 if not u_notification.read:
122 121 u_notification.mark_as_read()
123 122 Session().commit()
124 123
125 124 c.notification = notification
126 125
127 126 return self._get_template_context(c)
128 127
129 128 @LoginRequired()
130 129 @NotAnonymous()
131 130 @CSRFRequired()
132 131 def notifications_mark_all_read(self):
133 132 NotificationModel().mark_all_read_for_user(
134 133 self._rhodecode_db_user.user_id,
135 134 filter_=self.request.GET.getall('type'))
136 135 Session().commit()
137 136 raise HTTPFound(h.route_path('notifications_show_all'))
138 137
139 138 @LoginRequired()
140 139 @NotAnonymous()
141 140 @CSRFRequired()
142 141 def notification_update(self):
143 142 notification_id = self.request.matchdict['notification_id']
144 143 notification = Notification.get_or_404(notification_id)
145 144
146 145 if not self._has_permissions(notification):
147 146 log.debug('User %s does not have permission to access notification',
148 147 self._rhodecode_user)
149 148 raise HTTPNotFound()
150 149
151 150 try:
152 151 # updates notification read flag
153 152 NotificationModel().mark_read(
154 153 self._rhodecode_user.user_id, notification)
155 154 Session().commit()
156 155 return 'ok'
157 156 except Exception:
158 157 Session().rollback()
159 158 log.exception("Exception updating a notification item")
160 159
161 160 raise HTTPInternalServerError()
162 161
163 162 @LoginRequired()
164 163 @NotAnonymous()
165 164 @CSRFRequired()
166 165 def notification_delete(self):
167 166 notification_id = self.request.matchdict['notification_id']
168 167 notification = Notification.get_or_404(notification_id)
169 168 if not self._has_permissions(notification):
170 169 log.debug('User %s does not have permission to access notification',
171 170 self._rhodecode_user)
172 171 raise HTTPNotFound()
173 172
174 173 try:
175 174 # deletes only notification2user
176 175 NotificationModel().delete(
177 176 self._rhodecode_user.user_id, notification)
178 177 Session().commit()
179 178 return 'ok'
180 179 except Exception:
181 180 Session().rollback()
182 181 log.exception("Exception deleting a notification item")
183 182
184 183 raise HTTPInternalServerError()
@@ -1,146 +1,144 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPFound
24 22
25 23 from rhodecode.apps._base import BaseAppView, DataGridAppView
26 24 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
27 25 from rhodecode.events import trigger
28 26 from rhodecode.lib import helpers as h
29 27 from rhodecode.lib import audit_logger
30 28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
31 29 from rhodecode.model.db import IntegrityError, UserSshKeys
32 30 from rhodecode.model.meta import Session
33 31 from rhodecode.model.ssh_key import SshKeyModel
34 32
35 33 log = logging.getLogger(__name__)
36 34
37 35
38 36 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
39 37
40 38 def load_default_context(self):
41 39 c = self._get_local_tmpl_context()
42 40 c.user = c.auth_user.get_instance()
43 41 c.ssh_enabled = self.request.registry.settings.get(
44 42 'ssh.generate_authorized_keyfile')
45 43 return c
46 44
47 45 @LoginRequired()
48 46 @NotAnonymous()
49 47 def my_account_ssh_keys(self):
50 48 _ = self.request.translate
51 49
52 50 c = self.load_default_context()
53 51 c.active = 'ssh_keys'
54 52 c.default_key = self.request.GET.get('default_key')
55 53 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
56 54 return self._get_template_context(c)
57 55
58 56 @LoginRequired()
59 57 @NotAnonymous()
60 58 def ssh_keys_generate_keypair(self):
61 59 _ = self.request.translate
62 60 c = self.load_default_context()
63 61
64 62 c.active = 'ssh_keys_generate'
65 63 if c.ssh_key_generator_enabled:
66 64 private_format = self.request.GET.get('private_format') \
67 65 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
68 66 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
69 67 c.private, c.public = SshKeyModel().generate_keypair(
70 68 comment=comment, private_format=private_format)
71 69 c.target_form_url = h.route_path(
72 70 'my_account_ssh_keys', _query=dict(default_key=c.public))
73 71 return self._get_template_context(c)
74 72
75 73 @LoginRequired()
76 74 @NotAnonymous()
77 75 @CSRFRequired()
78 76 def my_account_ssh_keys_add(self):
79 77 _ = self.request.translate
80 78 c = self.load_default_context()
81 79
82 80 user_data = c.user.get_api_data()
83 81 key_data = self.request.POST.get('key_data')
84 82 description = self.request.POST.get('description')
85 83 fingerprint = 'unknown'
86 84 try:
87 85 if not key_data:
88 86 raise ValueError('Please add a valid public key')
89 87
90 88 key = SshKeyModel().parse_key(key_data.strip())
91 89 fingerprint = key.hash_md5()
92 90
93 91 ssh_key = SshKeyModel().create(
94 92 c.user.user_id, fingerprint, key.keydata, description)
95 93 ssh_key_data = ssh_key.get_api_data()
96 94
97 95 audit_logger.store_web(
98 96 'user.edit.ssh_key.add', action_data={
99 97 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
100 98 user=self._rhodecode_user, )
101 99 Session().commit()
102 100
103 101 # Trigger an event on change of keys.
104 102 trigger(SshKeyFileChangeEvent(), self.request.registry)
105 103
106 104 h.flash(_("Ssh Key successfully created"), category='success')
107 105
108 106 except IntegrityError:
109 107 log.exception("Exception during ssh key saving")
110 108 err = 'Such key with fingerprint `{}` already exists, ' \
111 109 'please use a different one'.format(fingerprint)
112 110 h.flash(_('An error occurred during ssh key saving: {}').format(err),
113 111 category='error')
114 112 except Exception as e:
115 113 log.exception("Exception during ssh key saving")
116 114 h.flash(_('An error occurred during ssh key saving: {}').format(e),
117 115 category='error')
118 116
119 117 return HTTPFound(h.route_path('my_account_ssh_keys'))
120 118
121 119 @LoginRequired()
122 120 @NotAnonymous()
123 121 @CSRFRequired()
124 122 def my_account_ssh_keys_delete(self):
125 123 _ = self.request.translate
126 124 c = self.load_default_context()
127 125
128 126 user_data = c.user.get_api_data()
129 127
130 128 del_ssh_key = self.request.POST.get('del_ssh_key')
131 129
132 130 if del_ssh_key:
133 131 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
134 132 ssh_key_data = ssh_key.get_api_data()
135 133
136 134 SshKeyModel().delete(del_ssh_key, c.user.user_id)
137 135 audit_logger.store_web(
138 136 'user.edit.ssh_key.delete', action_data={
139 137 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
140 138 user=self._rhodecode_user,)
141 139 Session().commit()
142 140 # Trigger an event on change of keys.
143 141 trigger(SshKeyFileChangeEvent(), self.request.registry)
144 142 h.flash(_("Ssh key successfully deleted"), category='success')
145 143
146 144 return HTTPFound(h.route_path('my_account_ssh_keys'))
@@ -1,64 +1,62 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 from rhodecode.apps._base import ADMIN_PREFIX
22 20
23 21
24 22 def admin_routes(config):
25 23 from rhodecode.apps.ops.views import OpsView
26 24
27 25 config.add_route(
28 26 name='ops_ping',
29 27 pattern='/ping')
30 28 config.add_view(
31 29 OpsView,
32 30 attr='ops_ping',
33 31 route_name='ops_ping', request_method='GET',
34 32 renderer='json_ext')
35 33
36 34 config.add_route(
37 35 name='ops_error_test',
38 36 pattern='/error')
39 37 config.add_view(
40 38 OpsView,
41 39 attr='ops_error_test',
42 40 route_name='ops_error_test', request_method='GET',
43 41 renderer='json_ext')
44 42
45 43 config.add_route(
46 44 name='ops_redirect_test',
47 45 pattern='/redirect')
48 46 config.add_view(
49 47 OpsView,
50 48 attr='ops_redirect_test',
51 49 route_name='ops_redirect_test', request_method='GET',
52 50 renderer='json_ext')
53 51
54 52 config.add_route(
55 53 name='ops_healthcheck',
56 54 pattern='/status')
57 55 config.add_view(
58 56 OpsView,
59 57 attr='ops_healthcheck',
60 58 route_name='ops_healthcheck', request_method='GET',
61 59 renderer='json_ext')
62 60
63 61 def includeme(config):
64 62 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
@@ -1,96 +1,94 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import time
22 20 import logging
23 21
24 22
25 23 from pyramid.httpexceptions import HTTPFound
26 24
27 25 from rhodecode.apps._base import BaseAppView
28 26 from rhodecode.lib import helpers as h
29 27 from rhodecode.lib.auth import LoginRequired
30 28 from collections import OrderedDict
31 29 from rhodecode.model.db import UserApiKeys
32 30
33 31 log = logging.getLogger(__name__)
34 32
35 33
36 34 class OpsView(BaseAppView):
37 35
38 36 def load_default_context(self):
39 37 c = self._get_local_tmpl_context()
40 38 c.user = c.auth_user.get_instance()
41 39
42 40 return c
43 41
44 42 def ops_ping(self):
45 43 data = OrderedDict()
46 44 data['instance'] = self.request.registry.settings.get('instance_id')
47 45
48 46 if getattr(self.request, 'user'):
49 47 caller_name = 'anonymous'
50 48 if self.request.user.user_id:
51 49 caller_name = self.request.user.username
52 50
53 51 data['caller_ip'] = self.request.user.ip_addr
54 52 data['caller_name'] = caller_name
55 53
56 54 return {'ok': data}
57 55
58 56 def ops_error_test(self):
59 57 """
60 58 Test exception handling and emails on errors
61 59 """
62 60
63 61 class TestException(Exception):
64 62 pass
65 63 # add timeout so we add some sort of rate limiter
66 64 time.sleep(2)
67 65 msg = ('RhodeCode Enterprise test exception. '
68 66 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
69 67 raise TestException(msg)
70 68
71 69 def ops_redirect_test(self):
72 70 """
73 71 Test redirect handling
74 72 """
75 73 redirect_to = self.request.GET.get('to') or h.route_path('home')
76 74 raise HTTPFound(redirect_to)
77 75
78 76 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_HTTP])
79 77 def ops_healthcheck(self):
80 78 from rhodecode.lib.system_info import load_system_info
81 79
82 80 vcsserver_info = load_system_info('vcs_server')
83 81 if vcsserver_info:
84 82 vcsserver_info = vcsserver_info['human_value']
85 83
86 84 db_info = load_system_info('database_info')
87 85 if db_info:
88 86 db_info = db_info['human_value']
89 87
90 88 health_spec = {
91 89 'caller_ip': self.request.user.ip_addr,
92 90 'vcsserver': vcsserver_info,
93 91 'db': db_info,
94 92 }
95 93
96 94 return {'healthcheck': health_spec}
@@ -1,102 +1,100 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18 from rhodecode.apps._base import add_route_with_slash
21 19 from rhodecode.apps.repo_group.views.repo_group_settings import RepoGroupSettingsView
22 20 from rhodecode.apps.repo_group.views.repo_group_advanced import RepoGroupAdvancedSettingsView
23 21 from rhodecode.apps.repo_group.views.repo_group_permissions import RepoGroupPermissionsView
24 22 from rhodecode.apps.home.views import HomeView
25 23
26 24
27 25 def includeme(config):
28 26
29 27 # Settings
30 28 config.add_route(
31 29 name='edit_repo_group',
32 30 pattern='/{repo_group_name:.*?[^/]}/_edit',
33 31 repo_group_route=True)
34 32 config.add_view(
35 33 RepoGroupSettingsView,
36 34 attr='edit_settings',
37 35 route_name='edit_repo_group', request_method='GET',
38 36 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
39 37 config.add_view(
40 38 RepoGroupSettingsView,
41 39 attr='edit_settings_update',
42 40 route_name='edit_repo_group', request_method='POST',
43 41 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
44 42
45 43 # Settings advanced
46 44 config.add_route(
47 45 name='edit_repo_group_advanced',
48 46 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
49 47 repo_group_route=True)
50 48 config.add_view(
51 49 RepoGroupAdvancedSettingsView,
52 50 attr='edit_repo_group_advanced',
53 51 route_name='edit_repo_group_advanced', request_method='GET',
54 52 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
55 53
56 54 config.add_route(
57 55 name='edit_repo_group_advanced_delete',
58 56 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
59 57 repo_group_route=True)
60 58 config.add_view(
61 59 RepoGroupAdvancedSettingsView,
62 60 attr='edit_repo_group_delete',
63 61 route_name='edit_repo_group_advanced_delete', request_method='POST',
64 62 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
65 63
66 64 # settings permissions
67 65 config.add_route(
68 66 name='edit_repo_group_perms',
69 67 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
70 68 repo_group_route=True)
71 69 config.add_view(
72 70 RepoGroupPermissionsView,
73 71 attr='edit_repo_group_permissions',
74 72 route_name='edit_repo_group_perms', request_method='GET',
75 73 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
76 74
77 75 config.add_route(
78 76 name='edit_repo_group_perms_update',
79 77 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
80 78 repo_group_route=True)
81 79 config.add_view(
82 80 RepoGroupPermissionsView,
83 81 attr='edit_repo_groups_permissions_update',
84 82 route_name='edit_repo_group_perms_update', request_method='POST',
85 83 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
86 84
87 85 # Summary, NOTE(marcink): needs to be at the end for catch-all
88 86 add_route_with_slash(
89 87 config,
90 88 name='repo_group_home',
91 89 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
92 90 config.add_view(
93 91 HomeView,
94 92 attr='repo_group_main_page',
95 93 route_name='repo_group_home', request_method='GET',
96 94 renderer='rhodecode:templates/index_repo_group.mako')
97 95 config.add_view(
98 96 HomeView,
99 97 attr='repo_group_main_page',
100 98 route_name='repo_group_home_slash', request_method='GET',
101 99 renderer='rhodecode:templates/index_repo_group.mako')
102 100
@@ -1,18 +1,17 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,90 +1,89 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import pytest
21 20
22 21 from rhodecode.tests import assert_session_flash
23 22
24 23
25 24 def route_path(name, params=None, **kwargs):
26 25 import urllib.request
27 26 import urllib.parse
28 27 import urllib.error
29 28
30 29 base_url = {
31 30 'edit_repo_group_advanced':
32 31 '/{repo_group_name}/_settings/advanced',
33 32 'edit_repo_group_advanced_delete':
34 33 '/{repo_group_name}/_settings/advanced/delete',
35 34 }[name].format(**kwargs)
36 35
37 36 if params:
38 37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 38 return base_url
40 39
41 40
42 41 @pytest.mark.usefixtures("app")
43 42 class TestRepoGroupsAdvancedView(object):
44 43
45 44 @pytest.mark.parametrize('repo_group_name', [
46 45 'gro',
47 46 '12345',
48 47 ])
49 48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
50 49 user_util._test_name = repo_group_name
51 50 gr = user_util.create_repo_group()
52 51 self.app.get(
53 52 route_path('edit_repo_group_advanced',
54 53 repo_group_name=gr.group_name))
55 54
56 55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
57 56 csrf_token):
58 57 gr = user_util.create_repo_group(auto_cleanup=False)
59 58 repo_group_name = gr.group_name
60 59
61 60 params = dict(
62 61 csrf_token=csrf_token
63 62 )
64 63 response = self.app.post(
65 64 route_path('edit_repo_group_advanced_delete',
66 65 repo_group_name=repo_group_name), params=params)
67 66 assert_session_flash(
68 67 response, 'Removed repository group `{}`'.format(repo_group_name))
69 68
70 69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
71 70 repo_groups, csrf_token):
72 71 zombie_group, parent_group, child_group = repo_groups
73 72
74 73 response = self.app.get(
75 74 route_path('edit_repo_group_advanced',
76 75 repo_group_name=parent_group.group_name))
77 76
78 77 response.mustcontain(
79 78 'This repository group includes 1 children repository group')
80 79
81 80 params = dict(
82 81 csrf_token=csrf_token
83 82 )
84 83 response = self.app.post(
85 84 route_path('edit_repo_group_advanced_delete',
86 85 repo_group_name=parent_group.group_name), params=params)
87 86
88 87 assert_session_flash(
89 88 response, 'This repository group contains 1 subgroup '
90 89 'and cannot be deleted')
@@ -1,87 +1,86 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import pytest
21 20
22 21 from rhodecode.tests.utils import permission_update_data_generator
23 22
24 23
25 24 def route_path(name, params=None, **kwargs):
26 25 import urllib.request
27 26 import urllib.parse
28 27 import urllib.error
29 28
30 29 base_url = {
31 30 'edit_repo_group_perms':
32 31 '/{repo_group_name:}/_settings/permissions',
33 32 'edit_repo_group_perms_update':
34 33 '/{repo_group_name}/_settings/permissions/update',
35 34 }[name].format(**kwargs)
36 35
37 36 if params:
38 37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 38 return base_url
40 39
41 40
42 41 @pytest.mark.usefixtures("app")
43 42 class TestRepoGroupPermissionsView(object):
44 43
45 44 def test_edit_perms_view(self, user_util, autologin_user):
46 45 repo_group = user_util.create_repo_group()
47 46
48 47 self.app.get(
49 48 route_path('edit_repo_group_perms',
50 49 repo_group_name=repo_group.group_name), status=200)
51 50
52 51 def test_update_permissions(self, csrf_token, user_util):
53 52 repo_group = user_util.create_repo_group()
54 53 repo_group_name = repo_group.group_name
55 54 user = user_util.create_user()
56 55 user_id = user.user_id
57 56 username = user.username
58 57
59 58 # grant new
60 59 form_data = permission_update_data_generator(
61 60 csrf_token,
62 61 default='group.write',
63 62 grant=[(user_id, 'group.write', username, 'user')])
64 63
65 64 # recursive flag required for repo groups
66 65 form_data.extend([('recursive', u'none')])
67 66
68 67 response = self.app.post(
69 68 route_path('edit_repo_group_perms_update',
70 69 repo_group_name=repo_group_name), form_data).follow()
71 70
72 71 assert 'Repository Group permissions updated' in response
73 72
74 73 # revoke given
75 74 form_data = permission_update_data_generator(
76 75 csrf_token,
77 76 default='group.read',
78 77 revoke=[(user_id, 'user')])
79 78
80 79 # recursive flag required for repo groups
81 80 form_data.extend([('recursive', u'none')])
82 81
83 82 response = self.app.post(
84 83 route_path('edit_repo_group_perms_update',
85 84 repo_group_name=repo_group_name), form_data).follow()
86 85
87 86 assert 'Repository Group permissions updated' in response
@@ -1,19 +1,17 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/ No newline at end of file
@@ -1,105 +1,103 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22 from pyramid.httpexceptions import HTTPFound
25 23
26 24 from rhodecode.apps._base import RepoGroupAppView
27 25 from rhodecode.lib import helpers as h
28 26 from rhodecode.lib import audit_logger
29 27 from rhodecode.lib.auth import (
30 28 LoginRequired, CSRFRequired, HasRepoGroupPermissionAnyDecorator)
31 29 from rhodecode.model.repo_group import RepoGroupModel
32 30 from rhodecode.model.meta import Session
33 31
34 32 log = logging.getLogger(__name__)
35 33
36 34
37 35 class RepoGroupAdvancedSettingsView(RepoGroupAppView):
38 36 def load_default_context(self):
39 37 c = self._get_local_tmpl_context()
40 38 return c
41 39
42 40 @LoginRequired()
43 41 @HasRepoGroupPermissionAnyDecorator('group.admin')
44 42 def edit_repo_group_advanced(self):
45 43 _ = self.request.translate
46 44 c = self.load_default_context()
47 45 c.active = 'advanced'
48 46 c.repo_group = self.db_repo_group
49 47
50 48 # update commit cache if GET flag is present
51 49 if self.request.GET.get('update_commit_cache'):
52 50 self.db_repo_group.update_commit_cache()
53 51 h.flash(_('updated commit cache'), category='success')
54 52
55 53 return self._get_template_context(c)
56 54
57 55 @LoginRequired()
58 56 @HasRepoGroupPermissionAnyDecorator('group.admin')
59 57 @CSRFRequired()
60 58 def edit_repo_group_delete(self):
61 59 _ = self.request.translate
62 60 _ungettext = self.request.plularize
63 61 c = self.load_default_context()
64 62 c.repo_group = self.db_repo_group
65 63
66 64 repos = c.repo_group.repositories.all()
67 65 if repos:
68 66 msg = _ungettext(
69 67 'This repository group contains %(num)d repository and cannot be deleted',
70 68 'This repository group contains %(num)d repositories and cannot be'
71 69 ' deleted',
72 70 len(repos)) % {'num': len(repos)}
73 71 h.flash(msg, category='warning')
74 72 raise HTTPFound(
75 73 h.route_path('edit_repo_group_advanced',
76 74 repo_group_name=self.db_repo_group_name))
77 75
78 76 children = c.repo_group.children.all()
79 77 if children:
80 78 msg = _ungettext(
81 79 'This repository group contains %(num)d subgroup and cannot be deleted',
82 80 'This repository group contains %(num)d subgroups and cannot be deleted',
83 81 len(children)) % {'num': len(children)}
84 82 h.flash(msg, category='warning')
85 83 raise HTTPFound(
86 84 h.route_path('edit_repo_group_advanced',
87 85 repo_group_name=self.db_repo_group_name))
88 86
89 87 try:
90 88 old_values = c.repo_group.get_api_data()
91 89 RepoGroupModel().delete(self.db_repo_group_name)
92 90
93 91 audit_logger.store_web(
94 92 'repo_group.delete', action_data={'old_data': old_values},
95 93 user=c.rhodecode_user)
96 94
97 95 Session().commit()
98 96 h.flash(_('Removed repository group `%s`') % self.db_repo_group_name,
99 97 category='success')
100 98 except Exception:
101 99 log.exception("Exception during deletion of repository group")
102 100 h.flash(_('Error occurred during deletion of repository group %s')
103 101 % self.db_repo_group_name, category='error')
104 102
105 103 raise HTTPFound(h.route_path('repo_groups'))
@@ -1,104 +1,102 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22 from pyramid.httpexceptions import HTTPFound
25 23
26 24 from rhodecode.apps._base import RepoGroupAppView
27 25 from rhodecode.lib import helpers as h
28 26 from rhodecode.lib import audit_logger
29 27 from rhodecode.lib.auth import (
30 28 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 29 from rhodecode.model.db import User
32 30 from rhodecode.model.permission import PermissionModel
33 31 from rhodecode.model.repo_group import RepoGroupModel
34 32 from rhodecode.model.forms import RepoGroupPermsForm
35 33 from rhodecode.model.meta import Session
36 34
37 35 log = logging.getLogger(__name__)
38 36
39 37
40 38 class RepoGroupPermissionsView(RepoGroupAppView):
41 39 def load_default_context(self):
42 40 c = self._get_local_tmpl_context()
43 41
44 42 return c
45 43
46 44 @LoginRequired()
47 45 @HasRepoGroupPermissionAnyDecorator('group.admin')
48 46 def edit_repo_group_permissions(self):
49 47 c = self.load_default_context()
50 48 c.active = 'permissions'
51 49 c.repo_group = self.db_repo_group
52 50 return self._get_template_context(c)
53 51
54 52 @LoginRequired()
55 53 @HasRepoGroupPermissionAnyDecorator('group.admin')
56 54 @CSRFRequired()
57 55 def edit_repo_groups_permissions_update(self):
58 56 _ = self.request.translate
59 57 c = self.load_default_context()
60 58 c.active = 'perms'
61 59 c.repo_group = self.db_repo_group
62 60
63 61 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
64 62 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
65 63 .to_python(self.request.POST)
66 64
67 65 if not c.rhodecode_user.is_admin:
68 66 if self._revoke_perms_on_yourself(form):
69 67 msg = _('Cannot change permission for yourself as admin')
70 68 h.flash(msg, category='warning')
71 69 raise HTTPFound(
72 70 h.route_path('edit_repo_group_perms',
73 71 repo_group_name=self.db_repo_group_name))
74 72
75 73 # iterate over all members(if in recursive mode) of this groups and
76 74 # set the permissions !
77 75 # this can be potentially heavy operation
78 76 changes = RepoGroupModel().update_permissions(
79 77 c.repo_group,
80 78 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
81 79 form['recursive'])
82 80
83 81 action_data = {
84 82 'added': changes['added'],
85 83 'updated': changes['updated'],
86 84 'deleted': changes['deleted'],
87 85 }
88 86 audit_logger.store_web(
89 87 'repo_group.edit.permissions', action_data=action_data,
90 88 user=c.rhodecode_user)
91 89
92 90 Session().commit()
93 91 h.flash(_('Repository Group permissions updated'), category='success')
94 92
95 93 affected_user_ids = None
96 94 if changes.get('default_user_changed', False):
97 95 # if we change the default user, we need to flush everyone permissions
98 96 affected_user_ids = User.get_all_user_ids()
99 97 PermissionModel().flush_user_permission_caches(
100 98 changes, affected_user_ids=affected_user_ids)
101 99
102 100 raise HTTPFound(
103 101 h.route_path('edit_repo_group_perms',
104 102 repo_group_name=self.db_repo_group_name))
@@ -1,187 +1,185 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20 import deform
23 21
24 22
25 23 from pyramid.httpexceptions import HTTPFound
26 24
27 25 from rhodecode import events
28 26 from rhodecode.apps._base import RepoGroupAppView
29 27 from rhodecode.forms import RcForm
30 28 from rhodecode.lib import helpers as h
31 29 from rhodecode.lib import audit_logger
32 30 from rhodecode.lib.auth import (
33 31 LoginRequired, HasPermissionAll,
34 32 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
35 33 from rhodecode.model.db import Session, RepoGroup, User
36 34 from rhodecode.model.permission import PermissionModel
37 35 from rhodecode.model.scm import RepoGroupList
38 36 from rhodecode.model.repo_group import RepoGroupModel
39 37 from rhodecode.model.validation_schema.schemas import repo_group_schema
40 38
41 39 log = logging.getLogger(__name__)
42 40
43 41
44 42 class RepoGroupSettingsView(RepoGroupAppView):
45 43 def load_default_context(self):
46 44 c = self._get_local_tmpl_context()
47 45 c.repo_group = self.db_repo_group
48 46 no_parrent = not c.repo_group.parent_group
49 47 can_create_in_root = self._can_create_repo_group()
50 48
51 49 show_root_location = False
52 50 if no_parrent or can_create_in_root:
53 51 # we're global admin, we're ok and we can create TOP level groups
54 52 # or in case this group is already at top-level we also allow
55 53 # creation in root
56 54 show_root_location = True
57 55
58 56 acl_groups = RepoGroupList(
59 57 RepoGroup.query().all(),
60 58 perm_set=['group.admin'])
61 59 c.repo_groups = RepoGroup.groups_choices(
62 60 groups=acl_groups,
63 61 show_empty_group=show_root_location)
64 62 # filter out current repo group
65 63 exclude_group_ids = [c.repo_group.group_id]
66 64 c.repo_groups = [x for x in c.repo_groups if x[0] not in exclude_group_ids]
67 65 c.repo_groups_choices = [k[0] for k in c.repo_groups]
68 66
69 67 parent_group = c.repo_group.parent_group
70 68
71 69 add_parent_group = (parent_group and (
72 70 parent_group.group_id not in c.repo_groups_choices))
73 71 if add_parent_group:
74 72 c.repo_groups_choices.append(parent_group.group_id)
75 73 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
76 74 return c
77 75
78 76 def _can_create_repo_group(self, parent_group_id=None):
79 77 is_admin = HasPermissionAll('hg.admin')('group create controller')
80 78 create_repo_group = HasPermissionAll(
81 79 'hg.repogroup.create.true')('group create controller')
82 80 if is_admin or (create_repo_group and not parent_group_id):
83 81 # we're global admin, or we have global repo group create
84 82 # permission
85 83 # we're ok and we can create TOP level groups
86 84 return True
87 85 elif parent_group_id:
88 86 # we check the permission if we can write to parent group
89 87 group = RepoGroup.get(parent_group_id)
90 88 group_name = group.group_name if group else None
91 89 if HasRepoGroupPermissionAny('group.admin')(
92 90 group_name, 'check if user is an admin of group'):
93 91 # we're an admin of passed in group, we're ok.
94 92 return True
95 93 else:
96 94 return False
97 95 return False
98 96
99 97 def _get_schema(self, c, old_values=None):
100 98 return repo_group_schema.RepoGroupSettingsSchema().bind(
101 99 repo_group_repo_group_options=c.repo_groups_choices,
102 100 repo_group_repo_group_items=c.repo_groups,
103 101
104 102 # user caller
105 103 user=self._rhodecode_user,
106 104 old_values=old_values
107 105 )
108 106
109 107 @LoginRequired()
110 108 @HasRepoGroupPermissionAnyDecorator('group.admin')
111 109 def edit_settings(self):
112 110 c = self.load_default_context()
113 111 c.active = 'settings'
114 112
115 113 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
116 114 defaults['repo_group_owner'] = defaults['user']
117 115
118 116 schema = self._get_schema(c)
119 117 c.form = RcForm(schema, appstruct=defaults)
120 118 return self._get_template_context(c)
121 119
122 120 @LoginRequired()
123 121 @HasRepoGroupPermissionAnyDecorator('group.admin')
124 122 @CSRFRequired()
125 123 def edit_settings_update(self):
126 124 _ = self.request.translate
127 125 c = self.load_default_context()
128 126 c.active = 'settings'
129 127
130 128 old_repo_group_name = self.db_repo_group_name
131 129 new_repo_group_name = old_repo_group_name
132 130
133 131 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
134 132 schema = self._get_schema(c, old_values=old_values)
135 133
136 134 c.form = RcForm(schema)
137 135 pstruct = list(self.request.POST.items())
138 136
139 137 try:
140 138 schema_data = c.form.validate(pstruct)
141 139 except deform.ValidationFailure as err_form:
142 140 return self._get_template_context(c)
143 141
144 142 # data is now VALID, proceed with updates
145 143 # save validated data back into the updates dict
146 144 validated_updates = dict(
147 145 group_name=schema_data['repo_group']['repo_group_name_without_group'],
148 146 group_parent_id=schema_data['repo_group']['repo_group_id'],
149 147 user=schema_data['repo_group_owner'],
150 148 group_description=schema_data['repo_group_description'],
151 149 enable_locking=schema_data['repo_group_enable_locking'],
152 150 )
153 151
154 152 try:
155 153 RepoGroupModel().update(self.db_repo_group, validated_updates)
156 154
157 155 audit_logger.store_web(
158 156 'repo_group.edit', action_data={'old_data': old_values},
159 157 user=c.rhodecode_user)
160 158
161 159 Session().commit()
162 160
163 161 # use the new full name for redirect once we know we updated
164 162 # the name on filesystem and in DB
165 163 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
166 164
167 165 h.flash(_('Repository Group `{}` updated successfully').format(
168 166 old_repo_group_name), category='success')
169 167
170 168 except Exception:
171 169 log.exception("Exception during update or repository group")
172 170 h.flash(_('Error occurred during update of repository group %s')
173 171 % old_repo_group_name, category='error')
174 172
175 173 name_changed = old_repo_group_name != new_repo_group_name
176 174 if name_changed:
177 175 current_perms = self.db_repo_group.permissions(expand_from_user_groups=True)
178 176 affected_user_ids = [perm['user_id'] for perm in current_perms]
179 177
180 178 # NOTE(marcink): also add owner maybe it has changed
181 179 owner = User.get_by_username(schema_data['repo_group_owner'])
182 180 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
183 181 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
184 182 PermissionModel().trigger_permission_flush(affected_user_ids)
185 183
186 184 raise HTTPFound(
187 185 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
@@ -1,1227 +1,1225 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18 from rhodecode.apps._base import add_route_with_slash
21 19
22 20
23 21 def includeme(config):
24 22 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
25 23 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
26 24 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
27 25 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
28 26 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
29 27 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
30 28 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
31 29 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
32 30 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
33 31 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
34 32 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
35 33 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
36 34 from rhodecode.apps.repository.views.repo_files import RepoFilesView
37 35 from rhodecode.apps.repository.views.repo_forks import RepoForksView
38 36 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
39 37 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
40 38 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
41 39 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
42 40 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
43 41 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
44 42 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
45 43 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
46 44 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
47 45 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
48 46 from rhodecode.apps.repository.views.repo_strip import RepoStripView
49 47 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
50 48 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
51 49
52 50 # repo creating checks, special cases that aren't repo routes
53 51 config.add_route(
54 52 name='repo_creating',
55 53 pattern='/{repo_name:.*?[^/]}/repo_creating')
56 54 config.add_view(
57 55 RepoChecksView,
58 56 attr='repo_creating',
59 57 route_name='repo_creating', request_method='GET',
60 58 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
61 59
62 60 config.add_route(
63 61 name='repo_creating_check',
64 62 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
65 63 config.add_view(
66 64 RepoChecksView,
67 65 attr='repo_creating_check',
68 66 route_name='repo_creating_check', request_method='GET',
69 67 renderer='json_ext')
70 68
71 69 # Summary
72 70 # NOTE(marcink): one additional route is defined in very bottom, catch
73 71 # all pattern
74 72 config.add_route(
75 73 name='repo_summary_explicit',
76 74 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
77 75 config.add_view(
78 76 RepoSummaryView,
79 77 attr='summary',
80 78 route_name='repo_summary_explicit', request_method='GET',
81 79 renderer='rhodecode:templates/summary/summary.mako')
82 80
83 81 config.add_route(
84 82 name='repo_summary_commits',
85 83 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
86 84 config.add_view(
87 85 RepoSummaryView,
88 86 attr='summary_commits',
89 87 route_name='repo_summary_commits', request_method='GET',
90 88 renderer='rhodecode:templates/summary/summary_commits.mako')
91 89
92 90 # Commits
93 91 config.add_route(
94 92 name='repo_commit',
95 93 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
96 94 config.add_view(
97 95 RepoCommitsView,
98 96 attr='repo_commit_show',
99 97 route_name='repo_commit', request_method='GET',
100 98 renderer=None)
101 99
102 100 config.add_route(
103 101 name='repo_commit_children',
104 102 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
105 103 config.add_view(
106 104 RepoCommitsView,
107 105 attr='repo_commit_children',
108 106 route_name='repo_commit_children', request_method='GET',
109 107 renderer='json_ext', xhr=True)
110 108
111 109 config.add_route(
112 110 name='repo_commit_parents',
113 111 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
114 112 config.add_view(
115 113 RepoCommitsView,
116 114 attr='repo_commit_parents',
117 115 route_name='repo_commit_parents', request_method='GET',
118 116 renderer='json_ext')
119 117
120 118 config.add_route(
121 119 name='repo_commit_raw',
122 120 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
123 121 config.add_view(
124 122 RepoCommitsView,
125 123 attr='repo_commit_raw',
126 124 route_name='repo_commit_raw', request_method='GET',
127 125 renderer=None)
128 126
129 127 config.add_route(
130 128 name='repo_commit_patch',
131 129 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
132 130 config.add_view(
133 131 RepoCommitsView,
134 132 attr='repo_commit_patch',
135 133 route_name='repo_commit_patch', request_method='GET',
136 134 renderer=None)
137 135
138 136 config.add_route(
139 137 name='repo_commit_download',
140 138 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
141 139 config.add_view(
142 140 RepoCommitsView,
143 141 attr='repo_commit_download',
144 142 route_name='repo_commit_download', request_method='GET',
145 143 renderer=None)
146 144
147 145 config.add_route(
148 146 name='repo_commit_data',
149 147 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
150 148 config.add_view(
151 149 RepoCommitsView,
152 150 attr='repo_commit_data',
153 151 route_name='repo_commit_data', request_method='GET',
154 152 renderer='json_ext', xhr=True)
155 153
156 154 config.add_route(
157 155 name='repo_commit_comment_create',
158 156 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
159 157 config.add_view(
160 158 RepoCommitsView,
161 159 attr='repo_commit_comment_create',
162 160 route_name='repo_commit_comment_create', request_method='POST',
163 161 renderer='json_ext')
164 162
165 163 config.add_route(
166 164 name='repo_commit_comment_preview',
167 165 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
168 166 config.add_view(
169 167 RepoCommitsView,
170 168 attr='repo_commit_comment_preview',
171 169 route_name='repo_commit_comment_preview', request_method='POST',
172 170 renderer='string', xhr=True)
173 171
174 172 config.add_route(
175 173 name='repo_commit_comment_history_view',
176 174 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
177 175 config.add_view(
178 176 RepoCommitsView,
179 177 attr='repo_commit_comment_history_view',
180 178 route_name='repo_commit_comment_history_view', request_method='POST',
181 179 renderer='string', xhr=True)
182 180
183 181 config.add_route(
184 182 name='repo_commit_comment_attachment_upload',
185 183 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
186 184 config.add_view(
187 185 RepoCommitsView,
188 186 attr='repo_commit_comment_attachment_upload',
189 187 route_name='repo_commit_comment_attachment_upload', request_method='POST',
190 188 renderer='json_ext', xhr=True)
191 189
192 190 config.add_route(
193 191 name='repo_commit_comment_delete',
194 192 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
195 193 config.add_view(
196 194 RepoCommitsView,
197 195 attr='repo_commit_comment_delete',
198 196 route_name='repo_commit_comment_delete', request_method='POST',
199 197 renderer='json_ext')
200 198
201 199 config.add_route(
202 200 name='repo_commit_comment_edit',
203 201 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
204 202 config.add_view(
205 203 RepoCommitsView,
206 204 attr='repo_commit_comment_edit',
207 205 route_name='repo_commit_comment_edit', request_method='POST',
208 206 renderer='json_ext')
209 207
210 208 # still working url for backward compat.
211 209 config.add_route(
212 210 name='repo_commit_raw_deprecated',
213 211 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
214 212 config.add_view(
215 213 RepoCommitsView,
216 214 attr='repo_commit_raw',
217 215 route_name='repo_commit_raw_deprecated', request_method='GET',
218 216 renderer=None)
219 217
220 218 # Files
221 219 config.add_route(
222 220 name='repo_archivefile',
223 221 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
224 222 config.add_view(
225 223 RepoFilesView,
226 224 attr='repo_archivefile',
227 225 route_name='repo_archivefile', request_method='GET',
228 226 renderer=None)
229 227
230 228 config.add_route(
231 229 name='repo_files_diff',
232 230 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
233 231 config.add_view(
234 232 RepoFilesView,
235 233 attr='repo_files_diff',
236 234 route_name='repo_files_diff', request_method='GET',
237 235 renderer=None)
238 236
239 237 config.add_route( # legacy route to make old links work
240 238 name='repo_files_diff_2way_redirect',
241 239 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
242 240 config.add_view(
243 241 RepoFilesView,
244 242 attr='repo_files_diff_2way_redirect',
245 243 route_name='repo_files_diff_2way_redirect', request_method='GET',
246 244 renderer=None)
247 245
248 246 config.add_route(
249 247 name='repo_files',
250 248 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
251 249 config.add_view(
252 250 RepoFilesView,
253 251 attr='repo_files',
254 252 route_name='repo_files', request_method='GET',
255 253 renderer=None)
256 254
257 255 config.add_route(
258 256 name='repo_files:default_path',
259 257 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
260 258 config.add_view(
261 259 RepoFilesView,
262 260 attr='repo_files',
263 261 route_name='repo_files:default_path', request_method='GET',
264 262 renderer=None)
265 263
266 264 config.add_route(
267 265 name='repo_files:default_commit',
268 266 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
269 267 config.add_view(
270 268 RepoFilesView,
271 269 attr='repo_files',
272 270 route_name='repo_files:default_commit', request_method='GET',
273 271 renderer=None)
274 272
275 273 config.add_route(
276 274 name='repo_files:rendered',
277 275 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
278 276 config.add_view(
279 277 RepoFilesView,
280 278 attr='repo_files',
281 279 route_name='repo_files:rendered', request_method='GET',
282 280 renderer=None)
283 281
284 282 config.add_route(
285 283 name='repo_files:annotated',
286 284 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
287 285 config.add_view(
288 286 RepoFilesView,
289 287 attr='repo_files',
290 288 route_name='repo_files:annotated', request_method='GET',
291 289 renderer=None)
292 290
293 291 config.add_route(
294 292 name='repo_files:annotated_previous',
295 293 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
296 294 config.add_view(
297 295 RepoFilesView,
298 296 attr='repo_files_annotated_previous',
299 297 route_name='repo_files:annotated_previous', request_method='GET',
300 298 renderer=None)
301 299
302 300 config.add_route(
303 301 name='repo_nodetree_full',
304 302 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
305 303 config.add_view(
306 304 RepoFilesView,
307 305 attr='repo_nodetree_full',
308 306 route_name='repo_nodetree_full', request_method='GET',
309 307 renderer=None, xhr=True)
310 308
311 309 config.add_route(
312 310 name='repo_nodetree_full:default_path',
313 311 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
314 312 config.add_view(
315 313 RepoFilesView,
316 314 attr='repo_nodetree_full',
317 315 route_name='repo_nodetree_full:default_path', request_method='GET',
318 316 renderer=None, xhr=True)
319 317
320 318 config.add_route(
321 319 name='repo_files_nodelist',
322 320 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
323 321 config.add_view(
324 322 RepoFilesView,
325 323 attr='repo_nodelist',
326 324 route_name='repo_files_nodelist', request_method='GET',
327 325 renderer='json_ext', xhr=True)
328 326
329 327 config.add_route(
330 328 name='repo_file_raw',
331 329 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
332 330 config.add_view(
333 331 RepoFilesView,
334 332 attr='repo_file_raw',
335 333 route_name='repo_file_raw', request_method='GET',
336 334 renderer=None)
337 335
338 336 config.add_route(
339 337 name='repo_file_download',
340 338 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
341 339 config.add_view(
342 340 RepoFilesView,
343 341 attr='repo_file_download',
344 342 route_name='repo_file_download', request_method='GET',
345 343 renderer=None)
346 344
347 345 config.add_route( # backward compat to keep old links working
348 346 name='repo_file_download:legacy',
349 347 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
350 348 repo_route=True)
351 349 config.add_view(
352 350 RepoFilesView,
353 351 attr='repo_file_download',
354 352 route_name='repo_file_download:legacy', request_method='GET',
355 353 renderer=None)
356 354
357 355 config.add_route(
358 356 name='repo_file_history',
359 357 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
360 358 config.add_view(
361 359 RepoFilesView,
362 360 attr='repo_file_history',
363 361 route_name='repo_file_history', request_method='GET',
364 362 renderer='json_ext')
365 363
366 364 config.add_route(
367 365 name='repo_file_authors',
368 366 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
369 367 config.add_view(
370 368 RepoFilesView,
371 369 attr='repo_file_authors',
372 370 route_name='repo_file_authors', request_method='GET',
373 371 renderer='rhodecode:templates/files/file_authors_box.mako')
374 372
375 373 config.add_route(
376 374 name='repo_files_check_head',
377 375 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
378 376 repo_route=True)
379 377 config.add_view(
380 378 RepoFilesView,
381 379 attr='repo_files_check_head',
382 380 route_name='repo_files_check_head', request_method='POST',
383 381 renderer='json_ext', xhr=True)
384 382
385 383 config.add_route(
386 384 name='repo_files_remove_file',
387 385 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
388 386 repo_route=True)
389 387 config.add_view(
390 388 RepoFilesView,
391 389 attr='repo_files_remove_file',
392 390 route_name='repo_files_remove_file', request_method='GET',
393 391 renderer='rhodecode:templates/files/files_delete.mako')
394 392
395 393 config.add_route(
396 394 name='repo_files_delete_file',
397 395 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
398 396 repo_route=True)
399 397 config.add_view(
400 398 RepoFilesView,
401 399 attr='repo_files_delete_file',
402 400 route_name='repo_files_delete_file', request_method='POST',
403 401 renderer=None)
404 402
405 403 config.add_route(
406 404 name='repo_files_edit_file',
407 405 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
408 406 repo_route=True)
409 407 config.add_view(
410 408 RepoFilesView,
411 409 attr='repo_files_edit_file',
412 410 route_name='repo_files_edit_file', request_method='GET',
413 411 renderer='rhodecode:templates/files/files_edit.mako')
414 412
415 413 config.add_route(
416 414 name='repo_files_update_file',
417 415 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
418 416 repo_route=True)
419 417 config.add_view(
420 418 RepoFilesView,
421 419 attr='repo_files_update_file',
422 420 route_name='repo_files_update_file', request_method='POST',
423 421 renderer=None)
424 422
425 423 config.add_route(
426 424 name='repo_files_add_file',
427 425 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
428 426 repo_route=True)
429 427 config.add_view(
430 428 RepoFilesView,
431 429 attr='repo_files_add_file',
432 430 route_name='repo_files_add_file', request_method='GET',
433 431 renderer='rhodecode:templates/files/files_add.mako')
434 432
435 433 config.add_route(
436 434 name='repo_files_upload_file',
437 435 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
438 436 repo_route=True)
439 437 config.add_view(
440 438 RepoFilesView,
441 439 attr='repo_files_add_file',
442 440 route_name='repo_files_upload_file', request_method='GET',
443 441 renderer='rhodecode:templates/files/files_upload.mako')
444 442 config.add_view( # POST creates
445 443 RepoFilesView,
446 444 attr='repo_files_upload_file',
447 445 route_name='repo_files_upload_file', request_method='POST',
448 446 renderer='json_ext')
449 447
450 448 config.add_route(
451 449 name='repo_files_create_file',
452 450 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
453 451 repo_route=True)
454 452 config.add_view( # POST creates
455 453 RepoFilesView,
456 454 attr='repo_files_create_file',
457 455 route_name='repo_files_create_file', request_method='POST',
458 456 renderer=None)
459 457
460 458 # Refs data
461 459 config.add_route(
462 460 name='repo_refs_data',
463 461 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
464 462 config.add_view(
465 463 RepoSummaryView,
466 464 attr='repo_refs_data',
467 465 route_name='repo_refs_data', request_method='GET',
468 466 renderer='json_ext')
469 467
470 468 config.add_route(
471 469 name='repo_refs_changelog_data',
472 470 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
473 471 config.add_view(
474 472 RepoSummaryView,
475 473 attr='repo_refs_changelog_data',
476 474 route_name='repo_refs_changelog_data', request_method='GET',
477 475 renderer='json_ext')
478 476
479 477 config.add_route(
480 478 name='repo_stats',
481 479 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
482 480 config.add_view(
483 481 RepoSummaryView,
484 482 attr='repo_stats',
485 483 route_name='repo_stats', request_method='GET',
486 484 renderer='json_ext')
487 485
488 486 # Commits
489 487 config.add_route(
490 488 name='repo_commits',
491 489 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
492 490 config.add_view(
493 491 RepoChangelogView,
494 492 attr='repo_changelog',
495 493 route_name='repo_commits', request_method='GET',
496 494 renderer='rhodecode:templates/commits/changelog.mako')
497 495 # old routes for backward compat
498 496 config.add_view(
499 497 RepoChangelogView,
500 498 attr='repo_changelog',
501 499 route_name='repo_changelog', request_method='GET',
502 500 renderer='rhodecode:templates/commits/changelog.mako')
503 501
504 502 config.add_route(
505 503 name='repo_commits_elements',
506 504 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
507 505 config.add_view(
508 506 RepoChangelogView,
509 507 attr='repo_commits_elements',
510 508 route_name='repo_commits_elements', request_method=('GET', 'POST'),
511 509 renderer='rhodecode:templates/commits/changelog_elements.mako',
512 510 xhr=True)
513 511
514 512 config.add_route(
515 513 name='repo_commits_elements_file',
516 514 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
517 515 config.add_view(
518 516 RepoChangelogView,
519 517 attr='repo_commits_elements',
520 518 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
521 519 renderer='rhodecode:templates/commits/changelog_elements.mako',
522 520 xhr=True)
523 521
524 522 config.add_route(
525 523 name='repo_commits_file',
526 524 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
527 525 config.add_view(
528 526 RepoChangelogView,
529 527 attr='repo_changelog',
530 528 route_name='repo_commits_file', request_method='GET',
531 529 renderer='rhodecode:templates/commits/changelog.mako')
532 530 # old routes for backward compat
533 531 config.add_view(
534 532 RepoChangelogView,
535 533 attr='repo_changelog',
536 534 route_name='repo_changelog_file', request_method='GET',
537 535 renderer='rhodecode:templates/commits/changelog.mako')
538 536
539 537 # Changelog (old deprecated name for commits page)
540 538 config.add_route(
541 539 name='repo_changelog',
542 540 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
543 541 config.add_route(
544 542 name='repo_changelog_file',
545 543 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
546 544
547 545 # Compare
548 546 config.add_route(
549 547 name='repo_compare_select',
550 548 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
551 549 config.add_view(
552 550 RepoCompareView,
553 551 attr='compare_select',
554 552 route_name='repo_compare_select', request_method='GET',
555 553 renderer='rhodecode:templates/compare/compare_diff.mako')
556 554
557 555 config.add_route(
558 556 name='repo_compare',
559 557 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
560 558 config.add_view(
561 559 RepoCompareView,
562 560 attr='compare',
563 561 route_name='repo_compare', request_method='GET',
564 562 renderer=None)
565 563
566 564 # Tags
567 565 config.add_route(
568 566 name='tags_home',
569 567 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
570 568 config.add_view(
571 569 RepoTagsView,
572 570 attr='tags',
573 571 route_name='tags_home', request_method='GET',
574 572 renderer='rhodecode:templates/tags/tags.mako')
575 573
576 574 # Branches
577 575 config.add_route(
578 576 name='branches_home',
579 577 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
580 578 config.add_view(
581 579 RepoBranchesView,
582 580 attr='branches',
583 581 route_name='branches_home', request_method='GET',
584 582 renderer='rhodecode:templates/branches/branches.mako')
585 583
586 584 # Bookmarks
587 585 config.add_route(
588 586 name='bookmarks_home',
589 587 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
590 588 config.add_view(
591 589 RepoBookmarksView,
592 590 attr='bookmarks',
593 591 route_name='bookmarks_home', request_method='GET',
594 592 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
595 593
596 594 # Forks
597 595 config.add_route(
598 596 name='repo_fork_new',
599 597 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
600 598 repo_forbid_when_archived=True,
601 599 repo_accepted_types=['hg', 'git'])
602 600 config.add_view(
603 601 RepoForksView,
604 602 attr='repo_fork_new',
605 603 route_name='repo_fork_new', request_method='GET',
606 604 renderer='rhodecode:templates/forks/forks.mako')
607 605
608 606 config.add_route(
609 607 name='repo_fork_create',
610 608 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
611 609 repo_forbid_when_archived=True,
612 610 repo_accepted_types=['hg', 'git'])
613 611 config.add_view(
614 612 RepoForksView,
615 613 attr='repo_fork_create',
616 614 route_name='repo_fork_create', request_method='POST',
617 615 renderer='rhodecode:templates/forks/fork.mako')
618 616
619 617 config.add_route(
620 618 name='repo_forks_show_all',
621 619 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
622 620 repo_accepted_types=['hg', 'git'])
623 621 config.add_view(
624 622 RepoForksView,
625 623 attr='repo_forks_show_all',
626 624 route_name='repo_forks_show_all', request_method='GET',
627 625 renderer='rhodecode:templates/forks/forks.mako')
628 626
629 627 config.add_route(
630 628 name='repo_forks_data',
631 629 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
632 630 repo_accepted_types=['hg', 'git'])
633 631 config.add_view(
634 632 RepoForksView,
635 633 attr='repo_forks_data',
636 634 route_name='repo_forks_data', request_method='GET',
637 635 renderer='json_ext', xhr=True)
638 636
639 637 # Pull Requests
640 638 config.add_route(
641 639 name='pullrequest_show',
642 640 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
643 641 repo_route=True)
644 642 config.add_view(
645 643 RepoPullRequestsView,
646 644 attr='pull_request_show',
647 645 route_name='pullrequest_show', request_method='GET',
648 646 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
649 647
650 648 config.add_route(
651 649 name='pullrequest_show_all',
652 650 pattern='/{repo_name:.*?[^/]}/pull-request',
653 651 repo_route=True, repo_accepted_types=['hg', 'git'])
654 652 config.add_view(
655 653 RepoPullRequestsView,
656 654 attr='pull_request_list',
657 655 route_name='pullrequest_show_all', request_method='GET',
658 656 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
659 657
660 658 config.add_route(
661 659 name='pullrequest_show_all_data',
662 660 pattern='/{repo_name:.*?[^/]}/pull-request-data',
663 661 repo_route=True, repo_accepted_types=['hg', 'git'])
664 662 config.add_view(
665 663 RepoPullRequestsView,
666 664 attr='pull_request_list_data',
667 665 route_name='pullrequest_show_all_data', request_method='GET',
668 666 renderer='json_ext', xhr=True)
669 667
670 668 config.add_route(
671 669 name='pullrequest_repo_refs',
672 670 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
673 671 repo_route=True)
674 672 config.add_view(
675 673 RepoPullRequestsView,
676 674 attr='pull_request_repo_refs',
677 675 route_name='pullrequest_repo_refs', request_method='GET',
678 676 renderer='json_ext', xhr=True)
679 677
680 678 config.add_route(
681 679 name='pullrequest_repo_targets',
682 680 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
683 681 repo_route=True)
684 682 config.add_view(
685 683 RepoPullRequestsView,
686 684 attr='pullrequest_repo_targets',
687 685 route_name='pullrequest_repo_targets', request_method='GET',
688 686 renderer='json_ext', xhr=True)
689 687
690 688 config.add_route(
691 689 name='pullrequest_new',
692 690 pattern='/{repo_name:.*?[^/]}/pull-request/new',
693 691 repo_route=True, repo_accepted_types=['hg', 'git'],
694 692 repo_forbid_when_archived=True)
695 693 config.add_view(
696 694 RepoPullRequestsView,
697 695 attr='pull_request_new',
698 696 route_name='pullrequest_new', request_method='GET',
699 697 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
700 698
701 699 config.add_route(
702 700 name='pullrequest_create',
703 701 pattern='/{repo_name:.*?[^/]}/pull-request/create',
704 702 repo_route=True, repo_accepted_types=['hg', 'git'],
705 703 repo_forbid_when_archived=True)
706 704 config.add_view(
707 705 RepoPullRequestsView,
708 706 attr='pull_request_create',
709 707 route_name='pullrequest_create', request_method='POST',
710 708 renderer=None)
711 709
712 710 config.add_route(
713 711 name='pullrequest_update',
714 712 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
715 713 repo_route=True, repo_forbid_when_archived=True)
716 714 config.add_view(
717 715 RepoPullRequestsView,
718 716 attr='pull_request_update',
719 717 route_name='pullrequest_update', request_method='POST',
720 718 renderer='json_ext')
721 719
722 720 config.add_route(
723 721 name='pullrequest_merge',
724 722 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
725 723 repo_route=True, repo_forbid_when_archived=True)
726 724 config.add_view(
727 725 RepoPullRequestsView,
728 726 attr='pull_request_merge',
729 727 route_name='pullrequest_merge', request_method='POST',
730 728 renderer='json_ext')
731 729
732 730 config.add_route(
733 731 name='pullrequest_delete',
734 732 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
735 733 repo_route=True, repo_forbid_when_archived=True)
736 734 config.add_view(
737 735 RepoPullRequestsView,
738 736 attr='pull_request_delete',
739 737 route_name='pullrequest_delete', request_method='POST',
740 738 renderer='json_ext')
741 739
742 740 config.add_route(
743 741 name='pullrequest_comment_create',
744 742 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
745 743 repo_route=True)
746 744 config.add_view(
747 745 RepoPullRequestsView,
748 746 attr='pull_request_comment_create',
749 747 route_name='pullrequest_comment_create', request_method='POST',
750 748 renderer='json_ext')
751 749
752 750 config.add_route(
753 751 name='pullrequest_comment_edit',
754 752 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
755 753 repo_route=True, repo_accepted_types=['hg', 'git'])
756 754 config.add_view(
757 755 RepoPullRequestsView,
758 756 attr='pull_request_comment_edit',
759 757 route_name='pullrequest_comment_edit', request_method='POST',
760 758 renderer='json_ext')
761 759
762 760 config.add_route(
763 761 name='pullrequest_comment_delete',
764 762 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
765 763 repo_route=True, repo_accepted_types=['hg', 'git'])
766 764 config.add_view(
767 765 RepoPullRequestsView,
768 766 attr='pull_request_comment_delete',
769 767 route_name='pullrequest_comment_delete', request_method='POST',
770 768 renderer='json_ext')
771 769
772 770 config.add_route(
773 771 name='pullrequest_comments',
774 772 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
775 773 repo_route=True)
776 774 config.add_view(
777 775 RepoPullRequestsView,
778 776 attr='pullrequest_comments',
779 777 route_name='pullrequest_comments', request_method='POST',
780 778 renderer='string_html', xhr=True)
781 779
782 780 config.add_route(
783 781 name='pullrequest_todos',
784 782 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
785 783 repo_route=True)
786 784 config.add_view(
787 785 RepoPullRequestsView,
788 786 attr='pullrequest_todos',
789 787 route_name='pullrequest_todos', request_method='POST',
790 788 renderer='string_html', xhr=True)
791 789
792 790 config.add_route(
793 791 name='pullrequest_drafts',
794 792 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
795 793 repo_route=True)
796 794 config.add_view(
797 795 RepoPullRequestsView,
798 796 attr='pullrequest_drafts',
799 797 route_name='pullrequest_drafts', request_method='POST',
800 798 renderer='string_html', xhr=True)
801 799
802 800 # Artifacts, (EE feature)
803 801 config.add_route(
804 802 name='repo_artifacts_list',
805 803 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
806 804 config.add_view(
807 805 RepoArtifactsView,
808 806 attr='repo_artifacts',
809 807 route_name='repo_artifacts_list', request_method='GET',
810 808 renderer='rhodecode:templates/artifacts/artifact_list.mako')
811 809
812 810 # Settings
813 811 config.add_route(
814 812 name='edit_repo',
815 813 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
816 814 config.add_view(
817 815 RepoSettingsView,
818 816 attr='edit_settings',
819 817 route_name='edit_repo', request_method='GET',
820 818 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
821 819 # update is POST on edit_repo
822 820 config.add_view(
823 821 RepoSettingsView,
824 822 attr='edit_settings_update',
825 823 route_name='edit_repo', request_method='POST',
826 824 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
827 825
828 826 # Settings advanced
829 827 config.add_route(
830 828 name='edit_repo_advanced',
831 829 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
832 830 config.add_view(
833 831 RepoSettingsAdvancedView,
834 832 attr='edit_advanced',
835 833 route_name='edit_repo_advanced', request_method='GET',
836 834 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
837 835
838 836 config.add_route(
839 837 name='edit_repo_advanced_archive',
840 838 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
841 839 config.add_view(
842 840 RepoSettingsAdvancedView,
843 841 attr='edit_advanced_archive',
844 842 route_name='edit_repo_advanced_archive', request_method='POST',
845 843 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
846 844
847 845 config.add_route(
848 846 name='edit_repo_advanced_delete',
849 847 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
850 848 config.add_view(
851 849 RepoSettingsAdvancedView,
852 850 attr='edit_advanced_delete',
853 851 route_name='edit_repo_advanced_delete', request_method='POST',
854 852 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
855 853
856 854 config.add_route(
857 855 name='edit_repo_advanced_locking',
858 856 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
859 857 config.add_view(
860 858 RepoSettingsAdvancedView,
861 859 attr='edit_advanced_toggle_locking',
862 860 route_name='edit_repo_advanced_locking', request_method='POST',
863 861 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
864 862
865 863 config.add_route(
866 864 name='edit_repo_advanced_journal',
867 865 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
868 866 config.add_view(
869 867 RepoSettingsAdvancedView,
870 868 attr='edit_advanced_journal',
871 869 route_name='edit_repo_advanced_journal', request_method='POST',
872 870 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
873 871
874 872 config.add_route(
875 873 name='edit_repo_advanced_fork',
876 874 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
877 875 config.add_view(
878 876 RepoSettingsAdvancedView,
879 877 attr='edit_advanced_fork',
880 878 route_name='edit_repo_advanced_fork', request_method='POST',
881 879 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
882 880
883 881 config.add_route(
884 882 name='edit_repo_advanced_hooks',
885 883 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
886 884 config.add_view(
887 885 RepoSettingsAdvancedView,
888 886 attr='edit_advanced_install_hooks',
889 887 route_name='edit_repo_advanced_hooks', request_method='GET',
890 888 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
891 889
892 890 # Caches
893 891 config.add_route(
894 892 name='edit_repo_caches',
895 893 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
896 894 config.add_view(
897 895 RepoCachesView,
898 896 attr='repo_caches',
899 897 route_name='edit_repo_caches', request_method='GET',
900 898 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
901 899 config.add_view(
902 900 RepoCachesView,
903 901 attr='repo_caches_purge',
904 902 route_name='edit_repo_caches', request_method='POST')
905 903
906 904 # Permissions
907 905 config.add_route(
908 906 name='edit_repo_perms',
909 907 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
910 908 config.add_view(
911 909 RepoSettingsPermissionsView,
912 910 attr='edit_permissions',
913 911 route_name='edit_repo_perms', request_method='GET',
914 912 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
915 913 config.add_view(
916 914 RepoSettingsPermissionsView,
917 915 attr='edit_permissions_update',
918 916 route_name='edit_repo_perms', request_method='POST',
919 917 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
920 918
921 919 config.add_route(
922 920 name='edit_repo_perms_set_private',
923 921 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
924 922 config.add_view(
925 923 RepoSettingsPermissionsView,
926 924 attr='edit_permissions_set_private_repo',
927 925 route_name='edit_repo_perms_set_private', request_method='POST',
928 926 renderer='json_ext')
929 927
930 928 # Permissions Branch (EE feature)
931 929 config.add_route(
932 930 name='edit_repo_perms_branch',
933 931 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
934 932 config.add_view(
935 933 RepoSettingsBranchPermissionsView,
936 934 attr='branch_permissions',
937 935 route_name='edit_repo_perms_branch', request_method='GET',
938 936 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
939 937
940 938 config.add_route(
941 939 name='edit_repo_perms_branch_delete',
942 940 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
943 941 repo_route=True)
944 942 ## Only implemented in EE
945 943
946 944 # Maintenance
947 945 config.add_route(
948 946 name='edit_repo_maintenance',
949 947 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
950 948 config.add_view(
951 949 RepoMaintenanceView,
952 950 attr='repo_maintenance',
953 951 route_name='edit_repo_maintenance', request_method='GET',
954 952 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
955 953
956 954 config.add_route(
957 955 name='edit_repo_maintenance_execute',
958 956 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
959 957 config.add_view(
960 958 RepoMaintenanceView,
961 959 attr='repo_maintenance_execute',
962 960 route_name='edit_repo_maintenance_execute', request_method='GET',
963 961 renderer='json', xhr=True)
964 962
965 963 # Fields
966 964 config.add_route(
967 965 name='edit_repo_fields',
968 966 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
969 967 config.add_view(
970 968 RepoSettingsFieldsView,
971 969 attr='repo_field_edit',
972 970 route_name='edit_repo_fields', request_method='GET',
973 971 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
974 972
975 973 config.add_route(
976 974 name='edit_repo_fields_create',
977 975 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
978 976 config.add_view(
979 977 RepoSettingsFieldsView,
980 978 attr='repo_field_create',
981 979 route_name='edit_repo_fields_create', request_method='POST',
982 980 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
983 981
984 982 config.add_route(
985 983 name='edit_repo_fields_delete',
986 984 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
987 985 config.add_view(
988 986 RepoSettingsFieldsView,
989 987 attr='repo_field_delete',
990 988 route_name='edit_repo_fields_delete', request_method='POST',
991 989 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
992 990
993 991 # quick actions: locking
994 992 config.add_route(
995 993 name='repo_settings_quick_actions',
996 994 pattern='/{repo_name:.*?[^/]}/settings/quick-action', repo_route=True)
997 995 config.add_view(
998 996 RepoSettingsView,
999 997 attr='repo_settings_quick_actions',
1000 998 route_name='repo_settings_quick_actions', request_method='GET',
1001 999 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1002 1000
1003 1001 # Remote
1004 1002 config.add_route(
1005 1003 name='edit_repo_remote',
1006 1004 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1007 1005 config.add_view(
1008 1006 RepoSettingsRemoteView,
1009 1007 attr='repo_remote_edit_form',
1010 1008 route_name='edit_repo_remote', request_method='GET',
1011 1009 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1012 1010
1013 1011 config.add_route(
1014 1012 name='edit_repo_remote_pull',
1015 1013 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1016 1014 config.add_view(
1017 1015 RepoSettingsRemoteView,
1018 1016 attr='repo_remote_pull_changes',
1019 1017 route_name='edit_repo_remote_pull', request_method='POST',
1020 1018 renderer=None)
1021 1019
1022 1020 config.add_route(
1023 1021 name='edit_repo_remote_push',
1024 1022 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1025 1023
1026 1024 # Statistics
1027 1025 config.add_route(
1028 1026 name='edit_repo_statistics',
1029 1027 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1030 1028 config.add_view(
1031 1029 RepoSettingsView,
1032 1030 attr='edit_statistics_form',
1033 1031 route_name='edit_repo_statistics', request_method='GET',
1034 1032 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1035 1033
1036 1034 config.add_route(
1037 1035 name='edit_repo_statistics_reset',
1038 1036 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1039 1037 config.add_view(
1040 1038 RepoSettingsView,
1041 1039 attr='repo_statistics_reset',
1042 1040 route_name='edit_repo_statistics_reset', request_method='POST',
1043 1041 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1044 1042
1045 1043 # Issue trackers
1046 1044 config.add_route(
1047 1045 name='edit_repo_issuetracker',
1048 1046 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1049 1047 config.add_view(
1050 1048 RepoSettingsIssueTrackersView,
1051 1049 attr='repo_issuetracker',
1052 1050 route_name='edit_repo_issuetracker', request_method='GET',
1053 1051 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1054 1052
1055 1053 config.add_route(
1056 1054 name='edit_repo_issuetracker_test',
1057 1055 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1058 1056 config.add_view(
1059 1057 RepoSettingsIssueTrackersView,
1060 1058 attr='repo_issuetracker_test',
1061 1059 route_name='edit_repo_issuetracker_test', request_method='POST',
1062 1060 renderer='string', xhr=True)
1063 1061
1064 1062 config.add_route(
1065 1063 name='edit_repo_issuetracker_delete',
1066 1064 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1067 1065 config.add_view(
1068 1066 RepoSettingsIssueTrackersView,
1069 1067 attr='repo_issuetracker_delete',
1070 1068 route_name='edit_repo_issuetracker_delete', request_method='POST',
1071 1069 renderer='json_ext', xhr=True)
1072 1070
1073 1071 config.add_route(
1074 1072 name='edit_repo_issuetracker_update',
1075 1073 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1076 1074 config.add_view(
1077 1075 RepoSettingsIssueTrackersView,
1078 1076 attr='repo_issuetracker_update',
1079 1077 route_name='edit_repo_issuetracker_update', request_method='POST',
1080 1078 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1081 1079
1082 1080 # VCS Settings
1083 1081 config.add_route(
1084 1082 name='edit_repo_vcs',
1085 1083 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1086 1084 config.add_view(
1087 1085 RepoSettingsVcsView,
1088 1086 attr='repo_vcs_settings',
1089 1087 route_name='edit_repo_vcs', request_method='GET',
1090 1088 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1091 1089
1092 1090 config.add_route(
1093 1091 name='edit_repo_vcs_update',
1094 1092 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1095 1093 config.add_view(
1096 1094 RepoSettingsVcsView,
1097 1095 attr='repo_settings_vcs_update',
1098 1096 route_name='edit_repo_vcs_update', request_method='POST',
1099 1097 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1100 1098
1101 1099 # svn pattern
1102 1100 config.add_route(
1103 1101 name='edit_repo_vcs_svn_pattern_delete',
1104 1102 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1105 1103 config.add_view(
1106 1104 RepoSettingsVcsView,
1107 1105 attr='repo_settings_delete_svn_pattern',
1108 1106 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1109 1107 renderer='json_ext', xhr=True)
1110 1108
1111 1109 # Repo Review Rules (EE feature)
1112 1110 config.add_route(
1113 1111 name='repo_reviewers',
1114 1112 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1115 1113 config.add_view(
1116 1114 RepoReviewRulesView,
1117 1115 attr='repo_review_rules',
1118 1116 route_name='repo_reviewers', request_method='GET',
1119 1117 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1120 1118
1121 1119 config.add_route(
1122 1120 name='repo_default_reviewers_data',
1123 1121 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1124 1122 config.add_view(
1125 1123 RepoReviewRulesView,
1126 1124 attr='repo_default_reviewers_data',
1127 1125 route_name='repo_default_reviewers_data', request_method='GET',
1128 1126 renderer='json_ext')
1129 1127
1130 1128 # Repo Automation (EE feature)
1131 1129 config.add_route(
1132 1130 name='repo_automation',
1133 1131 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1134 1132 config.add_view(
1135 1133 RepoAutomationView,
1136 1134 attr='repo_automation',
1137 1135 route_name='repo_automation', request_method='GET',
1138 1136 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1139 1137
1140 1138 # Strip
1141 1139 config.add_route(
1142 1140 name='edit_repo_strip',
1143 1141 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1144 1142 config.add_view(
1145 1143 RepoStripView,
1146 1144 attr='strip',
1147 1145 route_name='edit_repo_strip', request_method='GET',
1148 1146 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1149 1147
1150 1148 config.add_route(
1151 1149 name='strip_check',
1152 1150 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1153 1151 config.add_view(
1154 1152 RepoStripView,
1155 1153 attr='strip_check',
1156 1154 route_name='strip_check', request_method='POST',
1157 1155 renderer='json', xhr=True)
1158 1156
1159 1157 config.add_route(
1160 1158 name='strip_execute',
1161 1159 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1162 1160 config.add_view(
1163 1161 RepoStripView,
1164 1162 attr='strip_execute',
1165 1163 route_name='strip_execute', request_method='POST',
1166 1164 renderer='json', xhr=True)
1167 1165
1168 1166 # Audit logs
1169 1167 config.add_route(
1170 1168 name='edit_repo_audit_logs',
1171 1169 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1172 1170 config.add_view(
1173 1171 AuditLogsView,
1174 1172 attr='repo_audit_logs',
1175 1173 route_name='edit_repo_audit_logs', request_method='GET',
1176 1174 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1177 1175
1178 1176 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1179 1177 config.add_route(
1180 1178 name='rss_feed_home',
1181 1179 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1182 1180 config.add_view(
1183 1181 RepoFeedView,
1184 1182 attr='rss',
1185 1183 route_name='rss_feed_home', request_method='GET', renderer=None)
1186 1184
1187 1185 config.add_route(
1188 1186 name='rss_feed_home_old',
1189 1187 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1190 1188 config.add_view(
1191 1189 RepoFeedView,
1192 1190 attr='rss',
1193 1191 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1194 1192
1195 1193 config.add_route(
1196 1194 name='atom_feed_home',
1197 1195 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1198 1196 config.add_view(
1199 1197 RepoFeedView,
1200 1198 attr='atom',
1201 1199 route_name='atom_feed_home', request_method='GET', renderer=None)
1202 1200
1203 1201 config.add_route(
1204 1202 name='atom_feed_home_old',
1205 1203 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1206 1204 config.add_view(
1207 1205 RepoFeedView,
1208 1206 attr='atom',
1209 1207 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1210 1208
1211 1209 # NOTE(marcink): needs to be at the end for catch-all
1212 1210 add_route_with_slash(
1213 1211 config,
1214 1212 name='repo_summary',
1215 1213 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1216 1214 config.add_view(
1217 1215 RepoSummaryView,
1218 1216 attr='summary',
1219 1217 route_name='repo_summary', request_method='GET',
1220 1218 renderer='rhodecode:templates/summary/summary.mako')
1221 1219
1222 1220 # TODO(marcink): there's no such route??
1223 1221 config.add_view(
1224 1222 RepoSummaryView,
1225 1223 attr='summary',
1226 1224 route_name='repo_summary_slash', request_method='GET',
1227 1225 renderer='rhodecode:templates/summary/summary.mako') No newline at end of file
@@ -1,18 +1,17 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,113 +1,111 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 from rhodecode.lib import helpers as h, rc_cache
22 20 from rhodecode.lib.utils2 import safe_int
23 21 from rhodecode.model.pull_request import get_diff_info
24 22 from rhodecode.model.db import PullRequestReviewers
25 23 # V3 - Reviewers, with default rules data
26 24 # v4 - Added observers metadata
27 25 # v5 - pr_author/commit_author include/exclude logic
28 26 REVIEWER_API_VERSION = 'V5'
29 27
30 28
31 29 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
32 30 """
33 31 Returns json struct of a reviewer for frontend
34 32
35 33 :param user: the reviewer
36 34 :param reasons: list of strings of why they are reviewers
37 35 :param mandatory: bool, to set user as mandatory
38 36 """
39 37 role = role or PullRequestReviewers.ROLE_REVIEWER
40 38 if role not in PullRequestReviewers.ROLES:
41 39 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
42 40
43 41 return {
44 42 'user_id': user.user_id,
45 43 'reasons': reasons or [],
46 44 'rules': rules or [],
47 45 'role': role,
48 46 'mandatory': mandatory,
49 47 'user_group': user_group,
50 48 'username': user.username,
51 49 'first_name': user.first_name,
52 50 'last_name': user.last_name,
53 51 'user_link': h.link_to_user(user),
54 52 'gravatar_link': h.gravatar_url(user.email, 14),
55 53 }
56 54
57 55
58 56 def to_reviewers(e):
59 57 if isinstance(e, (tuple, list)):
60 58 return map(reviewer_as_json, e)
61 59 else:
62 60 return reviewer_as_json(e)
63 61
64 62
65 63 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
66 64 include_diff_info=True):
67 65 """
68 66 Return json for default reviewers of a repository
69 67 """
70 68
71 69 diff_info = {}
72 70 if include_diff_info:
73 71 diff_info = get_diff_info(
74 72 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
75 73
76 74 reasons = ['Default reviewer', 'Repository owner']
77 75 json_reviewers = [reviewer_as_json(
78 76 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
79 77
80 78 compute_key = rc_cache.utils.compute_key_from_params(
81 79 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
82 80 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
83 81 target_ref.commit_id)
84 82
85 83 return {
86 84 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
87 85 'compute_key': compute_key,
88 86 'diff_info': diff_info,
89 87 'reviewers': json_reviewers,
90 88 'rules': {},
91 89 'rules_data': {},
92 90 'rules_humanized': [],
93 91 }
94 92
95 93
96 94 def validate_default_reviewers(review_members, reviewer_rules):
97 95 """
98 96 Function to validate submitted reviewers against the saved rules
99 97 """
100 98 reviewers = []
101 99 reviewer_by_id = {}
102 100 for r in review_members:
103 101 reviewer_user_id = safe_int(r['user_id'])
104 102 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
105 103
106 104 reviewer_by_id[reviewer_user_id] = entry
107 105 reviewers.append(entry)
108 106
109 107 return reviewers
110 108
111 109
112 110 def validate_observers(observer_members, reviewer_rules):
113 111 return {}
@@ -1,19 +1,17 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,45 +1,43 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22
25 23 from rhodecode.apps._base import RepoAppView
26 24 from rhodecode.lib.auth import (
27 25 LoginRequired, HasRepoPermissionAnyDecorator)
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 class RepoArtifactsView(RepoAppView):
33 31
34 32 def load_default_context(self):
35 33 c = self._get_local_tmpl_context(include_app_defaults=True)
36 34 c.rhodecode_repo = self.rhodecode_vcs_repo
37 35 return c
38 36
39 37 @LoginRequired()
40 38 @HasRepoPermissionAnyDecorator(
41 39 'repository.read', 'repository.write', 'repository.admin')
42 40 def repo_artifacts(self):
43 41 c = self.load_default_context()
44 42 c.active = 'artifacts'
45 43 return self._get_template_context(c)
@@ -1,64 +1,62 b''
1
2
3 1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22 from rhodecode.apps._base import RepoAppView
25 23 from rhodecode.lib.helpers import SqlPage
26 24 from rhodecode.lib import helpers as h
27 25 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 26 from rhodecode.lib.utils2 import safe_int
29 27 from rhodecode.model.repo import RepoModel
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class AuditLogsView(RepoAppView):
35 33 def load_default_context(self):
36 34 c = self._get_local_tmpl_context()
37 35 return c
38 36
39 37 @LoginRequired()
40 38 @HasRepoPermissionAnyDecorator('repository.admin')
41 39 def repo_audit_logs(self):
42 40 _ = self.request.translate
43 41 c = self.load_default_context()
44 42 c.db_repo = self.db_repo
45 43
46 44 c.active = 'audit'
47 45
48 46 p = safe_int(self.request.GET.get('page', 1), 1)
49 47
50 48 filter_term = self.request.GET.get('filter')
51 49 user_log = RepoModel().get_repo_log(c.db_repo, filter_term)
52 50
53 51 def url_generator(page_num):
54 52 query_params = {
55 53 'page': page_num
56 54 }
57 55 if filter_term:
58 56 query_params['filter'] = filter_term
59 57 return self.request.current_route_path(_query=query_params)
60 58
61 59 c.audit_logs = SqlPage(
62 60 user_log, page=p, items_per_page=10, url_maker=url_generator)
63 61 c.filter_term = filter_term
64 62 return self._get_template_context(c)
@@ -1,43 +1,41 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22
25 23 from rhodecode.apps._base import RepoAppView
26 24 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 25 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 class RepoAutomationView(RepoAppView):
33 31 def load_default_context(self):
34 32 c = self._get_local_tmpl_context()
35 33 return c
36 34
37 35 @LoginRequired()
38 36 @HasRepoPermissionAnyDecorator('repository.admin')
39 37 def repo_automation(self):
40 38 c = self.load_default_context()
41 39 c.active = 'automation'
42 40
43 41 return self._get_template_context(c)
@@ -1,53 +1,51 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18 import logging
21 19
22 20 from pyramid.httpexceptions import HTTPNotFound
23 21
24 22 from rhodecode.apps._base import BaseReferencesView
25 23 from rhodecode.lib import ext_json
26 24 from rhodecode.lib import helpers as h
27 25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 26 from rhodecode.model.scm import ScmModel
29 27
30 28 log = logging.getLogger(__name__)
31 29
32 30
33 31 class RepoBookmarksView(BaseReferencesView):
34 32
35 33 @LoginRequired()
36 34 @HasRepoPermissionAnyDecorator(
37 35 'repository.read', 'repository.write', 'repository.admin')
38 36 def bookmarks(self):
39 37 c = self.load_default_context()
40 38 self._prepare_and_set_clone_url(c)
41 39 c.rhodecode_repo = self.rhodecode_vcs_repo
42 40 c.repository_forks = ScmModel().get_forks(self.db_repo)
43 41
44 42 if not h.is_hg(self.db_repo):
45 43 raise HTTPNotFound()
46 44
47 45 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
48 46 data = self.load_refs_context(
49 47 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
50 48
51 49 c.has_references = bool(data)
52 50 c.data = ext_json.str_json(data)
53 51 return self._get_template_context(c)
@@ -1,42 +1,40 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22
25 23 from rhodecode.apps._base import RepoAppView
26 24 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 25
28 26 log = logging.getLogger(__name__)
29 27
30 28
31 29 class RepoSettingsBranchPermissionsView(RepoAppView):
32 30
33 31 def load_default_context(self):
34 32 c = self._get_local_tmpl_context()
35 33 return c
36 34
37 35 @LoginRequired()
38 36 @HasRepoPermissionAnyDecorator('repository.admin')
39 37 def branch_permissions(self):
40 38 c = self.load_default_context()
41 39 c.active = 'permissions_branch'
42 40 return self._get_template_context(c)
@@ -1,49 +1,47 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22 from rhodecode.apps._base import BaseReferencesView
25 23 from rhodecode.lib import ext_json
26 24 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 25 from rhodecode.model.scm import ScmModel
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 class RepoBranchesView(BaseReferencesView):
33 31
34 32 @LoginRequired()
35 33 @HasRepoPermissionAnyDecorator(
36 34 'repository.read', 'repository.write', 'repository.admin')
37 35 def branches(self):
38 36 c = self.load_default_context()
39 37 self._prepare_and_set_clone_url(c)
40 38 c.rhodecode_repo = self.rhodecode_vcs_repo
41 39 c.repository_forks = ScmModel().get_forks(self.db_repo)
42 40
43 41 ref_items = self.rhodecode_vcs_repo.branches_all.items()
44 42 data = self.load_refs_context(
45 43 ref_items=ref_items, partials_template='branches/branches_data.mako')
46 44
47 45 c.has_references = bool(data)
48 46 c.data = ext_json.str_json(data)
49 47 return self._get_template_context(c)
@@ -1,93 +1,91 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import os
22 20 import logging
23 21
24 22 from pyramid.httpexceptions import HTTPFound
25 23
26 24
27 25 from rhodecode.apps._base import RepoAppView
28 26 from rhodecode.lib.auth import (
29 27 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 28 from rhodecode.lib import helpers as h, rc_cache
31 29 from rhodecode.lib import system_info
32 30 from rhodecode.model.meta import Session
33 31 from rhodecode.model.scm import ScmModel
34 32
35 33 log = logging.getLogger(__name__)
36 34
37 35
38 36 class RepoCachesView(RepoAppView):
39 37 def load_default_context(self):
40 38 c = self._get_local_tmpl_context()
41 39 return c
42 40
43 41 @LoginRequired()
44 42 @HasRepoPermissionAnyDecorator('repository.admin')
45 43 def repo_caches(self):
46 44 c = self.load_default_context()
47 45 c.active = 'caches'
48 46 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
49 47 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
50 48 c.cached_diff_size = 0
51 49 if os.path.isdir(cached_diffs_dir):
52 50 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
53 51 c.shadow_repos = c.rhodecode_db_repo.shadow_repos()
54 52
55 cache_namespace_uid = 'repo.{}'.format(self.db_repo.repo_id)
53 cache_namespace_uid = f'repo.{self.db_repo.repo_id}'
56 54 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
57 55 c.backend = c.region.backend
58 56 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
59 57
60 58 return self._get_template_context(c)
61 59
62 60 @LoginRequired()
63 61 @HasRepoPermissionAnyDecorator('repository.admin')
64 62 @CSRFRequired()
65 63 def repo_caches_purge(self):
66 64 _ = self.request.translate
67 65 c = self.load_default_context()
68 66 c.active = 'caches'
69 67 invalidated = 0
70 68
71 69 try:
72 70 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
73 71 Session().commit()
74 72 invalidated +=1
75 73 except Exception:
76 74 log.exception("Exception during cache invalidation")
77 75 h.flash(_('An error occurred during cache invalidation'),
78 76 category='error')
79 77
80 78 try:
81 79 invalidated += 1
82 80 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
83 81 except Exception:
84 82 log.exception("Exception during vcsserver cache invalidation")
85 83 h.flash(_('An error occurred during vcsserver cache invalidation'),
86 84 category='error')
87 85
88 86 if invalidated:
89 87 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
90 88 category='success')
91 89
92 90 raise HTTPFound(h.route_path(
93 91 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
@@ -1,356 +1,355 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19
21 20 import logging
22 21
23 22 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24 23
25 24 from pyramid.renderers import render
26 25 from pyramid.response import Response
27 26
28 27 from rhodecode.apps._base import RepoAppView
29 28 import rhodecode.lib.helpers as h
30 29 from rhodecode.lib import ext_json
31 30 from rhodecode.lib.auth import (
32 31 LoginRequired, HasRepoPermissionAnyDecorator)
33 32
34 33 from rhodecode.lib.graphmod import _colored, _dagwalker
35 34 from rhodecode.lib.helpers import RepoPage
36 35 from rhodecode.lib.utils2 import str2bool
37 36 from rhodecode.lib.str_utils import safe_int, safe_str
38 37 from rhodecode.lib.vcs.exceptions import (
39 38 RepositoryError, CommitDoesNotExistError,
40 39 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 40
42 41 log = logging.getLogger(__name__)
43 42
44 43 DEFAULT_CHANGELOG_SIZE = 20
45 44
46 45
47 46 class RepoChangelogView(RepoAppView):
48 47
49 48 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 49 """
51 50 This is a safe way to get commit. If an error occurs it redirects to
52 51 tip with proper message
53 52
54 53 :param commit_id: id of commit to fetch
55 54 :param redirect_after: toggle redirection
56 55 """
57 56 _ = self.request.translate
58 57
59 58 try:
60 59 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 60 except EmptyRepositoryError:
62 61 if not redirect_after:
63 62 return None
64 63
65 64 h.flash(h.literal(
66 65 _('There are no commits yet')), category='warning')
67 66 raise HTTPFound(
68 67 h.route_path('repo_summary', repo_name=self.db_repo_name))
69 68
70 69 except (CommitDoesNotExistError, LookupError):
71 70 msg = _('No such commit exists for this repository')
72 71 h.flash(msg, category='error')
73 72 raise HTTPNotFound()
74 73 except RepositoryError as e:
75 74 h.flash(h.escape(safe_str(e)), category='error')
76 75 raise HTTPNotFound()
77 76
78 77 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 78 """
80 79 Generates a DAG graph for repo
81 80
82 81 :param repo: repo instance
83 82 :param commits: list of commits
84 83 """
85 84 if not commits:
86 85 return ext_json.str_json([]), ext_json.str_json([])
87 86
88 87 def serialize(commit, parents=True):
89 88 data = dict(
90 89 raw_id=commit.raw_id,
91 90 idx=commit.idx,
92 91 branch=None,
93 92 )
94 93 if parents:
95 94 data['parents'] = [
96 95 serialize(x, parents=False) for x in commit.parents]
97 96 return data
98 97
99 98 prev_data = prev_data or []
100 99 next_data = next_data or []
101 100
102 101 current = [serialize(x) for x in commits]
103 102
104 103 commits = prev_data + current + next_data
105 104
106 105 dag = _dagwalker(repo, commits)
107 106
108 107 data = [[commit_id, vtx, edges, branch]
109 108 for commit_id, vtx, edges, branch in _colored(dag)]
110 109 return ext_json.str_json(data), ext_json.str_json(current)
111 110
112 111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
113 112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
114 h.flash(u'Branch {} is not found.'.format(h.escape(safe_str(branch_name))),
113 h.flash(f'Branch {h.escape(safe_str(branch_name))} is not found.',
115 114 category='warning')
116 115 redirect_url = h.route_path(
117 116 'repo_commits_file', repo_name=repo_name,
118 117 commit_id=branch_name, f_path=f_path or '')
119 118 raise HTTPFound(redirect_url)
120 119
121 120 def _load_changelog_data(
122 121 self, c, collection, page, chunk_size, branch_name=None,
123 122 dynamic=False, f_path=None, commit_id=None):
124 123
125 124 def url_generator(page_num):
126 125 query_params = {
127 126 'page': page_num
128 127 }
129 128
130 129 if branch_name:
131 130 query_params.update({
132 131 'branch': branch_name
133 132 })
134 133
135 134 if f_path:
136 135 # changelog for file
137 136 return h.route_path(
138 137 'repo_commits_file',
139 138 repo_name=c.rhodecode_db_repo.repo_name,
140 139 commit_id=commit_id, f_path=f_path,
141 140 _query=query_params)
142 141 else:
143 142 return h.route_path(
144 143 'repo_commits',
145 144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
146 145
147 146 c.total_cs = len(collection)
148 147 c.showing_commits = min(chunk_size, c.total_cs)
149 148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
150 149 items_per_page=chunk_size, url_maker=url_generator)
151 150
152 151 c.next_page = c.pagination.next_page
153 152 c.prev_page = c.pagination.previous_page
154 153
155 154 if dynamic:
156 155 if self.request.GET.get('chunk') != 'next':
157 156 c.next_page = None
158 157 if self.request.GET.get('chunk') != 'prev':
159 158 c.prev_page = None
160 159
161 160 page_commit_ids = [x.raw_id for x in c.pagination]
162 161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
163 162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
164 163
165 164 def load_default_context(self):
166 165 c = self._get_local_tmpl_context(include_app_defaults=True)
167 166
168 167 c.rhodecode_repo = self.rhodecode_vcs_repo
169 168
170 169 return c
171 170
172 171 @LoginRequired()
173 172 @HasRepoPermissionAnyDecorator(
174 173 'repository.read', 'repository.write', 'repository.admin')
175 174 def repo_changelog(self):
176 175 c = self.load_default_context()
177 176
178 177 commit_id = self.request.matchdict.get('commit_id')
179 178 f_path = self._get_f_path(self.request.matchdict)
180 179 show_hidden = str2bool(self.request.GET.get('evolve'))
181 180
182 181 chunk_size = 20
183 182
184 183 c.branch_name = branch_name = self.request.GET.get('branch') or ''
185 184 c.book_name = book_name = self.request.GET.get('bookmark') or ''
186 185 c.f_path = f_path
187 186 c.commit_id = commit_id
188 187 c.show_hidden = show_hidden
189 188
190 189 hist_limit = safe_int(self.request.GET.get('limit')) or None
191 190
192 191 p = safe_int(self.request.GET.get('page', 1), 1)
193 192
194 193 c.selected_name = branch_name or book_name
195 194 if not commit_id and branch_name:
196 195 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
197 196
198 197 c.changelog_for_path = f_path
199 198 pre_load = self.get_commit_preload_attrs()
200 199
201 200 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
202 201
203 202 try:
204 203 if f_path:
205 204 log.debug('generating changelog for path %s', f_path)
206 205 # get the history for the file !
207 206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
208 207
209 208 try:
210 209 collection = base_commit.get_path_history(
211 210 f_path, limit=hist_limit, pre_load=pre_load)
212 211 if collection and partial_xhr:
213 212 # for ajax call we remove first one since we're looking
214 213 # at it right now in the context of a file commit
215 214 collection.pop(0)
216 215 except (NodeDoesNotExistError, CommitError):
217 216 # this node is not present at tip!
218 217 try:
219 218 commit = self._get_commit_or_redirect(commit_id)
220 219 collection = commit.get_path_history(f_path)
221 220 except RepositoryError as e:
222 221 h.flash(safe_str(e), category='warning')
223 222 redirect_url = h.route_path(
224 223 'repo_commits', repo_name=self.db_repo_name)
225 224 raise HTTPFound(redirect_url)
226 225 collection = list(reversed(collection))
227 226 else:
228 227 collection = self.rhodecode_vcs_repo.get_commits(
229 228 branch_name=branch_name, show_hidden=show_hidden,
230 229 pre_load=pre_load, translate_tags=False)
231 230
232 231 self._load_changelog_data(
233 232 c, collection, p, chunk_size, c.branch_name,
234 233 f_path=f_path, commit_id=commit_id)
235 234
236 235 except EmptyRepositoryError as e:
237 236 h.flash(h.escape(safe_str(e)), category='warning')
238 237 raise HTTPFound(
239 238 h.route_path('repo_summary', repo_name=self.db_repo_name))
240 239 except HTTPFound:
241 240 raise
242 241 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
243 242 log.exception(safe_str(e))
244 243 h.flash(h.escape(safe_str(e)), category='error')
245 244
246 245 if commit_id:
247 246 # from single commit page, we redirect to main commits
248 247 raise HTTPFound(
249 248 h.route_path('repo_commits', repo_name=self.db_repo_name))
250 249 else:
251 250 # otherwise we redirect to summary
252 251 raise HTTPFound(
253 252 h.route_path('repo_summary', repo_name=self.db_repo_name))
254 253
255 254
256 255
257 256 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
258 257 # case when loading dynamic file history in file view
259 258 # loading from ajax, we don't want the first result, it's popped
260 259 # in the code above
261 260 html = render(
262 261 'rhodecode:templates/commits/changelog_file_history.mako',
263 262 self._get_template_context(c), self.request)
264 263 return Response(html)
265 264
266 265 commit_ids = []
267 266 if not f_path:
268 267 # only load graph data when not in file history mode
269 268 commit_ids = c.pagination
270 269
271 270 c.graph_data, c.graph_commits = self._graph(
272 271 self.rhodecode_vcs_repo, commit_ids)
273 272
274 273 return self._get_template_context(c)
275 274
276 275 @LoginRequired()
277 276 @HasRepoPermissionAnyDecorator(
278 277 'repository.read', 'repository.write', 'repository.admin')
279 278 def repo_commits_elements(self):
280 279 c = self.load_default_context()
281 280 commit_id = self.request.matchdict.get('commit_id')
282 281 f_path = self._get_f_path(self.request.matchdict)
283 282 show_hidden = str2bool(self.request.GET.get('evolve'))
284 283
285 284 chunk_size = 20
286 285 hist_limit = safe_int(self.request.GET.get('limit')) or None
287 286
288 287 def wrap_for_error(err):
289 288 html = '<tr>' \
290 289 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
291 290 '</tr>'.format(err)
292 291 return Response(html)
293 292
294 293 c.branch_name = branch_name = self.request.GET.get('branch') or ''
295 294 c.book_name = book_name = self.request.GET.get('bookmark') or ''
296 295 c.f_path = f_path
297 296 c.commit_id = commit_id
298 297 c.show_hidden = show_hidden
299 298
300 299 c.selected_name = branch_name or book_name
301 300 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
302 301 return wrap_for_error(
303 safe_str('Branch: {} is not valid'.format(branch_name)))
302 safe_str(f'Branch: {branch_name} is not valid'))
304 303
305 304 pre_load = self.get_commit_preload_attrs()
306 305
307 306 if f_path:
308 307 try:
309 308 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
310 309 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
311 310 log.exception(safe_str(e))
312 311 raise HTTPFound(
313 312 h.route_path('repo_commits', repo_name=self.db_repo_name))
314 313
315 314 collection = base_commit.get_path_history(
316 315 f_path, limit=hist_limit, pre_load=pre_load)
317 316 collection = list(reversed(collection))
318 317 else:
319 318 collection = self.rhodecode_vcs_repo.get_commits(
320 319 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
321 320 translate_tags=False)
322 321
323 322 p = safe_int(self.request.GET.get('page', 1), 1)
324 323 try:
325 324 self._load_changelog_data(
326 325 c, collection, p, chunk_size, dynamic=True,
327 326 f_path=f_path, commit_id=commit_id)
328 327 except EmptyRepositoryError as e:
329 328 return wrap_for_error(safe_str(e))
330 329 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
331 330 log.exception('Failed to fetch commits')
332 331 return wrap_for_error(safe_str(e))
333 332
334 333 prev_data = None
335 334 next_data = None
336 335
337 336 try:
338 337 prev_graph = ext_json.json.loads(self.request.POST.get('graph') or '{}')
339 338 except ext_json.json.JSONDecodeError:
340 339 prev_graph = {}
341 340
342 341 if self.request.GET.get('chunk') == 'prev':
343 342 next_data = prev_graph
344 343 elif self.request.GET.get('chunk') == 'next':
345 344 prev_data = prev_graph
346 345
347 346 commit_ids = []
348 347 if not f_path:
349 348 # only load graph data when not in file history mode
350 349 commit_ids = c.pagination
351 350
352 351 c.graph_data, c.graph_commits = self._graph(
353 352 self.rhodecode_vcs_repo, commit_ids,
354 353 prev_data=prev_data, next_data=next_data)
355 354
356 355 return self._get_template_context(c)
@@ -1,117 +1,115 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 22
25 23 from rhodecode.apps._base import BaseAppView
26 24 from rhodecode.lib import helpers as h
27 25 from rhodecode.lib.auth import (NotAnonymous, HasRepoPermissionAny)
28 26 from rhodecode.model.db import Repository
29 27 from rhodecode.model.permission import PermissionModel
30 28 from rhodecode.model.validation_schema.types import RepoNameType
31 29
32 30 log = logging.getLogger(__name__)
33 31
34 32
35 33 class RepoChecksView(BaseAppView):
36 34 def load_default_context(self):
37 35 c = self._get_local_tmpl_context()
38 36 return c
39 37
40 38 @NotAnonymous()
41 39 def repo_creating(self):
42 40 c = self.load_default_context()
43 41 repo_name = self.request.matchdict['repo_name']
44 42 repo_name = RepoNameType().deserialize(None, repo_name)
45 43 db_repo = Repository.get_by_repo_name(repo_name)
46 44
47 45 # check if maybe repo is already created
48 46 if db_repo and db_repo.repo_state in [Repository.STATE_CREATED]:
49 47 self.flush_permissions_on_creation(db_repo)
50 48
51 49 # re-check permissions before redirecting to prevent resource
52 50 # discovery by checking the 302 code
53 51 perm_set = ['repository.read', 'repository.write', 'repository.admin']
54 52 has_perm = HasRepoPermissionAny(*perm_set)(
55 53 db_repo.repo_name, 'Repo Creating check')
56 54 if not has_perm:
57 55 raise HTTPNotFound()
58 56
59 57 raise HTTPFound(h.route_path(
60 58 'repo_summary', repo_name=db_repo.repo_name))
61 59
62 60 c.task_id = self.request.GET.get('task_id')
63 61 c.repo_name = repo_name
64 62
65 63 return self._get_template_context(c)
66 64
67 65 @NotAnonymous()
68 66 def repo_creating_check(self):
69 67 _ = self.request.translate
70 68 task_id = self.request.GET.get('task_id')
71 69 self.load_default_context()
72 70
73 71 repo_name = self.request.matchdict['repo_name']
74 72
75 73 if task_id and task_id not in ['None']:
76 74 import rhodecode
77 75 from rhodecode.lib.celerylib.loader import celery_app, exceptions
78 76 if rhodecode.CELERY_ENABLED:
79 77 log.debug('celery: checking result for task:%s', task_id)
80 78 task = celery_app.AsyncResult(task_id)
81 79 try:
82 80 task.get(timeout=10)
83 81 except exceptions.TimeoutError:
84 82 task = None
85 83 if task and task.failed():
86 84 msg = self._log_creation_exception(task.result, repo_name)
87 85 h.flash(msg, category='error')
88 86 raise HTTPFound(h.route_path('home'), code=501)
89 87
90 88 db_repo = Repository.get_by_repo_name(repo_name)
91 89 if db_repo and db_repo.repo_state == Repository.STATE_CREATED:
92 90 if db_repo.clone_uri:
93 91 clone_uri = db_repo.clone_uri_hidden
94 92 h.flash(_('Created repository %s from %s')
95 93 % (db_repo.repo_name, clone_uri), category='success')
96 94 else:
97 95 repo_url = h.link_to(
98 96 db_repo.repo_name,
99 97 h.route_path('repo_summary', repo_name=db_repo.repo_name))
100 98 fork = db_repo.fork
101 99 if fork:
102 100 fork_name = fork.repo_name
103 101 h.flash(h.literal(_('Forked repository %s as %s')
104 102 % (fork_name, repo_url)), category='success')
105 103 else:
106 104 h.flash(h.literal(_('Created repository %s') % repo_url),
107 105 category='success')
108 106 self.flush_permissions_on_creation(db_repo)
109 107
110 108 return {'result': True}
111 109 return {'result': False}
112 110
113 111 def flush_permissions_on_creation(self, db_repo):
114 112 # repo is finished and created, we flush the permissions now
115 113 user_group_perms = db_repo.permissions(expand_from_user_groups=True)
116 114 affected_user_ids = [perm['user_id'] for perm in user_group_perms]
117 115 PermissionModel().trigger_permission_flush(affected_user_ids)
@@ -1,831 +1,830 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import logging
21 20 import collections
22 21
23 22 from pyramid.httpexceptions import (
24 23 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 24 from pyramid.renderers import render
26 25 from pyramid.response import Response
27 26
28 27 from rhodecode.apps._base import RepoAppView
29 28 from rhodecode.apps.file_store import utils as store_utils
30 29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 30
32 31 from rhodecode.lib import diffs, codeblocks, channelstream
33 32 from rhodecode.lib.auth import (
34 33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 34 from rhodecode.lib import ext_json
36 35 from collections import OrderedDict
37 36 from rhodecode.lib.diffs import (
38 37 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 38 get_diff_whitespace_flag)
40 39 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 40 import rhodecode.lib.helpers as h
42 41 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
43 42 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 43 from rhodecode.lib.vcs.exceptions import (
45 44 RepositoryError, CommitDoesNotExistError)
46 45 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 46 ChangesetCommentHistory
48 47 from rhodecode.model.changeset_status import ChangesetStatusModel
49 48 from rhodecode.model.comment import CommentsModel
50 49 from rhodecode.model.meta import Session
51 50 from rhodecode.model.settings import VcsSettingsModel
52 51
53 52 log = logging.getLogger(__name__)
54 53
55 54
56 55 def _update_with_GET(params, request):
57 56 for k in ['diff1', 'diff2', 'diff']:
58 57 params[k] += request.GET.getall(k)
59 58
60 59
61 60 class RepoCommitsView(RepoAppView):
62 61 def load_default_context(self):
63 62 c = self._get_local_tmpl_context(include_app_defaults=True)
64 63 c.rhodecode_repo = self.rhodecode_vcs_repo
65 64
66 65 return c
67 66
68 67 def _is_diff_cache_enabled(self, target_repo):
69 68 caching_enabled = self._get_general_setting(
70 69 target_repo, 'rhodecode_diff_cache')
71 70 log.debug('Diff caching enabled: %s', caching_enabled)
72 71 return caching_enabled
73 72
74 73 def _commit(self, commit_id_range, method):
75 74 _ = self.request.translate
76 75 c = self.load_default_context()
77 76 c.fulldiff = self.request.GET.get('fulldiff')
78 77 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79 78
80 79 # fetch global flags of ignore ws or context lines
81 80 diff_context = get_diff_context(self.request)
82 81 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 82
84 83 # diff_limit will cut off the whole diff if the limit is applied
85 84 # otherwise it will just hide the big files from the front-end
86 85 diff_limit = c.visual.cut_off_limit_diff
87 86 file_limit = c.visual.cut_off_limit_file
88 87
89 88 # get ranges of commit ids if preset
90 89 commit_range = commit_id_range.split('...')[:2]
91 90
92 91 try:
93 92 pre_load = ['affected_files', 'author', 'branch', 'date',
94 93 'message', 'parents']
95 94 if self.rhodecode_vcs_repo.alias == 'hg':
96 95 pre_load += ['hidden', 'obsolete', 'phase']
97 96
98 97 if len(commit_range) == 2:
99 98 commits = self.rhodecode_vcs_repo.get_commits(
100 99 start_id=commit_range[0], end_id=commit_range[1],
101 100 pre_load=pre_load, translate_tags=False)
102 101 commits = list(commits)
103 102 else:
104 103 commits = [self.rhodecode_vcs_repo.get_commit(
105 104 commit_id=commit_id_range, pre_load=pre_load)]
106 105
107 106 c.commit_ranges = commits
108 107 if not c.commit_ranges:
109 108 raise RepositoryError('The commit range returned an empty result')
110 109 except CommitDoesNotExistError as e:
111 110 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 111 h.flash(msg, category='error')
113 112 raise HTTPNotFound()
114 113 except Exception:
115 114 log.exception("General failure")
116 115 raise HTTPNotFound()
117 116 single_commit = len(c.commit_ranges) == 1
118 117
119 118 if redirect_to_combined and not single_commit:
120 119 source_ref = getattr(c.commit_ranges[0].parents[0]
121 120 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 121 target_ref = c.commit_ranges[-1].raw_id
123 122 next_url = h.route_path(
124 123 'repo_compare',
125 124 repo_name=c.repo_name,
126 125 source_ref_type='rev',
127 126 source_ref=source_ref,
128 127 target_ref_type='rev',
129 128 target_ref=target_ref)
130 129 raise HTTPFound(next_url)
131 130
132 131 c.changes = OrderedDict()
133 132 c.lines_added = 0
134 133 c.lines_deleted = 0
135 134
136 135 # auto collapse if we have more than limit
137 136 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 137 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139 138
140 139 c.commit_statuses = ChangesetStatus.STATUSES
141 140 c.inline_comments = []
142 141 c.files = []
143 142
144 143 c.comments = []
145 144 c.unresolved_comments = []
146 145 c.resolved_comments = []
147 146
148 147 # Single commit
149 148 if single_commit:
150 149 commit = c.commit_ranges[0]
151 150 c.comments = CommentsModel().get_comments(
152 151 self.db_repo.repo_id,
153 152 revision=commit.raw_id)
154 153
155 154 # comments from PR
156 155 statuses = ChangesetStatusModel().get_statuses(
157 156 self.db_repo.repo_id, commit.raw_id,
158 157 with_revisions=True)
159 158
160 159 prs = set()
161 160 reviewers = list()
162 161 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 162 for c_status in statuses:
164 163
165 164 # extract associated pull-requests from votes
166 165 if c_status.pull_request:
167 166 prs.add(c_status.pull_request)
168 167
169 168 # extract reviewers
170 169 _user_id = c_status.author.user_id
171 170 if _user_id not in reviewers_duplicates:
172 171 reviewers.append(
173 172 StrictAttributeDict({
174 173 'user': c_status.author,
175 174
176 175 # fake attributed for commit, page that we don't have
177 176 # but we share the display with PR page
178 177 'mandatory': False,
179 178 'reasons': [],
180 179 'rule_user_group_data': lambda: None
181 180 })
182 181 )
183 182 reviewers_duplicates.add(_user_id)
184 183
185 184 c.reviewers_count = len(reviewers)
186 185 c.observers_count = 0
187 186
188 187 # from associated statuses, check the pull requests, and
189 188 # show comments from them
190 189 for pr in prs:
191 190 c.comments.extend(pr.comments)
192 191
193 192 c.unresolved_comments = CommentsModel()\
194 193 .get_commit_unresolved_todos(commit.raw_id)
195 194 c.resolved_comments = CommentsModel()\
196 195 .get_commit_resolved_todos(commit.raw_id)
197 196
198 197 c.inline_comments_flat = CommentsModel()\
199 198 .get_commit_inline_comments(commit.raw_id)
200 199
201 200 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 201 statuses, reviewers)
203 202
204 203 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205 204
206 205 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207 206
208 207 for review_obj, member, reasons, mandatory, status in review_statuses:
209 208 member_reviewer = h.reviewer_as_json(
210 209 member, reasons=reasons, mandatory=mandatory, role=None,
211 210 user_group=None
212 211 )
213 212
214 213 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 214 member_reviewer['review_status'] = current_review_status
216 215 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 216 member_reviewer['allowed_to_update'] = False
218 217 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219 218
220 219 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
221 220
222 221 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 222 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 223 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225 224
226 225 diff = None
227 226 # Iterate over ranges (default commit view is always one commit)
228 227 for commit in c.commit_ranges:
229 228 c.changes[commit.raw_id] = []
230 229
231 230 commit2 = commit
232 231 commit1 = commit.first_parent
233 232
234 233 if method == 'show':
235 234 inline_comments = CommentsModel().get_inline_comments(
236 235 self.db_repo.repo_id, revision=commit.raw_id)
237 236 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 237 inline_comments))
239 238 c.inline_comments = inline_comments
240 239
241 240 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 241 self.db_repo)
243 242 cache_file_path = diff_cache_exist(
244 243 cache_path, 'diff', commit.raw_id,
245 244 hide_whitespace_changes, diff_context, c.fulldiff)
246 245
247 246 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 247 force_recache = str2bool(self.request.GET.get('force_recache'))
249 248
250 249 cached_diff = None
251 250 if caching_enabled:
252 251 cached_diff = load_cached_diff(cache_file_path)
253 252
254 253 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 254 if not force_recache and has_proper_diff_cache:
256 255 diffset = cached_diff['diff']
257 256 else:
258 257 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 258 commit1, commit2,
260 259 ignore_whitespace=hide_whitespace_changes,
261 260 context=diff_context)
262 261
263 262 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
264 263 diff_limit=diff_limit,
265 264 file_limit=file_limit,
266 265 show_full_diff=c.fulldiff)
267 266
268 267 _parsed = diff_processor.prepare()
269 268
270 269 diffset = codeblocks.DiffSet(
271 270 repo_name=self.db_repo_name,
272 271 source_node_getter=codeblocks.diffset_node_getter(commit1),
273 272 target_node_getter=codeblocks.diffset_node_getter(commit2))
274 273
275 274 diffset = self.path_filter.render_patchset_filtered(
276 275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277 276
278 277 # save cached diff
279 278 if caching_enabled:
280 279 cache_diff(cache_file_path, diffset, None)
281 280
282 281 c.limited_diff = diffset.limited_diff
283 282 c.changes[commit.raw_id] = diffset
284 283 else:
285 284 # TODO(marcink): no cache usage here...
286 285 _diff = self.rhodecode_vcs_repo.get_diff(
287 286 commit1, commit2,
288 287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
289 288 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
290 289 diff_limit=diff_limit,
291 290 file_limit=file_limit, show_full_diff=c.fulldiff)
292 291 # downloads/raw we only need RAW diff nothing else
293 292 diff = self.path_filter.get_raw_patch(diff_processor)
294 293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
295 294
296 295 # sort comments by how they were generated
297 296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
298 297 c.at_version_num = None
299 298
300 299 if len(c.commit_ranges) == 1:
301 300 c.commit = c.commit_ranges[0]
302 301 c.parent_tmpl = ''.join(
303 302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
304 303
305 304 if method == 'download':
306 305 response = Response(diff)
307 306 response.content_type = 'text/plain'
308 307 response.content_disposition = (
309 308 'attachment; filename=%s.diff' % commit_id_range[:12])
310 309 return response
311 310 elif method == 'patch':
312 311
313 312 c.diff = safe_str(diff)
314 313 patch = render(
315 314 'rhodecode:templates/changeset/patch_changeset.mako',
316 315 self._get_template_context(c), self.request)
317 316 response = Response(patch)
318 317 response.content_type = 'text/plain'
319 318 return response
320 319 elif method == 'raw':
321 320 response = Response(diff)
322 321 response.content_type = 'text/plain'
323 322 return response
324 323 elif method == 'show':
325 324 if len(c.commit_ranges) == 1:
326 325 html = render(
327 326 'rhodecode:templates/changeset/changeset.mako',
328 327 self._get_template_context(c), self.request)
329 328 return Response(html)
330 329 else:
331 330 c.ancestor = None
332 331 c.target_repo = self.db_repo
333 332 html = render(
334 333 'rhodecode:templates/changeset/changeset_range.mako',
335 334 self._get_template_context(c), self.request)
336 335 return Response(html)
337 336
338 337 raise HTTPBadRequest()
339 338
340 339 @LoginRequired()
341 340 @HasRepoPermissionAnyDecorator(
342 341 'repository.read', 'repository.write', 'repository.admin')
343 342 def repo_commit_show(self):
344 343 commit_id = self.request.matchdict['commit_id']
345 344 return self._commit(commit_id, method='show')
346 345
347 346 @LoginRequired()
348 347 @HasRepoPermissionAnyDecorator(
349 348 'repository.read', 'repository.write', 'repository.admin')
350 349 def repo_commit_raw(self):
351 350 commit_id = self.request.matchdict['commit_id']
352 351 return self._commit(commit_id, method='raw')
353 352
354 353 @LoginRequired()
355 354 @HasRepoPermissionAnyDecorator(
356 355 'repository.read', 'repository.write', 'repository.admin')
357 356 def repo_commit_patch(self):
358 357 commit_id = self.request.matchdict['commit_id']
359 358 return self._commit(commit_id, method='patch')
360 359
361 360 @LoginRequired()
362 361 @HasRepoPermissionAnyDecorator(
363 362 'repository.read', 'repository.write', 'repository.admin')
364 363 def repo_commit_download(self):
365 364 commit_id = self.request.matchdict['commit_id']
366 365 return self._commit(commit_id, method='download')
367 366
368 367 def _commit_comments_create(self, commit_id, comments):
369 368 _ = self.request.translate
370 369 data = {}
371 370 if not comments:
372 371 return
373 372
374 373 commit = self.db_repo.get_commit(commit_id)
375 374
376 375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
377 376 for entry in comments:
378 377 c = self.load_default_context()
379 378 comment_type = entry['comment_type']
380 379 text = entry['text']
381 380 status = entry['status']
382 381 is_draft = str2bool(entry['is_draft'])
383 382 resolves_comment_id = entry['resolves_comment_id']
384 383 f_path = entry['f_path']
385 384 line_no = entry['line']
386 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
385 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
387 386
388 387 if status:
389 388 text = text or (_('Status change %(transition_icon)s %(status)s')
390 389 % {'transition_icon': '>',
391 390 'status': ChangesetStatus.get_status_lbl(status)})
392 391
393 392 comment = CommentsModel().create(
394 393 text=text,
395 394 repo=self.db_repo.repo_id,
396 395 user=self._rhodecode_db_user.user_id,
397 396 commit_id=commit_id,
398 397 f_path=f_path,
399 398 line_no=line_no,
400 399 status_change=(ChangesetStatus.get_status_lbl(status)
401 400 if status else None),
402 401 status_change_type=status,
403 402 comment_type=comment_type,
404 403 is_draft=is_draft,
405 404 resolves_comment_id=resolves_comment_id,
406 405 auth_user=self._rhodecode_user,
407 406 send_email=not is_draft, # skip notification for draft comments
408 407 )
409 408 is_inline = comment.is_inline
410 409
411 410 # get status if set !
412 411 if status:
413 412 # `dont_allow_on_closed_pull_request = True` means
414 413 # if latest status was from pull request and it's closed
415 414 # disallow changing status !
416 415
417 416 try:
418 417 ChangesetStatusModel().set_status(
419 418 self.db_repo.repo_id,
420 419 status,
421 420 self._rhodecode_db_user.user_id,
422 421 comment,
423 422 revision=commit_id,
424 423 dont_allow_on_closed_pull_request=True
425 424 )
426 425 except StatusChangeOnClosedPullRequestError:
427 426 msg = _('Changing the status of a commit associated with '
428 427 'a closed pull request is not allowed')
429 428 log.exception(msg)
430 429 h.flash(msg, category='warning')
431 430 raise HTTPFound(h.route_path(
432 431 'repo_commit', repo_name=self.db_repo_name,
433 432 commit_id=commit_id))
434 433
435 434 Session().flush()
436 435 # this is somehow required to get access to some relationship
437 436 # loaded on comment
438 437 Session().refresh(comment)
439 438
440 439 # skip notifications for drafts
441 440 if not is_draft:
442 441 CommentsModel().trigger_commit_comment_hook(
443 442 self.db_repo, self._rhodecode_user, 'create',
444 443 data={'comment': comment, 'commit': commit})
445 444
446 445 comment_id = comment.comment_id
447 446 data[comment_id] = {
448 447 'target_id': target_elem_id
449 448 }
450 449 Session().flush()
451 450
452 451 c.co = comment
453 452 c.at_version_num = 0
454 453 c.is_new = True
455 454 rendered_comment = render(
456 455 'rhodecode:templates/changeset/changeset_comment_block.mako',
457 456 self._get_template_context(c), self.request)
458 457
459 458 data[comment_id].update(comment.get_dict())
460 459 data[comment_id].update({'rendered_text': rendered_comment})
461 460
462 461 # finalize, commit and redirect
463 462 Session().commit()
464 463
465 464 # skip channelstream for draft comments
466 465 if not all_drafts:
467 466 comment_broadcast_channel = channelstream.comment_channel(
468 467 self.db_repo_name, commit_obj=commit)
469 468
470 469 comment_data = data
471 470 posted_comment_type = 'inline' if is_inline else 'general'
472 471 if len(data) == 1:
473 472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
474 473 else:
475 474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
476 475
477 476 channelstream.comment_channelstream_push(
478 477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
479 478 comment_data=comment_data)
480 479
481 480 return data
482 481
483 482 @LoginRequired()
484 483 @NotAnonymous()
485 484 @HasRepoPermissionAnyDecorator(
486 485 'repository.read', 'repository.write', 'repository.admin')
487 486 @CSRFRequired()
488 487 def repo_commit_comment_create(self):
489 488 _ = self.request.translate
490 489 commit_id = self.request.matchdict['commit_id']
491 490
492 491 multi_commit_ids = []
493 492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
494 493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
495 494 if _commit_id not in multi_commit_ids:
496 495 multi_commit_ids.append(_commit_id)
497 496
498 497 commit_ids = multi_commit_ids or [commit_id]
499 498
500 499 data = []
501 500 # Multiple comments for each passed commit id
502 501 for current_id in filter(None, commit_ids):
503 502 comment_data = {
504 503 'comment_type': self.request.POST.get('comment_type'),
505 504 'text': self.request.POST.get('text'),
506 505 'status': self.request.POST.get('changeset_status', None),
507 506 'is_draft': self.request.POST.get('draft'),
508 507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
509 508 'close_pull_request': self.request.POST.get('close_pull_request'),
510 509 'f_path': self.request.POST.get('f_path'),
511 510 'line': self.request.POST.get('line'),
512 511 }
513 512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
514 513 data.append(comment)
515 514
516 515 return data if len(data) > 1 else data[0]
517 516
518 517 @LoginRequired()
519 518 @NotAnonymous()
520 519 @HasRepoPermissionAnyDecorator(
521 520 'repository.read', 'repository.write', 'repository.admin')
522 521 @CSRFRequired()
523 522 def repo_commit_comment_preview(self):
524 523 # Technically a CSRF token is not needed as no state changes with this
525 524 # call. However, as this is a POST is better to have it, so automated
526 525 # tools don't flag it as potential CSRF.
527 526 # Post is required because the payload could be bigger than the maximum
528 527 # allowed by GET.
529 528
530 529 text = self.request.POST.get('text')
531 530 renderer = self.request.POST.get('renderer') or 'rst'
532 531 if text:
533 532 return h.render(text, renderer=renderer, mentions=True,
534 533 repo_name=self.db_repo_name)
535 534 return ''
536 535
537 536 @LoginRequired()
538 537 @HasRepoPermissionAnyDecorator(
539 538 'repository.read', 'repository.write', 'repository.admin')
540 539 @CSRFRequired()
541 540 def repo_commit_comment_history_view(self):
542 541 c = self.load_default_context()
543 542 comment_id = self.request.matchdict['comment_id']
544 543 comment_history_id = self.request.matchdict['comment_history_id']
545 544
546 545 comment = ChangesetComment.get_or_404(comment_id)
547 546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
548 547 if comment.draft and not comment_owner:
549 548 # if we see draft comments history, we only allow this for owner
550 549 raise HTTPNotFound()
551 550
552 551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
553 552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
554 553
555 554 if is_repo_comment:
556 555 c.comment_history = comment_history
557 556
558 557 rendered_comment = render(
559 558 'rhodecode:templates/changeset/comment_history.mako',
560 559 self._get_template_context(c), self.request)
561 560 return rendered_comment
562 561 else:
563 562 log.warning('No permissions for user %s to show comment_history_id: %s',
564 563 self._rhodecode_db_user, comment_history_id)
565 564 raise HTTPNotFound()
566 565
567 566 @LoginRequired()
568 567 @NotAnonymous()
569 568 @HasRepoPermissionAnyDecorator(
570 569 'repository.read', 'repository.write', 'repository.admin')
571 570 @CSRFRequired()
572 571 def repo_commit_comment_attachment_upload(self):
573 572 c = self.load_default_context()
574 573 upload_key = 'attachment'
575 574
576 575 file_obj = self.request.POST.get(upload_key)
577 576
578 577 if file_obj is None:
579 578 self.request.response.status = 400
580 579 return {'store_fid': None,
581 580 'access_path': None,
582 'error': '{} data field is missing'.format(upload_key)}
581 'error': f'{upload_key} data field is missing'}
583 582
584 583 if not hasattr(file_obj, 'filename'):
585 584 self.request.response.status = 400
586 585 return {'store_fid': None,
587 586 'access_path': None,
588 587 'error': 'filename cannot be read from the data field'}
589 588
590 589 filename = file_obj.filename
591 590 file_display_name = filename
592 591
593 592 metadata = {
594 593 'user_uploaded': {'username': self._rhodecode_user.username,
595 594 'user_id': self._rhodecode_user.user_id,
596 595 'ip': self._rhodecode_user.ip_addr}}
597 596
598 597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
599 598 allowed_extensions = [
600 599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
601 600 '.pptx', '.txt', '.xlsx', '.zip']
602 601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
603 602
604 603 try:
605 604 storage = store_utils.get_file_storage(self.request.registry.settings)
606 605 store_uid, metadata = storage.save_file(
607 606 file_obj.file, filename, extra_metadata=metadata,
608 607 extensions=allowed_extensions, max_filesize=max_file_size)
609 608 except FileNotAllowedException:
610 609 self.request.response.status = 400
611 610 permitted_extensions = ', '.join(allowed_extensions)
612 611 error_msg = 'File `{}` is not allowed. ' \
613 612 'Only following extensions are permitted: {}'.format(
614 613 filename, permitted_extensions)
615 614 return {'store_fid': None,
616 615 'access_path': None,
617 616 'error': error_msg}
618 617 except FileOverSizeException:
619 618 self.request.response.status = 400
620 619 limit_mb = h.format_byte_size_binary(max_file_size)
621 620 return {'store_fid': None,
622 621 'access_path': None,
623 622 'error': 'File {} is exceeding allowed limit of {}.'.format(
624 623 filename, limit_mb)}
625 624
626 625 try:
627 626 entry = FileStore.create(
628 627 file_uid=store_uid, filename=metadata["filename"],
629 628 file_hash=metadata["sha256"], file_size=metadata["size"],
630 629 file_display_name=file_display_name,
631 file_description=u'comment attachment `{}`'.format(safe_str(filename)),
630 file_description=f'comment attachment `{safe_str(filename)}`',
632 631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
633 632 scope_repo_id=self.db_repo.repo_id
634 633 )
635 634 Session().add(entry)
636 635 Session().commit()
637 636 log.debug('Stored upload in DB as %s', entry)
638 637 except Exception:
639 638 log.exception('Failed to store file %s', filename)
640 639 self.request.response.status = 400
641 640 return {'store_fid': None,
642 641 'access_path': None,
643 'error': 'File {} failed to store in DB.'.format(filename)}
642 'error': f'File {filename} failed to store in DB.'}
644 643
645 644 Session().commit()
646 645
647 646 data = {
648 647 'store_fid': store_uid,
649 648 'access_path': h.route_path(
650 649 'download_file', fid=store_uid),
651 650 'fqn_access_path': h.route_url(
652 651 'download_file', fid=store_uid),
653 652 # for EE those are replaced by FQN links on repo-only like
654 653 'repo_access_path': h.route_url(
655 654 'download_file', fid=store_uid),
656 655 'repo_fqn_access_path': h.route_url(
657 656 'download_file', fid=store_uid),
658 657 }
659 658 # this data is a part of CE/EE additional code
660 659 if c.rhodecode_edition_id == 'EE':
661 660 data.update({
662 661 'repo_access_path': h.route_path(
663 662 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
664 663 'repo_fqn_access_path': h.route_url(
665 664 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
666 665 })
667 666
668 667 return data
669 668
670 669 @LoginRequired()
671 670 @NotAnonymous()
672 671 @HasRepoPermissionAnyDecorator(
673 672 'repository.read', 'repository.write', 'repository.admin')
674 673 @CSRFRequired()
675 674 def repo_commit_comment_delete(self):
676 675 commit_id = self.request.matchdict['commit_id']
677 676 comment_id = self.request.matchdict['comment_id']
678 677
679 678 comment = ChangesetComment.get_or_404(comment_id)
680 679 if not comment:
681 680 log.debug('Comment with id:%s not found, skipping', comment_id)
682 681 # comment already deleted in another call probably
683 682 return True
684 683
685 684 if comment.immutable:
686 685 # don't allow deleting comments that are immutable
687 686 raise HTTPForbidden()
688 687
689 688 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
690 689 super_admin = h.HasPermissionAny('hg.admin')()
691 690 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
692 691 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
693 692 comment_repo_admin = is_repo_admin and is_repo_comment
694 693
695 694 if comment.draft and not comment_owner:
696 695 # We never allow to delete draft comments for other than owners
697 696 raise HTTPNotFound()
698 697
699 698 if super_admin or comment_owner or comment_repo_admin:
700 699 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
701 700 Session().commit()
702 701 return True
703 702 else:
704 703 log.warning('No permissions for user %s to delete comment_id: %s',
705 704 self._rhodecode_db_user, comment_id)
706 705 raise HTTPNotFound()
707 706
708 707 @LoginRequired()
709 708 @NotAnonymous()
710 709 @HasRepoPermissionAnyDecorator(
711 710 'repository.read', 'repository.write', 'repository.admin')
712 711 @CSRFRequired()
713 712 def repo_commit_comment_edit(self):
714 713 self.load_default_context()
715 714
716 715 commit_id = self.request.matchdict['commit_id']
717 716 comment_id = self.request.matchdict['comment_id']
718 717 comment = ChangesetComment.get_or_404(comment_id)
719 718
720 719 if comment.immutable:
721 720 # don't allow deleting comments that are immutable
722 721 raise HTTPForbidden()
723 722
724 723 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
725 724 super_admin = h.HasPermissionAny('hg.admin')()
726 725 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
727 726 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
728 727 comment_repo_admin = is_repo_admin and is_repo_comment
729 728
730 729 if super_admin or comment_owner or comment_repo_admin:
731 730 text = self.request.POST.get('text')
732 731 version = self.request.POST.get('version')
733 732 if text == comment.text:
734 733 log.warning(
735 734 'Comment(repo): '
736 735 'Trying to create new version '
737 736 'with the same comment body {}'.format(
738 737 comment_id,
739 738 )
740 739 )
741 740 raise HTTPNotFound()
742 741
743 742 if version.isdigit():
744 743 version = int(version)
745 744 else:
746 745 log.warning(
747 746 'Comment(repo): Wrong version type {} {} '
748 747 'for comment {}'.format(
749 748 version,
750 749 type(version),
751 750 comment_id,
752 751 )
753 752 )
754 753 raise HTTPNotFound()
755 754
756 755 try:
757 756 comment_history = CommentsModel().edit(
758 757 comment_id=comment_id,
759 758 text=text,
760 759 auth_user=self._rhodecode_user,
761 760 version=version,
762 761 )
763 762 except CommentVersionMismatch:
764 763 raise HTTPConflict()
765 764
766 765 if not comment_history:
767 766 raise HTTPNotFound()
768 767
769 768 if not comment.draft:
770 769 commit = self.db_repo.get_commit(commit_id)
771 770 CommentsModel().trigger_commit_comment_hook(
772 771 self.db_repo, self._rhodecode_user, 'edit',
773 772 data={'comment': comment, 'commit': commit})
774 773
775 774 Session().commit()
776 775 return {
777 776 'comment_history_id': comment_history.comment_history_id,
778 777 'comment_id': comment.comment_id,
779 778 'comment_version': comment_history.version,
780 779 'comment_author_username': comment_history.author.username,
781 780 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
782 781 'comment_created_on': h.age_component(comment_history.created_on,
783 782 time_is_local=True),
784 783 }
785 784 else:
786 785 log.warning('No permissions for user %s to edit comment_id: %s',
787 786 self._rhodecode_db_user, comment_id)
788 787 raise HTTPNotFound()
789 788
790 789 @LoginRequired()
791 790 @HasRepoPermissionAnyDecorator(
792 791 'repository.read', 'repository.write', 'repository.admin')
793 792 def repo_commit_data(self):
794 793 commit_id = self.request.matchdict['commit_id']
795 794 self.load_default_context()
796 795
797 796 try:
798 797 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
799 798 except CommitDoesNotExistError as e:
800 799 return EmptyCommit(message=str(e))
801 800
802 801 @LoginRequired()
803 802 @HasRepoPermissionAnyDecorator(
804 803 'repository.read', 'repository.write', 'repository.admin')
805 804 def repo_commit_children(self):
806 805 commit_id = self.request.matchdict['commit_id']
807 806 self.load_default_context()
808 807
809 808 try:
810 809 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
811 810 children = commit.children
812 811 except CommitDoesNotExistError:
813 812 children = []
814 813
815 814 result = {"results": children}
816 815 return result
817 816
818 817 @LoginRequired()
819 818 @HasRepoPermissionAnyDecorator(
820 819 'repository.read', 'repository.write', 'repository.admin')
821 820 def repo_commit_parents(self):
822 821 commit_id = self.request.matchdict['commit_id']
823 822 self.load_default_context()
824 823
825 824 try:
826 825 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
827 826 parents = commit.parents
828 827 except CommitDoesNotExistError:
829 828 parents = []
830 829 result = {"results": parents}
831 830 return result
@@ -1,307 +1,305 b''
1
2
3 1 # Copyright (C) 2012-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19
22 20 import logging
23 21
24 22 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 23
26 24 from pyramid.renderers import render
27 25 from pyramid.response import Response
28 26
29 27 from rhodecode.apps._base import RepoAppView
30 28
31 29 from rhodecode.lib import helpers as h
32 30 from rhodecode.lib import diffs, codeblocks
33 31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 32 from rhodecode.lib.utils import safe_str
35 33 from rhodecode.lib.utils2 import str2bool
36 34 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 35 from rhodecode.lib.vcs.exceptions import (
38 36 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 37 NodeDoesNotExistError)
40 38 from rhodecode.model.db import Repository, ChangesetStatus
41 39
42 40 log = logging.getLogger(__name__)
43 41
44 42
45 43 class RepoCompareView(RepoAppView):
46 44 def load_default_context(self):
47 45 c = self._get_local_tmpl_context(include_app_defaults=True)
48 46 c.rhodecode_repo = self.rhodecode_vcs_repo
49 47 return c
50 48
51 49 def _get_commit_or_redirect(
52 50 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 51 """
54 52 This is a safe way to get a commit. If an error occurs it
55 53 redirects to a commit with a proper message. If partial is set
56 54 then it does not do redirect raise and throws an exception instead.
57 55 """
58 56 _ = self.request.translate
59 57 try:
60 58 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 59 except EmptyRepositoryError:
62 60 if not redirect_after:
63 61 return repo.scm_instance().EMPTY_COMMIT
64 62 h.flash(h.literal(_('There are no commits yet')),
65 63 category='warning')
66 64 if not partial:
67 65 raise HTTPFound(
68 66 h.route_path('repo_summary', repo_name=repo.repo_name))
69 67 raise HTTPBadRequest()
70 68
71 69 except RepositoryError as e:
72 70 log.exception(safe_str(e))
73 71 h.flash(h.escape(safe_str(e)), category='warning')
74 72 if not partial:
75 73 raise HTTPFound(
76 74 h.route_path('repo_summary', repo_name=repo.repo_name))
77 75 raise HTTPBadRequest()
78 76
79 77 @LoginRequired()
80 78 @HasRepoPermissionAnyDecorator(
81 79 'repository.read', 'repository.write', 'repository.admin')
82 80 def compare_select(self):
83 81 _ = self.request.translate
84 82 c = self.load_default_context()
85 83
86 84 source_repo = self.db_repo_name
87 85 target_repo = self.request.GET.get('target_repo', source_repo)
88 86 c.source_repo = Repository.get_by_repo_name(source_repo)
89 87 c.target_repo = Repository.get_by_repo_name(target_repo)
90 88
91 89 if c.source_repo is None or c.target_repo is None:
92 90 raise HTTPNotFound()
93 91
94 92 c.compare_home = True
95 93 c.commit_ranges = []
96 94 c.collapse_all_commits = False
97 95 c.diffset = None
98 96 c.limited_diff = False
99 97 c.source_ref = c.target_ref = _('Select commit')
100 98 c.source_ref_type = ""
101 99 c.target_ref_type = ""
102 100 c.commit_statuses = ChangesetStatus.STATUSES
103 101 c.preview_mode = False
104 102 c.file_path = None
105 103
106 104 return self._get_template_context(c)
107 105
108 106 @LoginRequired()
109 107 @HasRepoPermissionAnyDecorator(
110 108 'repository.read', 'repository.write', 'repository.admin')
111 109 def compare(self):
112 110 _ = self.request.translate
113 111 c = self.load_default_context()
114 112
115 113 source_ref_type = self.request.matchdict['source_ref_type']
116 114 source_ref = self.request.matchdict['source_ref']
117 115 target_ref_type = self.request.matchdict['target_ref_type']
118 116 target_ref = self.request.matchdict['target_ref']
119 117
120 118 # source_ref will be evaluated in source_repo
121 119 source_repo_name = self.db_repo_name
122 120 source_path, source_id = parse_path_ref(source_ref)
123 121
124 122 # target_ref will be evaluated in target_repo
125 123 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 124 target_path, target_id = parse_path_ref(
127 125 target_ref, default_path=self.request.GET.get('f_path', ''))
128 126
129 127 # if merge is True
130 128 # Show what changes since the shared ancestor commit of target/source
131 129 # the source would get if it was merged with target. Only commits
132 130 # which are in target but not in source will be shown.
133 131 merge = str2bool(self.request.GET.get('merge'))
134 132 # if merge is False
135 133 # Show a raw diff of source/target refs even if no ancestor exists
136 134
137 135 # c.fulldiff disables cut_off_limit
138 136 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139 137
140 138 # fetch global flags of ignore ws or context lines
141 139 diff_context = diffs.get_diff_context(self.request)
142 140 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143 141
144 142 c.file_path = target_path
145 143 c.commit_statuses = ChangesetStatus.STATUSES
146 144
147 145 # if partial, returns just compare_commits.html (commits log)
148 146 partial = self.request.is_xhr
149 147
150 148 # swap url for compare_diff page
151 149 c.swap_url = h.route_path(
152 150 'repo_compare',
153 151 repo_name=target_repo_name,
154 152 source_ref_type=target_ref_type,
155 153 source_ref=target_ref,
156 154 target_repo=source_repo_name,
157 155 target_ref_type=source_ref_type,
158 156 target_ref=source_ref,
159 157 _query=dict(merge=merge and '1' or '', f_path=target_path))
160 158
161 159 source_repo = Repository.get_by_repo_name(source_repo_name)
162 160 target_repo = Repository.get_by_repo_name(target_repo_name)
163 161
164 162 if source_repo is None:
165 163 log.error('Could not find the source repo: {}'
166 164 .format(source_repo_name))
167 165 h.flash(_('Could not find the source repo: `{}`')
168 166 .format(h.escape(source_repo_name)), category='error')
169 167 raise HTTPFound(
170 168 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171 169
172 170 if target_repo is None:
173 171 log.error('Could not find the target repo: {}'
174 172 .format(source_repo_name))
175 173 h.flash(_('Could not find the target repo: `{}`')
176 174 .format(h.escape(target_repo_name)), category='error')
177 175 raise HTTPFound(
178 176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179 177
180 178 source_scm = source_repo.scm_instance()
181 179 target_scm = target_repo.scm_instance()
182 180
183 181 source_alias = source_scm.alias
184 182 target_alias = target_scm.alias
185 183 if source_alias != target_alias:
186 184 msg = _('The comparison of two different kinds of remote repos '
187 185 'is not available')
188 186 log.error(msg)
189 187 h.flash(msg, category='error')
190 188 raise HTTPFound(
191 189 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192 190
193 191 source_commit = self._get_commit_or_redirect(
194 192 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 193 partial=partial)
196 194 target_commit = self._get_commit_or_redirect(
197 195 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 196 partial=partial)
199 197
200 198 c.compare_home = False
201 199 c.source_repo = source_repo
202 200 c.target_repo = target_repo
203 201 c.source_ref = source_ref
204 202 c.target_ref = target_ref
205 203 c.source_ref_type = source_ref_type
206 204 c.target_ref_type = target_ref_type
207 205
208 206 pre_load = ["author", "date", "message", "branch"]
209 207 c.ancestor = None
210 208
211 209 try:
212 210 c.commit_ranges = source_scm.compare(
213 211 source_commit.raw_id, target_commit.raw_id,
214 212 target_scm, merge, pre_load=pre_load) or []
215 213 if merge:
216 214 c.ancestor = source_scm.get_common_ancestor(
217 215 source_commit.raw_id, target_commit.raw_id, target_scm)
218 216 except RepositoryRequirementError:
219 217 msg = _('Could not compare repos with different '
220 218 'large file settings')
221 219 log.error(msg)
222 220 if partial:
223 221 return Response(msg)
224 222 h.flash(msg, category='error')
225 223 raise HTTPFound(
226 224 h.route_path('repo_compare_select',
227 225 repo_name=self.db_repo_name))
228 226
229 227 c.statuses = self.db_repo.statuses(
230 228 [x.raw_id for x in c.commit_ranges])
231 229
232 230 # auto collapse if we have more than limit
233 231 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 232 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235 233
236 234 if partial: # for PR ajax commits loader
237 235 if not c.ancestor:
238 236 return Response('') # cannot merge if there is no ancestor
239 237
240 238 html = render(
241 239 'rhodecode:templates/compare/compare_commits.mako',
242 240 self._get_template_context(c), self.request)
243 241 return Response(html)
244 242
245 243 if c.ancestor:
246 244 # case we want a simple diff without incoming commits,
247 245 # previewing what will be merged.
248 246 # Make the diff on target repo (which is known to have target_ref)
249 247 log.debug('Using ancestor %s as source_ref instead of %s',
250 248 c.ancestor, source_ref)
251 249 source_repo = target_repo
252 250 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253 251
254 252 # diff_limit will cut off the whole diff if the limit is applied
255 253 # otherwise it will just hide the big files from the front-end
256 254 diff_limit = c.visual.cut_off_limit_diff
257 255 file_limit = c.visual.cut_off_limit_file
258 256
259 257 log.debug('calculating diff between '
260 258 'source_ref:%s and target_ref:%s for repo `%s`',
261 259 source_commit, target_commit,
262 260 safe_str(source_repo.scm_instance().path))
263 261
264 262 if source_commit.repository != target_commit.repository:
265 263
266 264 msg = _(
267 265 "Repositories unrelated. "
268 266 "Cannot compare commit %(commit1)s from repository %(repo1)s "
269 267 "with commit %(commit2)s from repository %(repo2)s.") % {
270 268 'commit1': h.show_id(source_commit),
271 269 'repo1': source_repo.repo_name,
272 270 'commit2': h.show_id(target_commit),
273 271 'repo2': target_repo.repo_name,
274 272 }
275 273 h.flash(msg, category='error')
276 274 raise HTTPFound(
277 275 h.route_path('repo_compare_select',
278 276 repo_name=self.db_repo_name))
279 277
280 278 txt_diff = source_repo.scm_instance().get_diff(
281 279 commit1=source_commit, commit2=target_commit,
282 280 path=target_path, path1=source_path,
283 281 ignore_whitespace=hide_whitespace_changes, context=diff_context)
284 282
285 283 diff_processor = diffs.DiffProcessor(txt_diff, diff_format='newdiff',
286 284 diff_limit=diff_limit,
287 285 file_limit=file_limit,
288 286 show_full_diff=c.fulldiff)
289 287 _parsed = diff_processor.prepare()
290 288
291 289 diffset = codeblocks.DiffSet(
292 290 repo_name=source_repo.repo_name,
293 291 source_node_getter=codeblocks.diffset_node_getter(source_commit),
294 292 target_repo_name=self.db_repo_name,
295 293 target_node_getter=codeblocks.diffset_node_getter(target_commit),
296 294 )
297 295 c.diffset = self.path_filter.render_patchset_filtered(
298 296 diffset, _parsed, source_ref, target_ref)
299 297
300 298 c.preview_mode = merge
301 299 c.source_commit = source_commit
302 300 c.target_commit = target_commit
303 301
304 302 html = render(
305 303 'rhodecode:templates/compare/compare_diff.mako',
306 304 self._get_template_context(c), self.request)
307 305 return Response(html) No newline at end of file
@@ -1,215 +1,213 b''
1
2
3 1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20 import datetime
23 21
24 22 from pyramid.response import Response
25 23
26 24 from rhodecode.apps._base import RepoAppView
27 25 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
28 26 from rhodecode.lib import audit_logger
29 27 from rhodecode.lib import rc_cache
30 28 from rhodecode.lib import helpers as h
31 29 from rhodecode.lib.auth import (
32 30 LoginRequired, HasRepoPermissionAnyDecorator)
33 31 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 32 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
35 33 from rhodecode.model.db import UserApiKeys, CacheKey
36 34
37 35 log = logging.getLogger(__name__)
38 36
39 37
40 38 class RepoFeedView(RepoAppView):
41 39 def load_default_context(self):
42 40 c = self._get_local_tmpl_context()
43 41 self._load_defaults()
44 42 return c
45 43
46 44 def _get_config(self):
47 45 import rhodecode
48 46 config = rhodecode.CONFIG
49 47
50 48 return {
51 49 'language': 'en-us',
52 50 'feed_ttl': '5', # TTL of feed,
53 51 'feed_include_diff':
54 52 str2bool(config.get('rss_include_diff', False)),
55 53 'feed_items_per_page':
56 54 safe_int(config.get('rss_items_per_page', 20)),
57 55 'feed_diff_limit':
58 56 # we need to protect from parsing huge diffs here other way
59 57 # we can kill the server
60 58 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
61 59 }
62 60
63 61 def _load_defaults(self):
64 62 _ = self.request.translate
65 63 config = self._get_config()
66 64 # common values for feeds
67 65 self.description = _('Changes on %s repository')
68 66 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
69 67 self.language = config["language"]
70 68 self.ttl = config["feed_ttl"]
71 69 self.feed_include_diff = config['feed_include_diff']
72 70 self.feed_diff_limit = config['feed_diff_limit']
73 71 self.feed_items_per_page = config['feed_items_per_page']
74 72
75 73 def _changes(self, commit):
76 74 diff = commit.diff()
77 75 diff_processor = DiffProcessor(diff, diff_format='newdiff',
78 76 diff_limit=self.feed_diff_limit)
79 77 _parsed = diff_processor.prepare(inline_diff=False)
80 78 limited_diff = isinstance(_parsed, LimitedDiffContainer)
81 79
82 80 return diff_processor, _parsed, limited_diff
83 81
84 82 def _get_title(self, commit):
85 83 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
86 84
87 85 def _get_description(self, commit):
88 86 _renderer = self.request.get_partial_renderer(
89 87 'rhodecode:templates/feed/atom_feed_entry.mako')
90 88 diff_processor, parsed_diff, limited_diff = self._changes(commit)
91 89 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
92 90 return _renderer(
93 91 'body',
94 92 commit=commit,
95 93 parsed_diff=filtered_parsed_diff,
96 94 limited_diff=limited_diff,
97 95 feed_include_diff=self.feed_include_diff,
98 96 diff_processor=diff_processor,
99 97 has_hidden_changes=has_hidden_changes
100 98 )
101 99
102 def _set_timezone(self, date, tzinfo=datetime.timezone.utc):
100 def _set_timezone(self, date, tzinfo=datetime.UTC):
103 101 if not getattr(date, "tzinfo", None):
104 102 date.replace(tzinfo=tzinfo)
105 103 return date
106 104
107 105 def _get_commits(self):
108 106 pre_load = ['author', 'branch', 'date', 'message', 'parents']
109 107 if self.rhodecode_vcs_repo.is_empty():
110 108 return []
111 109
112 110 collection = self.rhodecode_vcs_repo.get_commits(
113 111 branch_name=None, show_hidden=False, pre_load=pre_load,
114 112 translate_tags=False)
115 113
116 114 return list(collection[-self.feed_items_per_page:])
117 115
118 116 def uid(self, repo_id, commit_id):
119 117 return '{}:{}'.format(
120 118 md5_safe(repo_id, return_type='str'),
121 119 md5_safe(commit_id, return_type='str')
122 120 )
123 121
124 122 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
125 123 @HasRepoPermissionAnyDecorator(
126 124 'repository.read', 'repository.write', 'repository.admin')
127 125 def atom(self):
128 126 """
129 127 Produce an atom-1.0 feed via feedgenerator module
130 128 """
131 129 self.load_default_context()
132 130 force_recache = self.get_recache_flag()
133 131
134 cache_namespace_uid = 'repo_feed.{}'.format(self.db_repo.repo_id)
132 cache_namespace_uid = f'repo_feed.{self.db_repo.repo_id}'
135 133 condition = not (self.path_filter.is_enabled or force_recache)
136 134 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
137 135
138 136 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
139 137 condition=condition)
140 138 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
141 139 feed = Atom1Feed(
142 140 title=self.title % 'atom',
143 141 link=h.route_url('repo_summary', repo_name=_repo_name),
144 142 description=self.description % _repo_name,
145 143 language=self.language,
146 144 ttl=self.ttl
147 145 )
148 146
149 147 for commit in reversed(self._get_commits()):
150 148 date = self._set_timezone(commit.date)
151 149 feed.add_item(
152 150 unique_id=self.uid(str(repo_id), commit.raw_id),
153 151 title=self._get_title(commit),
154 152 author_name=commit.author,
155 153 description=self._get_description(commit),
156 154 link=h.route_url(
157 155 'repo_commit', repo_name=_repo_name,
158 156 commit_id=commit.raw_id),
159 157 pubdate=date,)
160 158
161 159 return feed.content_type, feed.writeString('utf-8')
162 160
163 161 commit_id = self.db_repo.changeset_cache.get('raw_id')
164 162 content_type, feed = generate_atom_feed(
165 163 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
166 164
167 165 response = Response(feed)
168 166 response.content_type = content_type
169 167 return response
170 168
171 169 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
172 170 @HasRepoPermissionAnyDecorator(
173 171 'repository.read', 'repository.write', 'repository.admin')
174 172 def rss(self):
175 173 """
176 174 Produce an rss2 feed via feedgenerator module
177 175 """
178 176 self.load_default_context()
179 177 force_recache = self.get_recache_flag()
180 178
181 cache_namespace_uid = 'repo_feed.{}'.format(self.db_repo.repo_id)
179 cache_namespace_uid = f'repo_feed.{self.db_repo.repo_id}'
182 180 condition = not (self.path_filter.is_enabled or force_recache)
183 181 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
184 182
185 183 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
186 184 condition=condition)
187 185 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
188 186 feed = Rss201rev2Feed(
189 187 title=self.title % 'rss',
190 188 link=h.route_url('repo_summary', repo_name=_repo_name),
191 189 description=self.description % _repo_name,
192 190 language=self.language,
193 191 ttl=self.ttl
194 192 )
195 193
196 194 for commit in reversed(self._get_commits()):
197 195 date = self._set_timezone(commit.date)
198 196 feed.add_item(
199 197 unique_id=self.uid(str(repo_id), commit.raw_id),
200 198 title=self._get_title(commit),
201 199 author_name=commit.author,
202 200 description=self._get_description(commit),
203 201 link=h.route_url(
204 202 'repo_commit', repo_name=_repo_name,
205 203 commit_id=commit.raw_id),
206 204 pubdate=date,)
207 205 return feed.content_type, feed.writeString('utf-8')
208 206
209 207 commit_id = self.db_repo.changeset_cache.get('raw_id')
210 208 content_type, feed = generate_rss_feed(
211 209 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
212 210
213 211 response = Response(feed)
214 212 response.content_type = content_type
215 213 return response
@@ -1,1587 +1,1585 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import itertools
22 20 import logging
23 21 import os
24 22 import collections
25 23 import urllib.request
26 24 import urllib.parse
27 25 import urllib.error
28 26 import pathlib
29 27
30 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 29
32 30 from pyramid.renderers import render
33 31 from pyramid.response import Response
34 32
35 33 import rhodecode
36 34 from rhodecode.apps._base import RepoAppView
37 35
38 36
39 37 from rhodecode.lib import diffs, helpers as h, rc_cache
40 38 from rhodecode.lib import audit_logger
41 39 from rhodecode.lib.hash_utils import sha1_safe
42 40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
43 41 from rhodecode.lib.str_utils import safe_bytes
44 42 from rhodecode.lib.view_utils import parse_path_ref
45 43 from rhodecode.lib.exceptions import NonRelativePathError
46 44 from rhodecode.lib.codeblocks import (
47 45 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
48 46 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
49 47 from rhodecode.lib.type_utils import str2bool
50 48 from rhodecode.lib.str_utils import safe_str, safe_int
51 49 from rhodecode.lib.auth import (
52 50 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
53 51 from rhodecode.lib.vcs import path as vcspath
54 52 from rhodecode.lib.vcs.backends.base import EmptyCommit
55 53 from rhodecode.lib.vcs.conf import settings
56 54 from rhodecode.lib.vcs.nodes import FileNode
57 55 from rhodecode.lib.vcs.exceptions import (
58 56 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
59 57 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
60 58 NodeDoesNotExistError, CommitError, NodeError)
61 59
62 60 from rhodecode.model.scm import ScmModel
63 61 from rhodecode.model.db import Repository
64 62
65 63 log = logging.getLogger(__name__)
66 64
67 65
68 66 def get_archive_name(db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
69 67 # original backward compat name of archive
70 68 clean_name = safe_str(db_repo_name.replace('/', '_'))
71 69
72 70 # e.g vcsserver-sub-1-abcfdef-archive-all.zip
73 71 # vcsserver-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
74 72
75 73 sub_repo = 'sub-1' if subrepos else 'sub-0'
76 74 commit = commit_sha if with_hash else 'archive'
77 75 path_marker = (path_sha if with_hash else '') or 'all'
78 76 archive_name = f'{clean_name}-{sub_repo}-{commit}-{path_marker}{ext}'
79 77
80 78 return archive_name
81 79
82 80
83 81 def get_path_sha(at_path):
84 82 return safe_str(sha1_safe(at_path)[:8])
85 83
86 84
87 85 def _get_archive_spec(fname):
88 86 log.debug('Detecting archive spec for: `%s`', fname)
89 87
90 88 fileformat = None
91 89 ext = None
92 90 content_type = None
93 91 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
94 92
95 93 if fname.endswith(extension):
96 94 fileformat = a_type
97 95 log.debug('archive is of type: %s', fileformat)
98 96 ext = extension
99 97 break
100 98
101 99 if not fileformat:
102 100 raise ValueError()
103 101
104 102 # left over part of whole fname is the commit
105 103 commit_id = fname[:-len(ext)]
106 104
107 105 return commit_id, ext, fileformat, content_type
108 106
109 107
110 108 class RepoFilesView(RepoAppView):
111 109
112 110 @staticmethod
113 111 def adjust_file_path_for_svn(f_path, repo):
114 112 """
115 113 Computes the relative path of `f_path`.
116 114
117 115 This is mainly based on prefix matching of the recognized tags and
118 116 branches in the underlying repository.
119 117 """
120 118 tags_and_branches = itertools.chain(
121 119 repo.branches.keys(),
122 120 repo.tags.keys())
123 121 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
124 122
125 123 for name in tags_and_branches:
126 124 if f_path.startswith(f'{name}/'):
127 125 f_path = vcspath.relpath(f_path, name)
128 126 break
129 127 return f_path
130 128
131 129 def load_default_context(self):
132 130 c = self._get_local_tmpl_context(include_app_defaults=True)
133 131 c.rhodecode_repo = self.rhodecode_vcs_repo
134 132 c.enable_downloads = self.db_repo.enable_downloads
135 133 return c
136 134
137 135 def _ensure_not_locked(self, commit_id='tip'):
138 136 _ = self.request.translate
139 137
140 138 repo = self.db_repo
141 139 if repo.enable_locking and repo.locked[0]:
142 140 h.flash(_('This repository has been locked by %s on %s')
143 141 % (h.person_by_id(repo.locked[0]),
144 142 h.format_date(h.time_to_datetime(repo.locked[1]))),
145 143 'warning')
146 144 files_url = h.route_path(
147 145 'repo_files:default_path',
148 146 repo_name=self.db_repo_name, commit_id=commit_id)
149 147 raise HTTPFound(files_url)
150 148
151 149 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
152 150 _ = self.request.translate
153 151
154 152 if not is_head:
155 153 message = _('Cannot modify file. '
156 154 'Given commit `{}` is not head of a branch.').format(commit_id)
157 155 h.flash(message, category='warning')
158 156
159 157 if json_mode:
160 158 return message
161 159
162 160 files_url = h.route_path(
163 161 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
164 162 f_path=f_path)
165 163 raise HTTPFound(files_url)
166 164
167 165 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
168 166 _ = self.request.translate
169 167
170 168 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
171 169 self.db_repo_name, branch_name)
172 170 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
173 171 message = _('Branch `{}` changes forbidden by rule {}.').format(
174 172 h.escape(branch_name), h.escape(rule))
175 173 h.flash(message, 'warning')
176 174
177 175 if json_mode:
178 176 return message
179 177
180 178 files_url = h.route_path(
181 179 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
182 180
183 181 raise HTTPFound(files_url)
184 182
185 183 def _get_commit_and_path(self):
186 184 default_commit_id = self.db_repo.landing_ref_name
187 185 default_f_path = '/'
188 186
189 187 commit_id = self.request.matchdict.get(
190 188 'commit_id', default_commit_id)
191 189 f_path = self._get_f_path(self.request.matchdict, default_f_path)
192 190 return commit_id, f_path
193 191
194 192 def _get_default_encoding(self, c):
195 193 enc_list = getattr(c, 'default_encodings', [])
196 194 return enc_list[0] if enc_list else 'UTF-8'
197 195
198 196 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
199 197 """
200 198 This is a safe way to get commit. If an error occurs it redirects to
201 199 tip with proper message
202 200
203 201 :param commit_id: id of commit to fetch
204 202 :param redirect_after: toggle redirection
205 203 """
206 204 _ = self.request.translate
207 205
208 206 try:
209 207 return self.rhodecode_vcs_repo.get_commit(commit_id)
210 208 except EmptyRepositoryError:
211 209 if not redirect_after:
212 210 return None
213 211
214 212 add_new = upload_new = ""
215 213 if h.HasRepoPermissionAny(
216 214 'repository.write', 'repository.admin')(self.db_repo_name):
217 215 _url = h.route_path(
218 216 'repo_files_add_file',
219 217 repo_name=self.db_repo_name, commit_id=0, f_path='')
220 218 add_new = h.link_to(
221 219 _('add a new file'), _url, class_="alert-link")
222 220
223 221 _url_upld = h.route_path(
224 222 'repo_files_upload_file',
225 223 repo_name=self.db_repo_name, commit_id=0, f_path='')
226 224 upload_new = h.link_to(
227 225 _('upload a new file'), _url_upld, class_="alert-link")
228 226
229 227 h.flash(h.literal(
230 228 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
231 229 raise HTTPFound(
232 230 h.route_path('repo_summary', repo_name=self.db_repo_name))
233 231
234 232 except (CommitDoesNotExistError, LookupError) as e:
235 233 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
236 234 h.flash(msg, category='error')
237 235 raise HTTPNotFound()
238 236 except RepositoryError as e:
239 237 h.flash(h.escape(safe_str(e)), category='error')
240 238 raise HTTPNotFound()
241 239
242 240 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
243 241 """
244 242 Returns file_node, if error occurs or given path is directory,
245 243 it'll redirect to top level path
246 244 """
247 245 _ = self.request.translate
248 246
249 247 try:
250 248 file_node = commit_obj.get_node(path, pre_load=pre_load)
251 249 if file_node.is_dir():
252 250 raise RepositoryError('The given path is a directory')
253 251 except CommitDoesNotExistError:
254 252 log.exception('No such commit exists for this repository')
255 253 h.flash(_('No such commit exists for this repository'), category='error')
256 254 raise HTTPNotFound()
257 255 except RepositoryError as e:
258 256 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
259 257 h.flash(h.escape(safe_str(e)), category='error')
260 258 raise HTTPNotFound()
261 259
262 260 return file_node
263 261
264 262 def _is_valid_head(self, commit_id, repo, landing_ref):
265 263 branch_name = sha_commit_id = ''
266 264 is_head = False
267 265 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
268 266
269 267 for _branch_name, branch_commit_id in repo.branches.items():
270 268 # simple case we pass in branch name, it's a HEAD
271 269 if commit_id == _branch_name:
272 270 is_head = True
273 271 branch_name = _branch_name
274 272 sha_commit_id = branch_commit_id
275 273 break
276 274 # case when we pass in full sha commit_id, which is a head
277 275 elif commit_id == branch_commit_id:
278 276 is_head = True
279 277 branch_name = _branch_name
280 278 sha_commit_id = branch_commit_id
281 279 break
282 280
283 281 if h.is_svn(repo) and not repo.is_empty():
284 282 # Note: Subversion only has one head.
285 283 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
286 284 is_head = True
287 285 return branch_name, sha_commit_id, is_head
288 286
289 287 # checked branches, means we only need to try to get the branch/commit_sha
290 288 if repo.is_empty():
291 289 is_head = True
292 290 branch_name = landing_ref
293 291 sha_commit_id = EmptyCommit().raw_id
294 292 else:
295 293 commit = repo.get_commit(commit_id=commit_id)
296 294 if commit:
297 295 branch_name = commit.branch
298 296 sha_commit_id = commit.raw_id
299 297
300 298 return branch_name, sha_commit_id, is_head
301 299
302 300 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
303 301
304 302 repo_id = self.db_repo.repo_id
305 303 force_recache = self.get_recache_flag()
306 304
307 305 cache_seconds = safe_int(
308 306 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
309 307 cache_on = not force_recache and cache_seconds > 0
310 308 log.debug(
311 309 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
312 310 'with caching: %s[TTL: %ss]' % (
313 311 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
314 312
315 cache_namespace_uid = 'repo.{}'.format(repo_id)
313 cache_namespace_uid = f'repo.{repo_id}'
316 314 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
317 315
318 316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
319 317 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
320 318 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
321 319 ver, _repo_id, _commit_id, _f_path)
322 320
323 321 c.full_load = _full_load
324 322 return render(
325 323 'rhodecode:templates/files/files_browser_tree.mako',
326 324 self._get_template_context(c), self.request, _at_rev)
327 325
328 326 return compute_file_tree(
329 327 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
330 328 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
331 329
332 330 def create_pure_path(self, *parts):
333 331 # Split paths and sanitize them, removing any ../ etc
334 332 sanitized_path = [
335 333 x for x in pathlib.PurePath(*parts).parts
336 334 if x not in ['.', '..']]
337 335
338 336 pure_path = pathlib.PurePath(*sanitized_path)
339 337 return pure_path
340 338
341 339 def _is_lf_enabled(self, target_repo):
342 340 lf_enabled = False
343 341
344 342 lf_key_for_vcs_map = {
345 343 'hg': 'extensions_largefiles',
346 344 'git': 'vcs_git_lfs_enabled'
347 345 }
348 346
349 347 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
350 348
351 349 if lf_key_for_vcs:
352 350 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
353 351
354 352 return lf_enabled
355 353
356 354 @LoginRequired()
357 355 @HasRepoPermissionAnyDecorator(
358 356 'repository.read', 'repository.write', 'repository.admin')
359 357 def repo_archivefile(self):
360 358 # archive cache config
361 359 from rhodecode import CONFIG
362 360 _ = self.request.translate
363 361 self.load_default_context()
364 362 default_at_path = '/'
365 363 fname = self.request.matchdict['fname']
366 364 subrepos = self.request.GET.get('subrepos') == 'true'
367 365 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
368 366 at_path = self.request.GET.get('at_path') or default_at_path
369 367
370 368 if not self.db_repo.enable_downloads:
371 369 return Response(_('Downloads disabled'))
372 370
373 371 try:
374 372 commit_id, ext, fileformat, content_type = \
375 373 _get_archive_spec(fname)
376 374 except ValueError:
377 375 return Response(_('Unknown archive type for: `{}`').format(
378 376 h.escape(fname)))
379 377
380 378 try:
381 379 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
382 380 except CommitDoesNotExistError:
383 381 return Response(_('Unknown commit_id {}').format(
384 382 h.escape(commit_id)))
385 383 except EmptyRepositoryError:
386 384 return Response(_('Empty repository'))
387 385
388 386 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
389 387 if commit_id != commit.raw_id:
390 fname='{}{}'.format(commit.raw_id, ext)
388 fname=f'{commit.raw_id}{ext}'
391 389 raise HTTPFound(self.request.current_route_path(fname=fname))
392 390
393 391 try:
394 392 at_path = commit.get_node(at_path).path or default_at_path
395 393 except Exception:
396 394 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
397 395
398 396 path_sha = get_path_sha(at_path)
399 397
400 398 # used for cache etc, consistent unique archive name
401 399 archive_name_key = get_archive_name(
402 400 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 401 path_sha=path_sha, with_hash=True)
404 402
405 403 if not with_hash:
406 404 path_sha = ''
407 405
408 406 # what end client gets served
409 407 response_archive_name = get_archive_name(
410 408 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
411 409 path_sha=path_sha, with_hash=with_hash)
412 410
413 411 # remove extension from our archive directory name
414 412 archive_dir_name = response_archive_name[:-len(ext)]
415 413
416 414 archive_cache_disable = self.request.GET.get('no_cache')
417 415
418 416 d_cache = get_archival_cache_store(config=CONFIG)
419 417 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
420 418 d_cache_conf = get_archival_config(config=CONFIG)
421 419
422 420 reentrant_lock_key = archive_name_key + '.lock'
423 421 with ReentrantLock(d_cache, reentrant_lock_key):
424 422 # This is also a cache key
425 423 use_cached_archive = False
426 424 if archive_name_key in d_cache and not archive_cache_disable:
427 425 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
428 426 use_cached_archive = True
429 427 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
430 428 archive_name_key, tag, reader.name)
431 429 else:
432 430 reader = None
433 431 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
434 432
435 433 # generate new archive, as previous was not found in the cache
436 434 if not reader:
437 435 # first remove expired items, before generating a new one :)
438 436 # we di this manually because automatic eviction is disabled
439 437 d_cache.cull(retry=True)
440 438
441 439 try:
442 440 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 441 kind=fileformat, subrepos=subrepos,
444 442 archive_at_path=at_path, cache_config=d_cache_conf)
445 443 except ImproperArchiveTypeError:
446 444 return _('Unknown archive type')
447 445
448 446 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
449 447
450 448 if not reader:
451 449 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
452 450
453 451 def archive_iterator(_reader):
454 452 while 1:
455 453 data = _reader.read(1024)
456 454 if not data:
457 455 break
458 456 yield data
459 457
460 458 response = Response(app_iter=archive_iterator(reader))
461 459 response.content_disposition = f'attachment; filename={response_archive_name}'
462 460 response.content_type = str(content_type)
463 461
464 462 try:
465 463 return response
466 464 finally:
467 465 # store download action
468 466 audit_logger.store_web(
469 467 'repo.archive.download', action_data={
470 468 'user_agent': self.request.user_agent,
471 469 'archive_name': archive_name_key,
472 470 'archive_spec': fname,
473 471 'archive_cached': use_cached_archive},
474 472 user=self._rhodecode_user,
475 473 repo=self.db_repo,
476 474 commit=True
477 475 )
478 476
479 477 def _get_file_node(self, commit_id, f_path):
480 478 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
481 479 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
482 480 try:
483 481 node = commit.get_node(f_path)
484 482 if node.is_dir():
485 483 raise NodeError(f'{node} path is a {type(node)} not a file')
486 484 except NodeDoesNotExistError:
487 485 commit = EmptyCommit(
488 486 commit_id=commit_id,
489 487 idx=commit.idx,
490 488 repo=commit.repository,
491 489 alias=commit.repository.alias,
492 490 message=commit.message,
493 491 author=commit.author,
494 492 date=commit.date)
495 493 node = FileNode(safe_bytes(f_path), b'', commit=commit)
496 494 else:
497 495 commit = EmptyCommit(
498 496 repo=self.rhodecode_vcs_repo,
499 497 alias=self.rhodecode_vcs_repo.alias)
500 498 node = FileNode(safe_bytes(f_path), b'', commit=commit)
501 499 return node
502 500
503 501 @LoginRequired()
504 502 @HasRepoPermissionAnyDecorator(
505 503 'repository.read', 'repository.write', 'repository.admin')
506 504 def repo_files_diff(self):
507 505 c = self.load_default_context()
508 506 f_path = self._get_f_path(self.request.matchdict)
509 507 diff1 = self.request.GET.get('diff1', '')
510 508 diff2 = self.request.GET.get('diff2', '')
511 509
512 510 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
513 511
514 512 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
515 513 line_context = self.request.GET.get('context', 3)
516 514
517 515 if not any((diff1, diff2)):
518 516 h.flash(
519 517 'Need query parameter "diff1" or "diff2" to generate a diff.',
520 518 category='error')
521 519 raise HTTPBadRequest()
522 520
523 521 c.action = self.request.GET.get('diff')
524 522 if c.action not in ['download', 'raw']:
525 523 compare_url = h.route_path(
526 524 'repo_compare',
527 525 repo_name=self.db_repo_name,
528 526 source_ref_type='rev',
529 527 source_ref=diff1,
530 528 target_repo=self.db_repo_name,
531 529 target_ref_type='rev',
532 530 target_ref=diff2,
533 531 _query=dict(f_path=f_path))
534 532 # redirect to new view if we render diff
535 533 raise HTTPFound(compare_url)
536 534
537 535 try:
538 536 node1 = self._get_file_node(diff1, path1)
539 537 node2 = self._get_file_node(diff2, f_path)
540 538 except (RepositoryError, NodeError):
541 539 log.exception("Exception while trying to get node from repository")
542 540 raise HTTPFound(
543 541 h.route_path('repo_files', repo_name=self.db_repo_name,
544 542 commit_id='tip', f_path=f_path))
545 543
546 544 if all(isinstance(node.commit, EmptyCommit)
547 545 for node in (node1, node2)):
548 546 raise HTTPNotFound()
549 547
550 548 c.commit_1 = node1.commit
551 549 c.commit_2 = node2.commit
552 550
553 551 if c.action == 'download':
554 552 _diff = diffs.get_gitdiff(node1, node2,
555 553 ignore_whitespace=ignore_whitespace,
556 554 context=line_context)
557 555 # NOTE: this was using diff_format='gitdiff'
558 556 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
559 557
560 558 response = Response(self.path_filter.get_raw_patch(diff))
561 559 response.content_type = 'text/plain'
562 560 response.content_disposition = (
563 561 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
564 562 )
565 563 charset = self._get_default_encoding(c)
566 564 if charset:
567 565 response.charset = charset
568 566 return response
569 567
570 568 elif c.action == 'raw':
571 569 _diff = diffs.get_gitdiff(node1, node2,
572 570 ignore_whitespace=ignore_whitespace,
573 571 context=line_context)
574 572 # NOTE: this was using diff_format='gitdiff'
575 573 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
576 574
577 575 response = Response(self.path_filter.get_raw_patch(diff))
578 576 response.content_type = 'text/plain'
579 577 charset = self._get_default_encoding(c)
580 578 if charset:
581 579 response.charset = charset
582 580 return response
583 581
584 582 # in case we ever end up here
585 583 raise HTTPNotFound()
586 584
587 585 @LoginRequired()
588 586 @HasRepoPermissionAnyDecorator(
589 587 'repository.read', 'repository.write', 'repository.admin')
590 588 def repo_files_diff_2way_redirect(self):
591 589 """
592 590 Kept only to make OLD links work
593 591 """
594 592 f_path = self._get_f_path_unchecked(self.request.matchdict)
595 593 diff1 = self.request.GET.get('diff1', '')
596 594 diff2 = self.request.GET.get('diff2', '')
597 595
598 596 if not any((diff1, diff2)):
599 597 h.flash(
600 598 'Need query parameter "diff1" or "diff2" to generate a diff.',
601 599 category='error')
602 600 raise HTTPBadRequest()
603 601
604 602 compare_url = h.route_path(
605 603 'repo_compare',
606 604 repo_name=self.db_repo_name,
607 605 source_ref_type='rev',
608 606 source_ref=diff1,
609 607 target_ref_type='rev',
610 608 target_ref=diff2,
611 609 _query=dict(f_path=f_path, diffmode='sideside',
612 610 target_repo=self.db_repo_name,))
613 611 raise HTTPFound(compare_url)
614 612
615 613 @LoginRequired()
616 614 def repo_files_default_commit_redirect(self):
617 615 """
618 616 Special page that redirects to the landing page of files based on the default
619 617 commit for repository
620 618 """
621 619 c = self.load_default_context()
622 620 ref_name = c.rhodecode_db_repo.landing_ref_name
623 621 landing_url = h.repo_files_by_ref_url(
624 622 c.rhodecode_db_repo.repo_name,
625 623 c.rhodecode_db_repo.repo_type,
626 624 f_path='',
627 625 ref_name=ref_name,
628 626 commit_id='tip',
629 627 query=dict(at=ref_name)
630 628 )
631 629
632 630 raise HTTPFound(landing_url)
633 631
634 632 @LoginRequired()
635 633 @HasRepoPermissionAnyDecorator(
636 634 'repository.read', 'repository.write', 'repository.admin')
637 635 def repo_files(self):
638 636 c = self.load_default_context()
639 637
640 638 view_name = getattr(self.request.matched_route, 'name', None)
641 639
642 640 c.annotate = view_name == 'repo_files:annotated'
643 641 # default is false, but .rst/.md files later are auto rendered, we can
644 642 # overwrite auto rendering by setting this GET flag
645 643 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
646 644
647 645 commit_id, f_path = self._get_commit_and_path()
648 646
649 647 c.commit = self._get_commit_or_redirect(commit_id)
650 648 c.branch = self.request.GET.get('branch', None)
651 649 c.f_path = f_path
652 650 at_rev = self.request.GET.get('at')
653 651
654 652 # prev link
655 653 try:
656 654 prev_commit = c.commit.prev(c.branch)
657 655 c.prev_commit = prev_commit
658 656 c.url_prev = h.route_path(
659 657 'repo_files', repo_name=self.db_repo_name,
660 658 commit_id=prev_commit.raw_id, f_path=f_path)
661 659 if c.branch:
662 660 c.url_prev += '?branch=%s' % c.branch
663 661 except (CommitDoesNotExistError, VCSError):
664 662 c.url_prev = '#'
665 663 c.prev_commit = EmptyCommit()
666 664
667 665 # next link
668 666 try:
669 667 next_commit = c.commit.next(c.branch)
670 668 c.next_commit = next_commit
671 669 c.url_next = h.route_path(
672 670 'repo_files', repo_name=self.db_repo_name,
673 671 commit_id=next_commit.raw_id, f_path=f_path)
674 672 if c.branch:
675 673 c.url_next += '?branch=%s' % c.branch
676 674 except (CommitDoesNotExistError, VCSError):
677 675 c.url_next = '#'
678 676 c.next_commit = EmptyCommit()
679 677
680 678 # files or dirs
681 679 try:
682 680 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
683 681
684 682 c.file_author = True
685 683 c.file_tree = ''
686 684
687 685 # load file content
688 686 if c.file.is_file():
689 687 c.lf_node = {}
690 688
691 689 has_lf_enabled = self._is_lf_enabled(self.db_repo)
692 690 if has_lf_enabled:
693 691 c.lf_node = c.file.get_largefile_node()
694 692
695 693 c.file_source_page = 'true'
696 694 c.file_last_commit = c.file.last_commit
697 695
698 696 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
699 697
700 698 if not (c.file_size_too_big or c.file.is_binary):
701 699 if c.annotate: # annotation has precedence over renderer
702 700 c.annotated_lines = filenode_as_annotated_lines_tokens(
703 701 c.file
704 702 )
705 703 else:
706 704 c.renderer = (
707 705 c.renderer and h.renderer_from_filename(c.file.path)
708 706 )
709 707 if not c.renderer:
710 708 c.lines = filenode_as_lines_tokens(c.file)
711 709
712 710 _branch_name, _sha_commit_id, is_head = \
713 711 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
714 712 landing_ref=self.db_repo.landing_ref_name)
715 713 c.on_branch_head = is_head
716 714
717 715 branch = c.commit.branch if (
718 716 c.commit.branch and '/' not in c.commit.branch) else None
719 717 c.branch_or_raw_id = branch or c.commit.raw_id
720 718 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
721 719
722 720 author = c.file_last_commit.author
723 721 c.authors = [[
724 722 h.email(author),
725 723 h.person(author, 'username_or_name_or_email'),
726 724 1
727 725 ]]
728 726
729 727 else: # load tree content at path
730 728 c.file_source_page = 'false'
731 729 c.authors = []
732 730 # this loads a simple tree without metadata to speed things up
733 731 # later via ajax we call repo_nodetree_full and fetch whole
734 732 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
735 733
736 734 c.readme_data, c.readme_file = \
737 735 self._get_readme_data(self.db_repo, c.visual.default_renderer,
738 736 c.commit.raw_id, f_path)
739 737
740 738 except RepositoryError as e:
741 739 h.flash(h.escape(safe_str(e)), category='error')
742 740 raise HTTPNotFound()
743 741
744 742 if self.request.environ.get('HTTP_X_PJAX'):
745 743 html = render('rhodecode:templates/files/files_pjax.mako',
746 744 self._get_template_context(c), self.request)
747 745 else:
748 746 html = render('rhodecode:templates/files/files.mako',
749 747 self._get_template_context(c), self.request)
750 748 return Response(html)
751 749
752 750 @HasRepoPermissionAnyDecorator(
753 751 'repository.read', 'repository.write', 'repository.admin')
754 752 def repo_files_annotated_previous(self):
755 753 self.load_default_context()
756 754
757 755 commit_id, f_path = self._get_commit_and_path()
758 756 commit = self._get_commit_or_redirect(commit_id)
759 757 prev_commit_id = commit.raw_id
760 758 line_anchor = self.request.GET.get('line_anchor')
761 759 is_file = False
762 760 try:
763 761 _file = commit.get_node(f_path)
764 762 is_file = _file.is_file()
765 763 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
766 764 pass
767 765
768 766 if is_file:
769 767 history = commit.get_path_history(f_path)
770 768 prev_commit_id = history[1].raw_id \
771 769 if len(history) > 1 else prev_commit_id
772 770 prev_url = h.route_path(
773 771 'repo_files:annotated', repo_name=self.db_repo_name,
774 772 commit_id=prev_commit_id, f_path=f_path,
775 _anchor='L{}'.format(line_anchor))
773 _anchor=f'L{line_anchor}')
776 774
777 775 raise HTTPFound(prev_url)
778 776
779 777 @LoginRequired()
780 778 @HasRepoPermissionAnyDecorator(
781 779 'repository.read', 'repository.write', 'repository.admin')
782 780 def repo_nodetree_full(self):
783 781 """
784 782 Returns rendered html of file tree that contains commit date,
785 783 author, commit_id for the specified combination of
786 784 repo, commit_id and file path
787 785 """
788 786 c = self.load_default_context()
789 787
790 788 commit_id, f_path = self._get_commit_and_path()
791 789 commit = self._get_commit_or_redirect(commit_id)
792 790 try:
793 791 dir_node = commit.get_node(f_path)
794 792 except RepositoryError as e:
795 return Response('error: {}'.format(h.escape(safe_str(e))))
793 return Response(f'error: {h.escape(safe_str(e))}')
796 794
797 795 if dir_node.is_file():
798 796 return Response('')
799 797
800 798 c.file = dir_node
801 799 c.commit = commit
802 800 at_rev = self.request.GET.get('at')
803 801
804 802 html = self._get_tree_at_commit(
805 803 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
806 804
807 805 return Response(html)
808 806
809 807 def _get_attachement_headers(self, f_path):
810 808 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
811 809 safe_path = f_name.replace('"', '\\"')
812 810 encoded_path = urllib.parse.quote(f_name)
813 811
814 812 return "attachment; " \
815 813 "filename=\"{}\"; " \
816 814 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
817 815
818 816 @LoginRequired()
819 817 @HasRepoPermissionAnyDecorator(
820 818 'repository.read', 'repository.write', 'repository.admin')
821 819 def repo_file_raw(self):
822 820 """
823 821 Action for show as raw, some mimetypes are "rendered",
824 822 those include images, icons.
825 823 """
826 824 c = self.load_default_context()
827 825
828 826 commit_id, f_path = self._get_commit_and_path()
829 827 commit = self._get_commit_or_redirect(commit_id)
830 828 file_node = self._get_filenode_or_redirect(commit, f_path)
831 829
832 830 raw_mimetype_mapping = {
833 831 # map original mimetype to a mimetype used for "show as raw"
834 832 # you can also provide a content-disposition to override the
835 833 # default "attachment" disposition.
836 834 # orig_type: (new_type, new_dispo)
837 835
838 836 # show images inline:
839 837 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
840 838 # for example render an SVG with javascript inside or even render
841 839 # HTML.
842 840 'image/x-icon': ('image/x-icon', 'inline'),
843 841 'image/png': ('image/png', 'inline'),
844 842 'image/gif': ('image/gif', 'inline'),
845 843 'image/jpeg': ('image/jpeg', 'inline'),
846 844 'application/pdf': ('application/pdf', 'inline'),
847 845 }
848 846
849 847 mimetype = file_node.mimetype
850 848 try:
851 849 mimetype, disposition = raw_mimetype_mapping[mimetype]
852 850 except KeyError:
853 851 # we don't know anything special about this, handle it safely
854 852 if file_node.is_binary:
855 853 # do same as download raw for binary files
856 854 mimetype, disposition = 'application/octet-stream', 'attachment'
857 855 else:
858 856 # do not just use the original mimetype, but force text/plain,
859 857 # otherwise it would serve text/html and that might be unsafe.
860 858 # Note: underlying vcs library fakes text/plain mimetype if the
861 859 # mimetype can not be determined and it thinks it is not
862 860 # binary.This might lead to erroneous text display in some
863 861 # cases, but helps in other cases, like with text files
864 862 # without extension.
865 863 mimetype, disposition = 'text/plain', 'inline'
866 864
867 865 if disposition == 'attachment':
868 866 disposition = self._get_attachement_headers(f_path)
869 867
870 868 stream_content = file_node.stream_bytes()
871 869
872 870 response = Response(app_iter=stream_content)
873 871 response.content_disposition = disposition
874 872 response.content_type = mimetype
875 873
876 874 charset = self._get_default_encoding(c)
877 875 if charset:
878 876 response.charset = charset
879 877
880 878 return response
881 879
882 880 @LoginRequired()
883 881 @HasRepoPermissionAnyDecorator(
884 882 'repository.read', 'repository.write', 'repository.admin')
885 883 def repo_file_download(self):
886 884 c = self.load_default_context()
887 885
888 886 commit_id, f_path = self._get_commit_and_path()
889 887 commit = self._get_commit_or_redirect(commit_id)
890 888 file_node = self._get_filenode_or_redirect(commit, f_path)
891 889
892 890 if self.request.GET.get('lf'):
893 891 # only if lf get flag is passed, we download this file
894 892 # as LFS/Largefile
895 893 lf_node = file_node.get_largefile_node()
896 894 if lf_node:
897 895 # overwrite our pointer with the REAL large-file
898 896 file_node = lf_node
899 897
900 898 disposition = self._get_attachement_headers(f_path)
901 899
902 900 stream_content = file_node.stream_bytes()
903 901
904 902 response = Response(app_iter=stream_content)
905 903 response.content_disposition = disposition
906 904 response.content_type = file_node.mimetype
907 905
908 906 charset = self._get_default_encoding(c)
909 907 if charset:
910 908 response.charset = charset
911 909
912 910 return response
913 911
914 912 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
915 913
916 914 cache_seconds = safe_int(
917 915 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
918 916 cache_on = cache_seconds > 0
919 917 log.debug(
920 918 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
921 919 'with caching: %s[TTL: %ss]' % (
922 920 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
923 921
924 cache_namespace_uid = 'repo.{}'.format(repo_id)
922 cache_namespace_uid = f'repo.{repo_id}'
925 923 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
926 924
927 925 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
928 926 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
929 927 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
930 928 _repo_id, commit_id, f_path)
931 929 try:
932 930 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
933 931 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
934 932 log.exception(safe_str(e))
935 933 h.flash(h.escape(safe_str(e)), category='error')
936 934 raise HTTPFound(h.route_path(
937 935 'repo_files', repo_name=self.db_repo_name,
938 936 commit_id='tip', f_path='/'))
939 937
940 938 return _d + _f
941 939
942 940 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
943 941 commit_id, f_path)
944 942 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
945 943
946 944 @LoginRequired()
947 945 @HasRepoPermissionAnyDecorator(
948 946 'repository.read', 'repository.write', 'repository.admin')
949 947 def repo_nodelist(self):
950 948 self.load_default_context()
951 949
952 950 commit_id, f_path = self._get_commit_and_path()
953 951 commit = self._get_commit_or_redirect(commit_id)
954 952
955 953 metadata = self._get_nodelist_at_commit(
956 954 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
957 955 return {'nodes': [x for x in metadata]}
958 956
959 957 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
960 958 items = []
961 959 for name, commit_id in branches_or_tags.items():
962 960 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
963 961 items.append((sym_ref, name, ref_type))
964 962 return items
965 963
966 964 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
967 965 return commit_id
968 966
969 967 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
970 968 return commit_id
971 969
972 970 # NOTE(dan): old code we used in "diff" mode compare
973 971 new_f_path = vcspath.join(name, f_path)
974 972 return f'{new_f_path}@{commit_id}'
975 973
976 974 def _get_node_history(self, commit_obj, f_path, commits=None):
977 975 """
978 976 get commit history for given node
979 977
980 978 :param commit_obj: commit to calculate history
981 979 :param f_path: path for node to calculate history for
982 980 :param commits: if passed don't calculate history and take
983 981 commits defined in this list
984 982 """
985 983 _ = self.request.translate
986 984
987 985 # calculate history based on tip
988 986 tip = self.rhodecode_vcs_repo.get_commit()
989 987 if commits is None:
990 988 pre_load = ["author", "branch"]
991 989 try:
992 990 commits = tip.get_path_history(f_path, pre_load=pre_load)
993 991 except (NodeDoesNotExistError, CommitError):
994 992 # this node is not present at tip!
995 993 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
996 994
997 995 history = []
998 996 commits_group = ([], _("Changesets"))
999 997 for commit in commits:
1000 998 branch = ' (%s)' % commit.branch if commit.branch else ''
1001 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
999 n_desc = 'r{}:{}{}'.format(commit.idx, commit.short_id, branch)
1002 1000 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1003 1001 history.append(commits_group)
1004 1002
1005 1003 symbolic_reference = self._symbolic_reference
1006 1004
1007 1005 if self.rhodecode_vcs_repo.alias == 'svn':
1008 1006 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1009 1007 f_path, self.rhodecode_vcs_repo)
1010 1008 if adjusted_f_path != f_path:
1011 1009 log.debug(
1012 1010 'Recognized svn tag or branch in file "%s", using svn '
1013 1011 'specific symbolic references', f_path)
1014 1012 f_path = adjusted_f_path
1015 1013 symbolic_reference = self._symbolic_reference_svn
1016 1014
1017 1015 branches = self._create_references(
1018 1016 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1019 1017 branches_group = (branches, _("Branches"))
1020 1018
1021 1019 tags = self._create_references(
1022 1020 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1023 1021 tags_group = (tags, _("Tags"))
1024 1022
1025 1023 history.append(branches_group)
1026 1024 history.append(tags_group)
1027 1025
1028 1026 return history, commits
1029 1027
1030 1028 @LoginRequired()
1031 1029 @HasRepoPermissionAnyDecorator(
1032 1030 'repository.read', 'repository.write', 'repository.admin')
1033 1031 def repo_file_history(self):
1034 1032 self.load_default_context()
1035 1033
1036 1034 commit_id, f_path = self._get_commit_and_path()
1037 1035 commit = self._get_commit_or_redirect(commit_id)
1038 1036 file_node = self._get_filenode_or_redirect(commit, f_path)
1039 1037
1040 1038 if file_node.is_file():
1041 1039 file_history, _hist = self._get_node_history(commit, f_path)
1042 1040
1043 1041 res = []
1044 1042 for section_items, section in file_history:
1045 1043 items = []
1046 1044 for obj_id, obj_text, obj_type in section_items:
1047 1045 at_rev = ''
1048 1046 if obj_type in ['branch', 'bookmark', 'tag']:
1049 1047 at_rev = obj_text
1050 1048 entry = {
1051 1049 'id': obj_id,
1052 1050 'text': obj_text,
1053 1051 'type': obj_type,
1054 1052 'at_rev': at_rev
1055 1053 }
1056 1054
1057 1055 items.append(entry)
1058 1056
1059 1057 res.append({
1060 1058 'text': section,
1061 1059 'children': items
1062 1060 })
1063 1061
1064 1062 data = {
1065 1063 'more': False,
1066 1064 'results': res
1067 1065 }
1068 1066 return data
1069 1067
1070 1068 log.warning('Cannot fetch history for directory')
1071 1069 raise HTTPBadRequest()
1072 1070
1073 1071 @LoginRequired()
1074 1072 @HasRepoPermissionAnyDecorator(
1075 1073 'repository.read', 'repository.write', 'repository.admin')
1076 1074 def repo_file_authors(self):
1077 1075 c = self.load_default_context()
1078 1076
1079 1077 commit_id, f_path = self._get_commit_and_path()
1080 1078 commit = self._get_commit_or_redirect(commit_id)
1081 1079 file_node = self._get_filenode_or_redirect(commit, f_path)
1082 1080
1083 1081 if not file_node.is_file():
1084 1082 raise HTTPBadRequest()
1085 1083
1086 1084 c.file_last_commit = file_node.last_commit
1087 1085 if self.request.GET.get('annotate') == '1':
1088 1086 # use _hist from annotation if annotation mode is on
1089 commit_ids = set(x[1] for x in file_node.annotate)
1087 commit_ids = {x[1] for x in file_node.annotate}
1090 1088 _hist = (
1091 1089 self.rhodecode_vcs_repo.get_commit(commit_id)
1092 1090 for commit_id in commit_ids)
1093 1091 else:
1094 1092 _f_history, _hist = self._get_node_history(commit, f_path)
1095 1093 c.file_author = False
1096 1094
1097 1095 unique = collections.OrderedDict()
1098 1096 for commit in _hist:
1099 1097 author = commit.author
1100 1098 if author not in unique:
1101 1099 unique[commit.author] = [
1102 1100 h.email(author),
1103 1101 h.person(author, 'username_or_name_or_email'),
1104 1102 1 # counter
1105 1103 ]
1106 1104
1107 1105 else:
1108 1106 # increase counter
1109 1107 unique[commit.author][2] += 1
1110 1108
1111 1109 c.authors = [val for val in unique.values()]
1112 1110
1113 1111 return self._get_template_context(c)
1114 1112
1115 1113 @LoginRequired()
1116 1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1117 1115 def repo_files_check_head(self):
1118 1116 self.load_default_context()
1119 1117
1120 1118 commit_id, f_path = self._get_commit_and_path()
1121 1119 _branch_name, _sha_commit_id, is_head = \
1122 1120 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1123 1121 landing_ref=self.db_repo.landing_ref_name)
1124 1122
1125 1123 new_path = self.request.POST.get('path')
1126 1124 operation = self.request.POST.get('operation')
1127 1125 path_exist = ''
1128 1126
1129 1127 if new_path and operation in ['create', 'upload']:
1130 1128 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1131 1129 try:
1132 1130 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1133 1131 # NOTE(dan): construct whole path without leading /
1134 1132 file_node = commit_obj.get_node(new_f_path)
1135 1133 if file_node is not None:
1136 1134 path_exist = new_f_path
1137 1135 except EmptyRepositoryError:
1138 1136 pass
1139 1137 except Exception:
1140 1138 pass
1141 1139
1142 1140 return {
1143 1141 'branch': _branch_name,
1144 1142 'sha': _sha_commit_id,
1145 1143 'is_head': is_head,
1146 1144 'path_exists': path_exist
1147 1145 }
1148 1146
1149 1147 @LoginRequired()
1150 1148 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 1149 def repo_files_remove_file(self):
1152 1150 _ = self.request.translate
1153 1151 c = self.load_default_context()
1154 1152 commit_id, f_path = self._get_commit_and_path()
1155 1153
1156 1154 self._ensure_not_locked()
1157 1155 _branch_name, _sha_commit_id, is_head = \
1158 1156 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1159 1157 landing_ref=self.db_repo.landing_ref_name)
1160 1158
1161 1159 self.forbid_non_head(is_head, f_path)
1162 1160 self.check_branch_permission(_branch_name)
1163 1161
1164 1162 c.commit = self._get_commit_or_redirect(commit_id)
1165 1163 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1166 1164
1167 1165 c.default_message = _(
1168 1166 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1169 1167 c.f_path = f_path
1170 1168
1171 1169 return self._get_template_context(c)
1172 1170
1173 1171 @LoginRequired()
1174 1172 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1175 1173 @CSRFRequired()
1176 1174 def repo_files_delete_file(self):
1177 1175 _ = self.request.translate
1178 1176
1179 1177 c = self.load_default_context()
1180 1178 commit_id, f_path = self._get_commit_and_path()
1181 1179
1182 1180 self._ensure_not_locked()
1183 1181 _branch_name, _sha_commit_id, is_head = \
1184 1182 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1185 1183 landing_ref=self.db_repo.landing_ref_name)
1186 1184
1187 1185 self.forbid_non_head(is_head, f_path)
1188 1186 self.check_branch_permission(_branch_name)
1189 1187
1190 1188 c.commit = self._get_commit_or_redirect(commit_id)
1191 1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1192 1190
1193 1191 c.default_message = _(
1194 1192 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1195 1193 c.f_path = f_path
1196 1194 node_path = f_path
1197 1195 author = self._rhodecode_db_user.full_contact
1198 1196 message = self.request.POST.get('message') or c.default_message
1199 1197 try:
1200 1198 nodes = {
1201 1199 safe_bytes(node_path): {
1202 1200 'content': b''
1203 1201 }
1204 1202 }
1205 1203 ScmModel().delete_nodes(
1206 1204 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1207 1205 message=message,
1208 1206 nodes=nodes,
1209 1207 parent_commit=c.commit,
1210 1208 author=author,
1211 1209 )
1212 1210
1213 1211 h.flash(
1214 1212 _('Successfully deleted file `{}`').format(
1215 1213 h.escape(f_path)), category='success')
1216 1214 except Exception:
1217 1215 log.exception('Error during commit operation')
1218 1216 h.flash(_('Error occurred during commit'), category='error')
1219 1217 raise HTTPFound(
1220 1218 h.route_path('repo_commit', repo_name=self.db_repo_name,
1221 1219 commit_id='tip'))
1222 1220
1223 1221 @LoginRequired()
1224 1222 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1225 1223 def repo_files_edit_file(self):
1226 1224 _ = self.request.translate
1227 1225 c = self.load_default_context()
1228 1226 commit_id, f_path = self._get_commit_and_path()
1229 1227
1230 1228 self._ensure_not_locked()
1231 1229 _branch_name, _sha_commit_id, is_head = \
1232 1230 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1233 1231 landing_ref=self.db_repo.landing_ref_name)
1234 1232
1235 1233 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1236 1234 self.check_branch_permission(_branch_name, commit_id=commit_id)
1237 1235
1238 1236 c.commit = self._get_commit_or_redirect(commit_id)
1239 1237 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1240 1238
1241 1239 if c.file.is_binary:
1242 1240 files_url = h.route_path(
1243 1241 'repo_files',
1244 1242 repo_name=self.db_repo_name,
1245 1243 commit_id=c.commit.raw_id, f_path=f_path)
1246 1244 raise HTTPFound(files_url)
1247 1245
1248 1246 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1249 1247 c.f_path = f_path
1250 1248
1251 1249 return self._get_template_context(c)
1252 1250
1253 1251 @LoginRequired()
1254 1252 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1255 1253 @CSRFRequired()
1256 1254 def repo_files_update_file(self):
1257 1255 _ = self.request.translate
1258 1256 c = self.load_default_context()
1259 1257 commit_id, f_path = self._get_commit_and_path()
1260 1258
1261 1259 self._ensure_not_locked()
1262 1260
1263 1261 c.commit = self._get_commit_or_redirect(commit_id)
1264 1262 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1265 1263
1266 1264 if c.file.is_binary:
1267 1265 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1268 1266 commit_id=c.commit.raw_id, f_path=f_path))
1269 1267
1270 1268 _branch_name, _sha_commit_id, is_head = \
1271 1269 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1272 1270 landing_ref=self.db_repo.landing_ref_name)
1273 1271
1274 1272 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1275 1273 self.check_branch_permission(_branch_name, commit_id=commit_id)
1276 1274
1277 1275 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1278 1276 c.f_path = f_path
1279 1277
1280 1278 old_content = c.file.str_content
1281 1279 sl = old_content.splitlines(1)
1282 1280 first_line = sl[0] if sl else ''
1283 1281
1284 1282 r_post = self.request.POST
1285 1283 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1286 1284 line_ending_mode = detect_mode(first_line, 0)
1287 1285 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1288 1286
1289 1287 message = r_post.get('message') or c.default_message
1290 1288
1291 1289 org_node_path = c.file.str_path
1292 1290 filename = r_post['filename']
1293 1291
1294 1292 root_path = c.file.dir_path
1295 1293 pure_path = self.create_pure_path(root_path, filename)
1296 1294 node_path = pure_path.as_posix()
1297 1295
1298 1296 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1299 1297 commit_id=commit_id)
1300 1298 if content == old_content and node_path == org_node_path:
1301 1299 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1302 1300 category='warning')
1303 1301 raise HTTPFound(default_redirect_url)
1304 1302
1305 1303 try:
1306 1304 mapping = {
1307 1305 c.file.bytes_path: {
1308 1306 'org_filename': org_node_path,
1309 1307 'filename': safe_bytes(node_path),
1310 1308 'content': safe_bytes(content),
1311 1309 'lexer': '',
1312 1310 'op': 'mod',
1313 1311 'mode': c.file.mode
1314 1312 }
1315 1313 }
1316 1314
1317 1315 commit = ScmModel().update_nodes(
1318 1316 user=self._rhodecode_db_user.user_id,
1319 1317 repo=self.db_repo,
1320 1318 message=message,
1321 1319 nodes=mapping,
1322 1320 parent_commit=c.commit,
1323 1321 )
1324 1322
1325 1323 h.flash(_('Successfully committed changes to file `{}`').format(
1326 1324 h.escape(f_path)), category='success')
1327 1325 default_redirect_url = h.route_path(
1328 1326 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1329 1327
1330 1328 except Exception:
1331 1329 log.exception('Error occurred during commit')
1332 1330 h.flash(_('Error occurred during commit'), category='error')
1333 1331
1334 1332 raise HTTPFound(default_redirect_url)
1335 1333
1336 1334 @LoginRequired()
1337 1335 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1338 1336 def repo_files_add_file(self):
1339 1337 _ = self.request.translate
1340 1338 c = self.load_default_context()
1341 1339 commit_id, f_path = self._get_commit_and_path()
1342 1340
1343 1341 self._ensure_not_locked()
1344 1342
1345 1343 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1346 1344 if c.commit is None:
1347 1345 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1348 1346
1349 1347 if self.rhodecode_vcs_repo.is_empty():
1350 1348 # for empty repository we cannot check for current branch, we rely on
1351 1349 # c.commit.branch instead
1352 1350 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1353 1351 else:
1354 1352 _branch_name, _sha_commit_id, is_head = \
1355 1353 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1356 1354 landing_ref=self.db_repo.landing_ref_name)
1357 1355
1358 1356 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1359 1357 self.check_branch_permission(_branch_name, commit_id=commit_id)
1360 1358
1361 1359 c.default_message = (_('Added file via RhodeCode Enterprise'))
1362 1360 c.f_path = f_path.lstrip('/') # ensure not relative path
1363 1361
1364 1362 return self._get_template_context(c)
1365 1363
1366 1364 @LoginRequired()
1367 1365 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1368 1366 @CSRFRequired()
1369 1367 def repo_files_create_file(self):
1370 1368 _ = self.request.translate
1371 1369 c = self.load_default_context()
1372 1370 commit_id, f_path = self._get_commit_and_path()
1373 1371
1374 1372 self._ensure_not_locked()
1375 1373
1376 1374 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1377 1375 if c.commit is None:
1378 1376 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1379 1377
1380 1378 # calculate redirect URL
1381 1379 if self.rhodecode_vcs_repo.is_empty():
1382 1380 default_redirect_url = h.route_path(
1383 1381 'repo_summary', repo_name=self.db_repo_name)
1384 1382 else:
1385 1383 default_redirect_url = h.route_path(
1386 1384 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1387 1385
1388 1386 if self.rhodecode_vcs_repo.is_empty():
1389 1387 # for empty repository we cannot check for current branch, we rely on
1390 1388 # c.commit.branch instead
1391 1389 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1392 1390 else:
1393 1391 _branch_name, _sha_commit_id, is_head = \
1394 1392 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1395 1393 landing_ref=self.db_repo.landing_ref_name)
1396 1394
1397 1395 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1398 1396 self.check_branch_permission(_branch_name, commit_id=commit_id)
1399 1397
1400 1398 c.default_message = (_('Added file via RhodeCode Enterprise'))
1401 1399 c.f_path = f_path
1402 1400
1403 1401 r_post = self.request.POST
1404 1402 message = r_post.get('message') or c.default_message
1405 1403 filename = r_post.get('filename')
1406 1404 unix_mode = 0
1407 1405
1408 1406 if not filename:
1409 1407 # If there's no commit, redirect to repo summary
1410 1408 if type(c.commit) is EmptyCommit:
1411 1409 redirect_url = h.route_path(
1412 1410 'repo_summary', repo_name=self.db_repo_name)
1413 1411 else:
1414 1412 redirect_url = default_redirect_url
1415 1413 h.flash(_('No filename specified'), category='warning')
1416 1414 raise HTTPFound(redirect_url)
1417 1415
1418 1416 root_path = f_path
1419 1417 pure_path = self.create_pure_path(root_path, filename)
1420 1418 node_path = pure_path.as_posix().lstrip('/')
1421 1419
1422 1420 author = self._rhodecode_db_user.full_contact
1423 1421 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1424 1422 nodes = {
1425 1423 safe_bytes(node_path): {
1426 1424 'content': safe_bytes(content)
1427 1425 }
1428 1426 }
1429 1427
1430 1428 try:
1431 1429
1432 1430 commit = ScmModel().create_nodes(
1433 1431 user=self._rhodecode_db_user.user_id,
1434 1432 repo=self.db_repo,
1435 1433 message=message,
1436 1434 nodes=nodes,
1437 1435 parent_commit=c.commit,
1438 1436 author=author,
1439 1437 )
1440 1438
1441 1439 h.flash(_('Successfully committed new file `{}`').format(
1442 1440 h.escape(node_path)), category='success')
1443 1441
1444 1442 default_redirect_url = h.route_path(
1445 1443 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1446 1444
1447 1445 except NonRelativePathError:
1448 1446 log.exception('Non Relative path found')
1449 1447 h.flash(_('The location specified must be a relative path and must not '
1450 1448 'contain .. in the path'), category='warning')
1451 1449 raise HTTPFound(default_redirect_url)
1452 1450 except (NodeError, NodeAlreadyExistsError) as e:
1453 1451 h.flash(h.escape(safe_str(e)), category='error')
1454 1452 except Exception:
1455 1453 log.exception('Error occurred during commit')
1456 1454 h.flash(_('Error occurred during commit'), category='error')
1457 1455
1458 1456 raise HTTPFound(default_redirect_url)
1459 1457
1460 1458 @LoginRequired()
1461 1459 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1462 1460 @CSRFRequired()
1463 1461 def repo_files_upload_file(self):
1464 1462 _ = self.request.translate
1465 1463 c = self.load_default_context()
1466 1464 commit_id, f_path = self._get_commit_and_path()
1467 1465
1468 1466 self._ensure_not_locked()
1469 1467
1470 1468 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1471 1469 if c.commit is None:
1472 1470 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1473 1471
1474 1472 # calculate redirect URL
1475 1473 if self.rhodecode_vcs_repo.is_empty():
1476 1474 default_redirect_url = h.route_path(
1477 1475 'repo_summary', repo_name=self.db_repo_name)
1478 1476 else:
1479 1477 default_redirect_url = h.route_path(
1480 1478 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1481 1479
1482 1480 if self.rhodecode_vcs_repo.is_empty():
1483 1481 # for empty repository we cannot check for current branch, we rely on
1484 1482 # c.commit.branch instead
1485 1483 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1486 1484 else:
1487 1485 _branch_name, _sha_commit_id, is_head = \
1488 1486 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1489 1487 landing_ref=self.db_repo.landing_ref_name)
1490 1488
1491 1489 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1492 1490 if error:
1493 1491 return {
1494 1492 'error': error,
1495 1493 'redirect_url': default_redirect_url
1496 1494 }
1497 1495 error = self.check_branch_permission(_branch_name, json_mode=True)
1498 1496 if error:
1499 1497 return {
1500 1498 'error': error,
1501 1499 'redirect_url': default_redirect_url
1502 1500 }
1503 1501
1504 1502 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1505 1503 c.f_path = f_path
1506 1504
1507 1505 r_post = self.request.POST
1508 1506
1509 1507 message = c.default_message
1510 1508 user_message = r_post.getall('message')
1511 1509 if isinstance(user_message, list) and user_message:
1512 1510 # we take the first from duplicated results if it's not empty
1513 1511 message = user_message[0] if user_message[0] else message
1514 1512
1515 1513 nodes = {}
1516 1514
1517 1515 for file_obj in r_post.getall('files_upload') or []:
1518 1516 content = file_obj.file
1519 1517 filename = file_obj.filename
1520 1518
1521 1519 root_path = f_path
1522 1520 pure_path = self.create_pure_path(root_path, filename)
1523 1521 node_path = pure_path.as_posix().lstrip('/')
1524 1522
1525 1523 nodes[safe_bytes(node_path)] = {
1526 1524 'content': content
1527 1525 }
1528 1526
1529 1527 if not nodes:
1530 1528 error = 'missing files'
1531 1529 return {
1532 1530 'error': error,
1533 1531 'redirect_url': default_redirect_url
1534 1532 }
1535 1533
1536 1534 author = self._rhodecode_db_user.full_contact
1537 1535
1538 1536 try:
1539 1537 commit = ScmModel().create_nodes(
1540 1538 user=self._rhodecode_db_user.user_id,
1541 1539 repo=self.db_repo,
1542 1540 message=message,
1543 1541 nodes=nodes,
1544 1542 parent_commit=c.commit,
1545 1543 author=author,
1546 1544 )
1547 1545 if len(nodes) == 1:
1548 1546 flash_message = _('Successfully committed {} new files').format(len(nodes))
1549 1547 else:
1550 1548 flash_message = _('Successfully committed 1 new file')
1551 1549
1552 1550 h.flash(flash_message, category='success')
1553 1551
1554 1552 default_redirect_url = h.route_path(
1555 1553 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1556 1554
1557 1555 except NonRelativePathError:
1558 1556 log.exception('Non Relative path found')
1559 1557 error = _('The location specified must be a relative path and must not '
1560 1558 'contain .. in the path')
1561 1559 h.flash(error, category='warning')
1562 1560
1563 1561 return {
1564 1562 'error': error,
1565 1563 'redirect_url': default_redirect_url
1566 1564 }
1567 1565 except (NodeError, NodeAlreadyExistsError) as e:
1568 1566 error = h.escape(e)
1569 1567 h.flash(error, category='error')
1570 1568
1571 1569 return {
1572 1570 'error': error,
1573 1571 'redirect_url': default_redirect_url
1574 1572 }
1575 1573 except Exception:
1576 1574 log.exception('Error occurred during commit')
1577 1575 error = _('Error occurred during commit')
1578 1576 h.flash(error, category='error')
1579 1577 return {
1580 1578 'error': error,
1581 1579 'redirect_url': default_redirect_url
1582 1580 }
1583 1581
1584 1582 return {
1585 1583 'error': None,
1586 1584 'redirect_url': default_redirect_url
1587 1585 }
@@ -1,254 +1,252 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20 import datetime
23 21 import formencode
24 22 import formencode.htmlfill
25 23
26 24 from pyramid.httpexceptions import HTTPFound
27 25
28 26 from pyramid.renderers import render
29 27 from pyramid.response import Response
30 28
31 29 from rhodecode import events
32 30 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 31 from rhodecode.lib.auth import (
34 32 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 33 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 34 import rhodecode.lib.helpers as h
37 35 from rhodecode.lib.str_utils import safe_str
38 36 from rhodecode.lib.celerylib.utils import get_task_id
39 37 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
40 38 from rhodecode.model.permission import PermissionModel
41 39 from rhodecode.model.repo import RepoModel
42 40 from rhodecode.model.forms import RepoForkForm
43 41 from rhodecode.model.scm import ScmModel, RepoGroupList
44 42
45 43 log = logging.getLogger(__name__)
46 44
47 45
48 46 class RepoForksView(RepoAppView, DataGridAppView):
49 47
50 48 def load_default_context(self):
51 49 c = self._get_local_tmpl_context(include_app_defaults=True)
52 50 c.rhodecode_repo = self.rhodecode_vcs_repo
53 51
54 52 acl_groups = RepoGroupList(
55 53 RepoGroup.query().all(),
56 54 perm_set=['group.write', 'group.admin'])
57 55 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 56 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
59 57
60 58 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61 59
62 60 return c
63 61
64 62 @LoginRequired()
65 63 @HasRepoPermissionAnyDecorator(
66 64 'repository.read', 'repository.write', 'repository.admin')
67 65 def repo_forks_show_all(self):
68 66 c = self.load_default_context()
69 67 return self._get_template_context(c)
70 68
71 69 @LoginRequired()
72 70 @HasRepoPermissionAnyDecorator(
73 71 'repository.read', 'repository.write', 'repository.admin')
74 72 def repo_forks_data(self):
75 73 _ = self.request.translate
76 74 self.load_default_context()
77 75 column_map = {
78 76 'fork_name': 'repo_name',
79 77 'fork_date': 'created_on',
80 78 'last_activity': 'updated_on'
81 79 }
82 80 draw, start, limit = self._extract_chunk(self.request)
83 81 search_q, order_by, order_dir = self._extract_ordering(
84 82 self.request, column_map=column_map)
85 83
86 84 acl_check = HasRepoPermissionAny(
87 85 'repository.read', 'repository.write', 'repository.admin')
88 86 repo_id = self.db_repo.repo_id
89 87 allowed_ids = [-1]
90 88 for f in Repository.query().filter(Repository.fork_id == repo_id):
91 89 if acl_check(f.repo_name, 'get forks check'):
92 90 allowed_ids.append(f.repo_id)
93 91
94 92 forks_data_total_count = Repository.query()\
95 93 .filter(Repository.fork_id == repo_id)\
96 94 .filter(Repository.repo_id.in_(allowed_ids))\
97 95 .count()
98 96
99 97 # json generate
100 98 base_q = Repository.query()\
101 99 .filter(Repository.fork_id == repo_id)\
102 100 .filter(Repository.repo_id.in_(allowed_ids))\
103 101
104 102 if search_q:
105 like_expression = u'%{}%'.format(safe_str(search_q))
103 like_expression = f'%{safe_str(search_q)}%'
106 104 base_q = base_q.filter(or_(
107 105 Repository.repo_name.ilike(like_expression),
108 106 Repository.description.ilike(like_expression),
109 107 ))
110 108
111 109 forks_data_total_filtered_count = base_q.count()
112 110
113 111 sort_col = getattr(Repository, order_by, None)
114 112 if sort_col:
115 113 if order_dir == 'asc':
116 114 # handle null values properly to order by NULL last
117 115 if order_by in ['last_activity']:
118 116 sort_col = coalesce(sort_col, datetime.date.max)
119 117 sort_col = sort_col.asc()
120 118 else:
121 119 # handle null values properly to order by NULL last
122 120 if order_by in ['last_activity']:
123 121 sort_col = coalesce(sort_col, datetime.date.min)
124 122 sort_col = sort_col.desc()
125 123
126 124 base_q = base_q.order_by(sort_col)
127 125 base_q = base_q.offset(start).limit(limit)
128 126
129 127 fork_list = base_q.all()
130 128
131 129 def fork_actions(fork):
132 130 url_link = h.route_path(
133 131 'repo_compare',
134 132 repo_name=fork.repo_name,
135 133 source_ref_type=self.db_repo.landing_ref_type,
136 134 source_ref=self.db_repo.landing_ref_name,
137 135 target_ref_type=self.db_repo.landing_ref_type,
138 136 target_ref=self.db_repo.landing_ref_name,
139 137 _query=dict(merge=1, target_repo=f.repo_name))
140 138 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
141 139
142 140 def fork_name(fork):
143 141 return h.link_to(fork.repo_name,
144 142 h.route_path('repo_summary', repo_name=fork.repo_name))
145 143
146 144 forks_data = []
147 145 for fork in fork_list:
148 146 forks_data.append({
149 147 "username": h.gravatar_with_user(self.request, fork.user.username),
150 148 "fork_name": fork_name(fork),
151 149 "description": fork.description_safe,
152 150 "fork_date": h.age_component(fork.created_on, time_is_local=True),
153 151 "last_activity": h.format_date(fork.updated_on),
154 152 "action": fork_actions(fork),
155 153 })
156 154
157 155 data = ({
158 156 'draw': draw,
159 157 'data': forks_data,
160 158 'recordsTotal': forks_data_total_count,
161 159 'recordsFiltered': forks_data_total_filtered_count,
162 160 })
163 161
164 162 return data
165 163
166 164 @LoginRequired()
167 165 @NotAnonymous()
168 166 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
169 167 @HasRepoPermissionAnyDecorator(
170 168 'repository.read', 'repository.write', 'repository.admin')
171 169 def repo_fork_new(self):
172 170 c = self.load_default_context()
173 171
174 172 defaults = RepoModel()._get_defaults(self.db_repo_name)
175 173 # alter the description to indicate a fork
176 174 defaults['description'] = (
177 'fork of repository: %s \n%s' % (
175 'fork of repository: {} \n{}'.format(
178 176 defaults['repo_name'], defaults['description']))
179 177 # add suffix to fork
180 178 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
181 179
182 180 data = render('rhodecode:templates/forks/fork.mako',
183 181 self._get_template_context(c), self.request)
184 182 html = formencode.htmlfill.render(
185 183 data,
186 184 defaults=defaults,
187 185 encoding="UTF-8",
188 186 force_defaults=False
189 187 )
190 188 return Response(html)
191 189
192 190 @LoginRequired()
193 191 @NotAnonymous()
194 192 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
195 193 @HasRepoPermissionAnyDecorator(
196 194 'repository.read', 'repository.write', 'repository.admin')
197 195 @CSRFRequired()
198 196 def repo_fork_create(self):
199 197 _ = self.request.translate
200 198 c = self.load_default_context()
201 199
202 200 _form = RepoForkForm(self.request.translate,
203 201 old_data={'repo_type': self.db_repo.repo_type},
204 202 repo_groups=c.repo_groups_choices)()
205 203 post_data = dict(self.request.POST)
206 204
207 205 # forbid injecting other repo by forging a request
208 206 post_data['fork_parent_id'] = self.db_repo.repo_id
209 207 post_data['landing_rev'] = self.db_repo._landing_revision
210 208
211 209 form_result = {}
212 210 task_id = None
213 211 try:
214 212 form_result = _form.to_python(post_data)
215 213 copy_permissions = form_result.get('copy_permissions')
216 214 # create fork is done sometimes async on celery, db transaction
217 215 # management is handled there.
218 216 task = RepoModel().create_fork(
219 217 form_result, c.rhodecode_user.user_id)
220 218
221 219 task_id = get_task_id(task)
222 220 except formencode.Invalid as errors:
223 221 c.rhodecode_db_repo = self.db_repo
224 222
225 223 data = render('rhodecode:templates/forks/fork.mako',
226 224 self._get_template_context(c), self.request)
227 225 html = formencode.htmlfill.render(
228 226 data,
229 227 defaults=errors.value,
230 228 errors=errors.error_dict or {},
231 229 prefix_error=False,
232 230 encoding="UTF-8",
233 231 force_defaults=False
234 232 )
235 233 return Response(html)
236 234 except Exception:
237 235 log.exception(
238 u'Exception while trying to fork the repository %s', self.db_repo_name)
236 'Exception while trying to fork the repository %s', self.db_repo_name)
239 237 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
240 238 h.flash(msg, category='error')
241 239 raise HTTPFound(h.route_path('home'))
242 240
243 241 repo_name = form_result.get('repo_name_full', self.db_repo_name)
244 242
245 243 affected_user_ids = [self._rhodecode_user.user_id]
246 244 if copy_permissions:
247 245 # permission flush is done in repo creating
248 246 pass
249 247
250 248 PermissionModel().trigger_permission_flush(affected_user_ids)
251 249
252 250 raise HTTPFound(
253 251 h.route_path('repo_creating', repo_name=repo_name,
254 252 _query=dict(task_id=task_id)))
@@ -1,56 +1,54 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21
24 22
25 23 from rhodecode.apps._base import RepoAppView
26 24 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 25 from rhodecode.lib import repo_maintenance
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 class RepoMaintenanceView(RepoAppView):
33 31 def load_default_context(self):
34 32 c = self._get_local_tmpl_context()
35 33 return c
36 34
37 35 @LoginRequired()
38 36 @HasRepoPermissionAnyDecorator('repository.admin')
39 37 def repo_maintenance(self):
40 38 c = self.load_default_context()
41 39 c.active = 'maintenance'
42 40 maintenance = repo_maintenance.RepoMaintenance()
43 41 c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo)
44 42 return self._get_template_context(c)
45 43
46 44 @LoginRequired()
47 45 @HasRepoPermissionAnyDecorator('repository.admin')
48 46 def repo_maintenance_execute(self):
49 47 c = self.load_default_context()
50 48 c.active = 'maintenance'
51 49 _ = self.request.translate
52 50
53 51 maintenance = repo_maintenance.RepoMaintenance()
54 52 executed_types = maintenance.execute(self.db_repo)
55 53
56 54 return executed_types
@@ -1,130 +1,128 b''
1
2
3 1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 import logging
22 20
23 21 from pyramid.httpexceptions import HTTPFound
24 22
25 23 from rhodecode.apps._base import RepoAppView
26 24 from rhodecode.lib import helpers as h
27 25 from rhodecode.lib import audit_logger
28 26 from rhodecode.lib.auth import (
29 27 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 28 from rhodecode.lib.utils2 import str2bool
31 29 from rhodecode.model.db import User
32 30 from rhodecode.model.forms import RepoPermsForm
33 31 from rhodecode.model.meta import Session
34 32 from rhodecode.model.permission import PermissionModel
35 33 from rhodecode.model.repo import RepoModel
36 34
37 35 log = logging.getLogger(__name__)
38 36
39 37
40 38 class RepoSettingsPermissionsView(RepoAppView):
41 39
42 40 def load_default_context(self):
43 41 c = self._get_local_tmpl_context()
44 42 return c
45 43
46 44 @LoginRequired()
47 45 @HasRepoPermissionAnyDecorator('repository.admin')
48 46 def edit_permissions(self):
49 47 _ = self.request.translate
50 48 c = self.load_default_context()
51 49 c.active = 'permissions'
52 50 if self.request.GET.get('branch_permissions'):
53 51 h.flash(_('Explicitly add user or user group with write or higher '
54 52 'permission to modify their branch permissions.'),
55 53 category='notice')
56 54 return self._get_template_context(c)
57 55
58 56 @LoginRequired()
59 57 @HasRepoPermissionAnyDecorator('repository.admin')
60 58 @CSRFRequired()
61 59 def edit_permissions_update(self):
62 60 _ = self.request.translate
63 61 c = self.load_default_context()
64 62 c.active = 'permissions'
65 63 data = self.request.POST
66 64 # store private flag outside of HTML to verify if we can modify
67 65 # default user permissions, prevents submission of FAKE post data
68 66 # into the form for private repos
69 67 data['repo_private'] = self.db_repo.private
70 68 form = RepoPermsForm(self.request.translate)().to_python(data)
71 69 changes = RepoModel().update_permissions(
72 70 self.db_repo_name, form['perm_additions'], form['perm_updates'],
73 71 form['perm_deletions'])
74 72
75 73 action_data = {
76 74 'added': changes['added'],
77 75 'updated': changes['updated'],
78 76 'deleted': changes['deleted'],
79 77 }
80 78 audit_logger.store_web(
81 79 'repo.edit.permissions', action_data=action_data,
82 80 user=self._rhodecode_user, repo=self.db_repo)
83 81
84 82 Session().commit()
85 83 h.flash(_('Repository access permissions updated'), category='success')
86 84
87 85 affected_user_ids = None
88 86 if changes.get('default_user_changed', False):
89 87 # if we change the default user, we need to flush everyone permissions
90 88 affected_user_ids = User.get_all_user_ids()
91 89 PermissionModel().flush_user_permission_caches(
92 90 changes, affected_user_ids=affected_user_ids)
93 91
94 92 raise HTTPFound(
95 93 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
96 94
97 95 @LoginRequired()
98 96 @HasRepoPermissionAnyDecorator('repository.admin')
99 97 @CSRFRequired()
100 98 def edit_permissions_set_private_repo(self):
101 99 _ = self.request.translate
102 100 self.load_default_context()
103 101
104 102 private_flag = str2bool(self.request.POST.get('private'))
105 103
106 104 try:
107 105 repo = RepoModel().get(self.db_repo.repo_id)
108 106 repo.private = private_flag
109 107 Session().add(repo)
110 108 RepoModel().grant_user_permission(
111 109 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
112 110 )
113 111
114 112 Session().commit()
115 113
116 114 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
117 115 category='success')
118 116 # NOTE(dan): we change repo private mode we need to notify all USERS
119 117 affected_user_ids = User.get_all_user_ids()
120 118 PermissionModel().trigger_permission_flush(affected_user_ids)
121 119
122 120 except Exception:
123 121 log.exception("Exception during update of repository")
124 122 h.flash(_('Error occurred during update of repository {}').format(
125 123 self.db_repo_name), category='error')
126 124
127 125 return {
128 126 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
129 127 'private': private_flag
130 128 }
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now