##// END OF EJS Templates
apps: modernize for python3
super-admin -
r5093:525812a8 default
parent child Browse files
Show More
@@ -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,1876 +1,1874 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 collections
23 21
24 22 import formencode
25 23 import formencode.htmlfill
26 24 import peppercorn
27 25 from pyramid.httpexceptions import (
28 26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 27
30 28 from pyramid.renderers import render
31 29
32 30 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 31
34 32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 33 from rhodecode.lib.base import vcs_operation_context
36 34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 35 from rhodecode.lib.exceptions import CommentVersionMismatch
38 36 from rhodecode.lib import ext_json
39 37 from rhodecode.lib.auth import (
40 38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 39 NotAnonymous, CSRFRequired)
42 40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
43 41 from rhodecode.lib.vcs.backends.base import (
44 42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 43 from rhodecode.lib.vcs.exceptions import (
46 44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 45 from rhodecode.model.changeset_status import ChangesetStatusModel
48 46 from rhodecode.model.comment import CommentsModel
49 47 from rhodecode.model.db import (
50 48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 49 PullRequestReviewers)
52 50 from rhodecode.model.forms import PullRequestForm
53 51 from rhodecode.model.meta import Session
54 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 53 from rhodecode.model.scm import ScmModel
56 54
57 55 log = logging.getLogger(__name__)
58 56
59 57
60 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 59
62 60 def load_default_context(self):
63 61 c = self._get_local_tmpl_context(include_app_defaults=True)
64 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 64 # backward compat., we use for OLD PRs a plain renderer
67 65 c.renderer = 'plain'
68 66 return c
69 67
70 68 def _get_pull_requests_list(
71 69 self, repo_name, source, filter_type, opened_by, statuses):
72 70
73 71 draw, start, limit = self._extract_chunk(self.request)
74 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 73 _render = self.request.get_partial_renderer(
76 74 'rhodecode:templates/data_table/_dt_elements.mako')
77 75
78 76 # pagination
79 77
80 78 if filter_type == 'awaiting_review':
81 79 pull_requests = PullRequestModel().get_awaiting_review(
82 80 repo_name,
83 81 search_q=search_q, statuses=statuses,
84 82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 84 repo_name,
87 85 search_q=search_q, statuses=statuses)
88 86 elif filter_type == 'awaiting_my_review':
89 87 pull_requests = PullRequestModel().get_awaiting_my_review(
90 88 repo_name, self._rhodecode_user.user_id,
91 89 search_q=search_q, statuses=statuses,
92 90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 92 repo_name, self._rhodecode_user.user_id,
95 93 search_q=search_q, statuses=statuses)
96 94 else:
97 95 pull_requests = PullRequestModel().get_all(
98 96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 97 statuses=statuses, offset=start, length=limit,
100 98 order_by=order_by, order_dir=order_dir)
101 99 pull_requests_total_count = PullRequestModel().count_all(
102 100 repo_name, search_q=search_q, source=source, statuses=statuses,
103 101 opened_by=opened_by)
104 102
105 103 data = []
106 104 comments_model = CommentsModel()
107 105 for pr in pull_requests:
108 106 comments_count = comments_model.get_all_comments(
109 107 self.db_repo.repo_id, pull_request=pr,
110 108 include_drafts=False, count_only=True)
111 109
112 110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 112 if review_statuses and review_statuses[4]:
115 113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 114 my_review_status = statuses[0][1].status
117 115
118 116 data.append({
119 117 'name': _render('pullrequest_name',
120 118 pr.pull_request_id, pr.pull_request_state,
121 119 pr.work_in_progress, pr.target_repo.repo_name,
122 120 short=True),
123 121 'name_raw': pr.pull_request_id,
124 122 'status': _render('pullrequest_status',
125 123 pr.calculated_review_status()),
126 124 'my_status': _render('pullrequest_status',
127 125 my_review_status),
128 126 'title': _render('pullrequest_title', pr.title, pr.description),
129 127 'description': h.escape(pr.description),
130 128 'updated_on': _render('pullrequest_updated_on',
131 129 h.datetime_to_time(pr.updated_on),
132 130 pr.versions_count),
133 131 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 132 'created_on': _render('pullrequest_updated_on',
135 133 h.datetime_to_time(pr.created_on)),
136 134 'created_on_raw': h.datetime_to_time(pr.created_on),
137 135 'state': pr.pull_request_state,
138 136 'author': _render('pullrequest_author',
139 137 pr.author.full_contact, ),
140 138 'author_raw': pr.author.full_name,
141 139 'comments': _render('pullrequest_comments', comments_count),
142 140 'comments_raw': comments_count,
143 141 'closed': pr.is_closed(),
144 142 })
145 143
146 144 data = ({
147 145 'draw': draw,
148 146 'data': data,
149 147 'recordsTotal': pull_requests_total_count,
150 148 'recordsFiltered': pull_requests_total_count,
151 149 })
152 150 return data
153 151
154 152 @LoginRequired()
155 153 @HasRepoPermissionAnyDecorator(
156 154 'repository.read', 'repository.write', 'repository.admin')
157 155 def pull_request_list(self):
158 156 c = self.load_default_context()
159 157
160 158 req_get = self.request.GET
161 159 c.source = str2bool(req_get.get('source'))
162 160 c.closed = str2bool(req_get.get('closed'))
163 161 c.my = str2bool(req_get.get('my'))
164 162 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 163 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 164
167 165 c.active = 'open'
168 166 if c.my:
169 167 c.active = 'my'
170 168 if c.closed:
171 169 c.active = 'closed'
172 170 if c.awaiting_review and not c.source:
173 171 c.active = 'awaiting'
174 172 if c.source and not c.awaiting_review:
175 173 c.active = 'source'
176 174 if c.awaiting_my_review:
177 175 c.active = 'awaiting_my'
178 176
179 177 return self._get_template_context(c)
180 178
181 179 @LoginRequired()
182 180 @HasRepoPermissionAnyDecorator(
183 181 'repository.read', 'repository.write', 'repository.admin')
184 182 def pull_request_list_data(self):
185 183 self.load_default_context()
186 184
187 185 # additional filters
188 186 req_get = self.request.GET
189 187 source = str2bool(req_get.get('source'))
190 188 closed = str2bool(req_get.get('closed'))
191 189 my = str2bool(req_get.get('my'))
192 190 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 191 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 192
195 193 filter_type = 'awaiting_review' if awaiting_review \
196 194 else 'awaiting_my_review' if awaiting_my_review \
197 195 else None
198 196
199 197 opened_by = None
200 198 if my:
201 199 opened_by = [self._rhodecode_user.user_id]
202 200
203 201 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 202 if closed:
205 203 statuses = [PullRequest.STATUS_CLOSED]
206 204
207 205 data = self._get_pull_requests_list(
208 206 repo_name=self.db_repo_name, source=source,
209 207 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 208
211 209 return data
212 210
213 211 def _is_diff_cache_enabled(self, target_repo):
214 212 caching_enabled = self._get_general_setting(
215 213 target_repo, 'rhodecode_diff_cache')
216 214 log.debug('Diff caching enabled: %s', caching_enabled)
217 215 return caching_enabled
218 216
219 217 def _get_diffset(self, source_repo_name, source_repo,
220 218 ancestor_commit,
221 219 source_ref_id, target_ref_id,
222 220 target_commit, source_commit, diff_limit, file_limit,
223 221 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 222
225 223 target_commit_final = target_commit
226 224 source_commit_final = source_commit
227 225
228 226 if use_ancestor:
229 227 # we might want to not use it for versions
230 228 target_ref_id = ancestor_commit.raw_id
231 229 target_commit_final = ancestor_commit
232 230
233 231 vcs_diff = PullRequestModel().get_diff(
234 232 source_repo, source_ref_id, target_ref_id,
235 233 hide_whitespace_changes, diff_context)
236 234
237 235 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
238 236 file_limit=file_limit, show_full_diff=fulldiff)
239 237
240 238 _parsed = diff_processor.prepare()
241 239
242 240 diffset = codeblocks.DiffSet(
243 241 repo_name=self.db_repo_name,
244 242 source_repo_name=source_repo_name,
245 243 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 244 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 245 )
248 246 diffset = self.path_filter.render_patchset_filtered(
249 247 diffset, _parsed, target_ref_id, source_ref_id)
250 248
251 249 return diffset
252 250
253 251 def _get_range_diffset(self, source_scm, source_repo,
254 252 commit1, commit2, diff_limit, file_limit,
255 253 fulldiff, hide_whitespace_changes, diff_context):
256 254 vcs_diff = source_scm.get_diff(
257 255 commit1, commit2,
258 256 ignore_whitespace=hide_whitespace_changes,
259 257 context=diff_context)
260 258
261 259 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
262 260 diff_limit=diff_limit,
263 261 file_limit=file_limit, show_full_diff=fulldiff)
264 262
265 263 _parsed = diff_processor.prepare()
266 264
267 265 diffset = codeblocks.DiffSet(
268 266 repo_name=source_repo.repo_name,
269 267 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 268 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 269
272 270 diffset = self.path_filter.render_patchset_filtered(
273 271 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 272
275 273 return diffset
276 274
277 275 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 276 comments_model = CommentsModel()
279 277
280 278 # GENERAL COMMENTS with versions #
281 279 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 280 q = q.order_by(ChangesetComment.comment_id.asc())
283 281 if not include_drafts:
284 282 q = q.filter(ChangesetComment.draft == false())
285 283 general_comments = q
286 284
287 285 # pick comments we want to render at current version
288 286 c.comment_versions = comments_model.aggregate_comments(
289 287 general_comments, versions, c.at_version_num)
290 288
291 289 # INLINE COMMENTS with versions #
292 290 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 291 q = q.order_by(ChangesetComment.comment_id.asc())
294 292 if not include_drafts:
295 293 q = q.filter(ChangesetComment.draft == false())
296 294 inline_comments = q
297 295
298 296 c.inline_versions = comments_model.aggregate_comments(
299 297 inline_comments, versions, c.at_version_num, inline=True)
300 298
301 299 # Comments inline+general
302 300 if c.at_version:
303 301 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 302 c.comments = c.comment_versions[c.at_version_num]['display']
305 303 else:
306 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 305 c.comments = c.comment_versions[c.at_version_num]['until']
308 306
309 307 return general_comments, inline_comments
310 308
311 309 @LoginRequired()
312 310 @HasRepoPermissionAnyDecorator(
313 311 'repository.read', 'repository.write', 'repository.admin')
314 312 def pull_request_show(self):
315 313 _ = self.request.translate
316 314 c = self.load_default_context()
317 315
318 316 pull_request = PullRequest.get_or_404(
319 317 self.request.matchdict['pull_request_id'])
320 318 pull_request_id = pull_request.pull_request_id
321 319
322 320 c.state_progressing = pull_request.is_state_changing()
323 321 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 322
325 323 _new_state = {
326 324 'created': PullRequest.STATE_CREATED,
327 325 }.get(self.request.GET.get('force_state'))
328 326 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329 327
330 328 if can_force_state and _new_state:
331 329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 330 h.flash(
333 331 _('Pull Request state was force changed to `{}`').format(_new_state),
334 332 category='success')
335 333 Session().commit()
336 334
337 335 raise HTTPFound(h.route_path(
338 336 'pullrequest_show', repo_name=self.db_repo_name,
339 337 pull_request_id=pull_request_id))
340 338
341 339 version = self.request.GET.get('version')
342 340 from_version = self.request.GET.get('from_version') or version
343 341 merge_checks = self.request.GET.get('merge_checks')
344 342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347 345
348 346 # fetch global flags of ignore ws or context lines
349 347 diff_context = diffs.get_diff_context(self.request)
350 348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351 349
352 350 (pull_request_latest,
353 351 pull_request_at_ver,
354 352 pull_request_display_obj,
355 353 at_version) = PullRequestModel().get_pr_version(
356 354 pull_request_id, version=version)
357 355
358 356 pr_closed = pull_request_latest.is_closed()
359 357
360 358 if pr_closed and (version or from_version):
361 359 # not allow to browse versions for closed PR
362 360 raise HTTPFound(h.route_path(
363 361 'pullrequest_show', repo_name=self.db_repo_name,
364 362 pull_request_id=pull_request_id))
365 363
366 364 versions = pull_request_display_obj.versions()
367 365
368 366 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369 367
370 368 # used to store per-commit range diffs
371 369 c.changes = collections.OrderedDict()
372 370
373 371 c.at_version = at_version
374 372 c.at_version_num = (at_version
375 373 if at_version and at_version != PullRequest.LATEST_VER
376 374 else None)
377 375
378 376 c.at_version_index = ChangesetComment.get_index_from_version(
379 377 c.at_version_num, versions)
380 378
381 379 (prev_pull_request_latest,
382 380 prev_pull_request_at_ver,
383 381 prev_pull_request_display_obj,
384 382 prev_at_version) = PullRequestModel().get_pr_version(
385 383 pull_request_id, version=from_version)
386 384
387 385 c.from_version = prev_at_version
388 386 c.from_version_num = (prev_at_version
389 387 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 388 else None)
391 389 c.from_version_index = ChangesetComment.get_index_from_version(
392 390 c.from_version_num, versions)
393 391
394 392 # define if we're in COMPARE mode or VIEW at version mode
395 393 compare = at_version != prev_at_version
396 394
397 395 # pull_requests repo_name we opened it against
398 396 # ie. target_repo must match
399 397 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 398 log.warning('Mismatch between the current repo: %s, and target %s',
401 399 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 400 raise HTTPNotFound()
403 401
404 402 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405 403
406 404 c.pull_request = pull_request_display_obj
407 405 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 406 c.pull_request_latest = pull_request_latest
409 407
410 408 # inject latest version
411 409 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 410 c.versions = versions + [latest_ver]
413 411
414 412 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 413 c.allowed_to_change_status = False
416 414 c.allowed_to_update = False
417 415 c.allowed_to_merge = False
418 416 c.allowed_to_delete = False
419 417 c.allowed_to_comment = False
420 418 c.allowed_to_close = False
421 419 else:
422 420 can_change_status = PullRequestModel().check_user_change_status(
423 421 pull_request_at_ver, self._rhodecode_user)
424 422 c.allowed_to_change_status = can_change_status and not pr_closed
425 423
426 424 c.allowed_to_update = PullRequestModel().check_user_update(
427 425 pull_request_latest, self._rhodecode_user) and not pr_closed
428 426 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 427 pull_request_latest, self._rhodecode_user) and not pr_closed
430 428 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 429 pull_request_latest, self._rhodecode_user) and not pr_closed
432 430 c.allowed_to_comment = not pr_closed
433 431 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434 432
435 433 c.forbid_adding_reviewers = False
436 434
437 435 if pull_request_latest.reviewer_data and \
438 436 'rules' in pull_request_latest.reviewer_data:
439 437 rules = pull_request_latest.reviewer_data['rules'] or {}
440 438 try:
441 439 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 440 except Exception:
443 441 pass
444 442
445 443 # check merge capabilities
446 444 _merge_check = MergeCheck.validate(
447 445 pull_request_latest, auth_user=self._rhodecode_user,
448 446 translator=self.request.translate,
449 447 force_shadow_repo_refresh=force_refresh)
450 448
451 449 c.pr_merge_errors = _merge_check.error_details
452 450 c.pr_merge_possible = not _merge_check.failed
453 451 c.pr_merge_message = _merge_check.merge_msg
454 452 c.pr_merge_source_commit = _merge_check.source_commit
455 453 c.pr_merge_target_commit = _merge_check.target_commit
456 454
457 455 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 456 pull_request_latest, translator=self.request.translate)
459 457
460 458 c.pull_request_review_status = _merge_check.review_status
461 459 if merge_checks:
462 460 self.request.override_renderer = \
463 461 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 462 return self._get_template_context(c)
465 463
466 464 c.reviewers_count = pull_request.reviewers_count
467 465 c.observers_count = pull_request.observers_count
468 466
469 467 # reviewers and statuses
470 468 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
471 469 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 470 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473 471
474 472 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 473 member_reviewer = h.reviewer_as_json(
476 474 member, reasons=reasons, mandatory=mandatory,
477 475 role=review_obj.role,
478 476 user_group=review_obj.rule_user_group_data()
479 477 )
480 478
481 479 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 480 member_reviewer['review_status'] = current_review_status
483 481 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 482 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 483 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486 484
487 485 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
488 486
489 487 for observer_obj, member in pull_request_at_ver.observers():
490 488 member_observer = h.reviewer_as_json(
491 489 member, reasons=[], mandatory=False,
492 490 role=observer_obj.role,
493 491 user_group=observer_obj.rule_user_group_data()
494 492 )
495 493 member_observer['allowed_to_update'] = c.allowed_to_update
496 494 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 495
498 496 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
499 497
500 498 general_comments, inline_comments = \
501 499 self.register_comments_vars(c, pull_request_latest, versions)
502 500
503 501 # TODOs
504 502 c.unresolved_comments = CommentsModel() \
505 503 .get_pull_request_unresolved_todos(pull_request_latest)
506 504 c.resolved_comments = CommentsModel() \
507 505 .get_pull_request_resolved_todos(pull_request_latest)
508 506
509 507 # Drafts
510 508 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 509 self._rhodecode_db_user.user_id,
512 510 pull_request_latest)
513 511
514 512 # if we use version, then do not show later comments
515 513 # than current version
516 514 display_inline_comments = collections.defaultdict(
517 515 lambda: collections.defaultdict(list))
518 516 for co in inline_comments:
519 517 if c.at_version_num:
520 518 # pick comments that are at least UPTO given version, so we
521 519 # don't render comments for higher version
522 520 should_render = co.pull_request_version_id and \
523 521 co.pull_request_version_id <= c.at_version_num
524 522 else:
525 523 # showing all, for 'latest'
526 524 should_render = True
527 525
528 526 if should_render:
529 527 display_inline_comments[co.f_path][co.line_no].append(co)
530 528
531 529 # load diff data into template context, if we use compare mode then
532 530 # diff is calculated based on changes between versions of PR
533 531
534 532 source_repo = pull_request_at_ver.source_repo
535 533 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 534
537 535 target_repo = pull_request_at_ver.target_repo
538 536 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 537
540 538 if compare:
541 539 # in compare switch the diff base to latest commit from prev version
542 540 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 541
544 542 # despite opening commits for bookmarks/branches/tags, we always
545 543 # convert this to rev to prevent changes after bookmark or branch change
546 544 c.source_ref_type = 'rev'
547 545 c.source_ref = source_ref_id
548 546
549 547 c.target_ref_type = 'rev'
550 548 c.target_ref = target_ref_id
551 549
552 550 c.source_repo = source_repo
553 551 c.target_repo = target_repo
554 552
555 553 c.commit_ranges = []
556 554 source_commit = EmptyCommit()
557 555 target_commit = EmptyCommit()
558 556 c.missing_requirements = False
559 557
560 558 source_scm = source_repo.scm_instance()
561 559 target_scm = target_repo.scm_instance()
562 560
563 561 shadow_scm = None
564 562 try:
565 563 shadow_scm = pull_request_latest.get_shadow_repo()
566 564 except Exception:
567 565 log.debug('Failed to get shadow repo', exc_info=True)
568 566 # try first the existing source_repo, and then shadow
569 567 # repo if we can obtain one
570 568 commits_source_repo = source_scm
571 569 if shadow_scm:
572 570 commits_source_repo = shadow_scm
573 571
574 572 c.commits_source_repo = commits_source_repo
575 573 c.ancestor = None # set it to None, to hide it from PR view
576 574
577 575 # empty version means latest, so we keep this to prevent
578 576 # double caching
579 577 version_normalized = version or PullRequest.LATEST_VER
580 578 from_version_normalized = from_version or PullRequest.LATEST_VER
581 579
582 580 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 581 cache_file_path = diff_cache_exist(
584 582 cache_path, 'pull_request', pull_request_id, version_normalized,
585 583 from_version_normalized, source_ref_id, target_ref_id,
586 584 hide_whitespace_changes, diff_context, c.fulldiff)
587 585
588 586 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 587 force_recache = self.get_recache_flag()
590 588
591 589 cached_diff = None
592 590 if caching_enabled:
593 591 cached_diff = load_cached_diff(cache_file_path)
594 592
595 593 has_proper_commit_cache = (
596 594 cached_diff and cached_diff.get('commits')
597 595 and len(cached_diff.get('commits', [])) == 5
598 596 and cached_diff.get('commits')[0]
599 597 and cached_diff.get('commits')[3])
600 598
601 599 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 600 diff_commit_cache = \
603 601 (ancestor_commit, commit_cache, missing_requirements,
604 602 source_commit, target_commit) = cached_diff['commits']
605 603 else:
606 604 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 605 # merge errors resulting in potentially hidden commits in the shadow repo.
608 606 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 607 and _merge_check.merge_response
610 608 maybe_unreachable = maybe_unreachable \
611 609 and _merge_check.merge_response.metadata.get('unresolved_files')
612 610 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 611 diff_commit_cache = \
614 612 (ancestor_commit, commit_cache, missing_requirements,
615 613 source_commit, target_commit) = self.get_commits(
616 614 commits_source_repo,
617 615 pull_request_at_ver,
618 616 source_commit,
619 617 source_ref_id,
620 618 source_scm,
621 619 target_commit,
622 620 target_ref_id,
623 621 target_scm,
624 622 maybe_unreachable=maybe_unreachable)
625 623
626 624 # register our commit range
627 625 for comm in commit_cache.values():
628 626 c.commit_ranges.append(comm)
629 627
630 628 c.missing_requirements = missing_requirements
631 629 c.ancestor_commit = ancestor_commit
632 630 c.statuses = source_repo.statuses(
633 631 [x.raw_id for x in c.commit_ranges])
634 632
635 633 # auto collapse if we have more than limit
636 634 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 635 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 636 c.compare_mode = compare
639 637
640 638 # diff_limit is the old behavior, will cut off the whole diff
641 639 # if the limit is applied otherwise will just hide the
642 640 # big files from the front-end
643 641 diff_limit = c.visual.cut_off_limit_diff
644 642 file_limit = c.visual.cut_off_limit_file
645 643
646 644 c.missing_commits = False
647 645 if (c.missing_requirements
648 646 or isinstance(source_commit, EmptyCommit)
649 647 or source_commit == target_commit):
650 648
651 649 c.missing_commits = True
652 650 else:
653 651 c.inline_comments = display_inline_comments
654 652
655 653 use_ancestor = True
656 654 if from_version_normalized != version_normalized:
657 655 use_ancestor = False
658 656
659 657 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 658 if not force_recache and has_proper_diff_cache:
661 659 c.diffset = cached_diff['diff']
662 660 else:
663 661 try:
664 662 c.diffset = self._get_diffset(
665 663 c.source_repo.repo_name, commits_source_repo,
666 664 c.ancestor_commit,
667 665 source_ref_id, target_ref_id,
668 666 target_commit, source_commit,
669 667 diff_limit, file_limit, c.fulldiff,
670 668 hide_whitespace_changes, diff_context,
671 669 use_ancestor=use_ancestor
672 670 )
673 671
674 672 # save cached diff
675 673 if caching_enabled:
676 674 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 675 except CommitDoesNotExistError:
678 676 log.exception('Failed to generate diffset')
679 677 c.missing_commits = True
680 678
681 679 if not c.missing_commits:
682 680
683 681 c.limited_diff = c.diffset.limited_diff
684 682
685 683 # calculate removed files that are bound to comments
686 684 comment_deleted_files = [
687 685 fname for fname in display_inline_comments
688 686 if fname not in c.diffset.file_stats]
689 687
690 688 c.deleted_files_comments = collections.defaultdict(dict)
691 689 for fname, per_line_comments in display_inline_comments.items():
692 690 if fname in comment_deleted_files:
693 691 c.deleted_files_comments[fname]['stats'] = 0
694 692 c.deleted_files_comments[fname]['comments'] = list()
695 693 for lno, comments in per_line_comments.items():
696 694 c.deleted_files_comments[fname]['comments'].extend(comments)
697 695
698 696 # maybe calculate the range diff
699 697 if c.range_diff_on:
700 698 # TODO(marcink): set whitespace/context
701 699 context_lcl = 3
702 700 ign_whitespace_lcl = False
703 701
704 702 for commit in c.commit_ranges:
705 703 commit2 = commit
706 704 commit1 = commit.first_parent
707 705
708 706 range_diff_cache_file_path = diff_cache_exist(
709 707 cache_path, 'diff', commit.raw_id,
710 708 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 709
712 710 cached_diff = None
713 711 if caching_enabled:
714 712 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 713
716 714 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 715 if not force_recache and has_proper_diff_cache:
718 716 diffset = cached_diff['diff']
719 717 else:
720 718 diffset = self._get_range_diffset(
721 719 commits_source_repo, source_repo,
722 720 commit1, commit2, diff_limit, file_limit,
723 721 c.fulldiff, ign_whitespace_lcl, context_lcl
724 722 )
725 723
726 724 # save cached diff
727 725 if caching_enabled:
728 726 cache_diff(range_diff_cache_file_path, diffset, None)
729 727
730 728 c.changes[commit.raw_id] = diffset
731 729
732 730 # this is a hack to properly display links, when creating PR, the
733 731 # compare view and others uses different notation, and
734 732 # compare_commits.mako renders links based on the target_repo.
735 733 # We need to swap that here to generate it properly on the html side
736 734 c.target_repo = c.source_repo
737 735
738 736 c.commit_statuses = ChangesetStatus.STATUSES
739 737
740 738 c.show_version_changes = not pr_closed
741 739 if c.show_version_changes:
742 740 cur_obj = pull_request_at_ver
743 741 prev_obj = prev_pull_request_at_ver
744 742
745 743 old_commit_ids = prev_obj.revisions
746 744 new_commit_ids = cur_obj.revisions
747 745 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 746 old_commit_ids, new_commit_ids)
749 747 c.commit_changes_summary = commit_changes
750 748
751 749 # calculate the diff for commits between versions
752 750 c.commit_changes = []
753 751
754 752 def mark(cs, fw):
755 753 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
756 754
757 755 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 756 + mark(commit_changes.removed, 'r') \
759 757 + mark(commit_changes.common, 'c'):
760 758
761 759 if raw_id in commit_cache:
762 760 commit = commit_cache[raw_id]
763 761 else:
764 762 try:
765 763 commit = commits_source_repo.get_commit(raw_id)
766 764 except CommitDoesNotExistError:
767 765 # in case we fail extracting still use "dummy" commit
768 766 # for display in commit diff
769 767 commit = h.AttributeDict(
770 768 {'raw_id': raw_id,
771 769 'message': 'EMPTY or MISSING COMMIT'})
772 770 c.commit_changes.append([c_type, commit])
773 771
774 772 # current user review statuses for each version
775 773 c.review_versions = {}
776 774 is_reviewer = PullRequestModel().is_user_reviewer(
777 775 pull_request, self._rhodecode_user)
778 776 if is_reviewer:
779 777 for co in general_comments:
780 778 if co.author.user_id == self._rhodecode_user.user_id:
781 779 status = co.status_change
782 780 if status:
783 781 _ver_pr = status[0].comment.pull_request_version_id
784 782 c.review_versions[_ver_pr] = status[0]
785 783
786 784 return self._get_template_context(c)
787 785
788 786 def get_commits(
789 787 self, commits_source_repo, pull_request_at_ver, source_commit,
790 788 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 789 maybe_unreachable=False):
792 790
793 791 commit_cache = collections.OrderedDict()
794 792 missing_requirements = False
795 793
796 794 try:
797 795 pre_load = ["author", "date", "message", "branch", "parents"]
798 796
799 797 pull_request_commits = pull_request_at_ver.revisions
800 798 log.debug('Loading %s commits from %s',
801 799 len(pull_request_commits), commits_source_repo)
802 800
803 801 for rev in pull_request_commits:
804 802 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 803 maybe_unreachable=maybe_unreachable)
806 804 commit_cache[comm.raw_id] = comm
807 805
808 806 # Order here matters, we first need to get target, and then
809 807 # the source
810 808 target_commit = commits_source_repo.get_commit(
811 809 commit_id=safe_str(target_ref_id))
812 810
813 811 source_commit = commits_source_repo.get_commit(
814 812 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 813 except CommitDoesNotExistError:
816 814 log.warning('Failed to get commit from `{}` repo'.format(
817 815 commits_source_repo), exc_info=True)
818 816 except RepositoryRequirementError:
819 817 log.warning('Failed to get all required data from repo', exc_info=True)
820 818 missing_requirements = True
821 819
822 820 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 821
824 822 try:
825 823 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 824 except Exception:
827 825 ancestor_commit = None
828 826
829 827 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 828
831 829 def assure_not_empty_repo(self):
832 830 _ = self.request.translate
833 831
834 832 try:
835 833 self.db_repo.scm_instance().get_commit()
836 834 except EmptyRepositoryError:
837 835 h.flash(h.literal(_('There are no commits yet')),
838 836 category='warning')
839 837 raise HTTPFound(
840 838 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 839
842 840 @LoginRequired()
843 841 @NotAnonymous()
844 842 @HasRepoPermissionAnyDecorator(
845 843 'repository.read', 'repository.write', 'repository.admin')
846 844 def pull_request_new(self):
847 845 _ = self.request.translate
848 846 c = self.load_default_context()
849 847
850 848 self.assure_not_empty_repo()
851 849 source_repo = self.db_repo
852 850
853 851 commit_id = self.request.GET.get('commit')
854 852 branch_ref = self.request.GET.get('branch')
855 853 bookmark_ref = self.request.GET.get('bookmark')
856 854
857 855 try:
858 856 source_repo_data = PullRequestModel().generate_repo_data(
859 857 source_repo, commit_id=commit_id,
860 858 branch=branch_ref, bookmark=bookmark_ref,
861 859 translator=self.request.translate)
862 860 except CommitDoesNotExistError as e:
863 861 log.exception(e)
864 862 h.flash(_('Commit does not exist'), 'error')
865 863 raise HTTPFound(
866 864 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 865
868 866 default_target_repo = source_repo
869 867
870 868 if source_repo.parent and c.has_origin_repo_read_perm:
871 869 parent_vcs_obj = source_repo.parent.scm_instance()
872 870 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 871 # change default if we have a parent repo
874 872 default_target_repo = source_repo.parent
875 873
876 874 target_repo_data = PullRequestModel().generate_repo_data(
877 875 default_target_repo, translator=self.request.translate)
878 876
879 877 selected_source_ref = source_repo_data['refs']['selected_ref']
880 878 title_source_ref = ''
881 879 if selected_source_ref:
882 880 title_source_ref = selected_source_ref.split(':', 2)[1]
883 881 c.default_title = PullRequestModel().generate_pullrequest_title(
884 882 source=source_repo.repo_name,
885 883 source_ref=title_source_ref,
886 884 target=default_target_repo.repo_name
887 885 )
888 886
889 887 c.default_repo_data = {
890 888 'source_repo_name': source_repo.repo_name,
891 889 'source_refs_json': ext_json.str_json(source_repo_data),
892 890 'target_repo_name': default_target_repo.repo_name,
893 891 'target_refs_json': ext_json.str_json(target_repo_data),
894 892 }
895 893 c.default_source_ref = selected_source_ref
896 894
897 895 return self._get_template_context(c)
898 896
899 897 @LoginRequired()
900 898 @NotAnonymous()
901 899 @HasRepoPermissionAnyDecorator(
902 900 'repository.read', 'repository.write', 'repository.admin')
903 901 def pull_request_repo_refs(self):
904 902 self.load_default_context()
905 903 target_repo_name = self.request.matchdict['target_repo_name']
906 904 repo = Repository.get_by_repo_name(target_repo_name)
907 905 if not repo:
908 906 raise HTTPNotFound()
909 907
910 908 target_perm = HasRepoPermissionAny(
911 909 'repository.read', 'repository.write', 'repository.admin')(
912 910 target_repo_name)
913 911 if not target_perm:
914 912 raise HTTPNotFound()
915 913
916 914 return PullRequestModel().generate_repo_data(
917 915 repo, translator=self.request.translate)
918 916
919 917 @LoginRequired()
920 918 @NotAnonymous()
921 919 @HasRepoPermissionAnyDecorator(
922 920 'repository.read', 'repository.write', 'repository.admin')
923 921 def pullrequest_repo_targets(self):
924 922 _ = self.request.translate
925 923 filter_query = self.request.GET.get('query')
926 924
927 925 # get the parents
928 926 parent_target_repos = []
929 927 if self.db_repo.parent:
930 928 parents_query = Repository.query() \
931 929 .order_by(func.length(Repository.repo_name)) \
932 930 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 931
934 932 if filter_query:
935 ilike_expression = u'%{}%'.format(safe_str(filter_query))
933 ilike_expression = f'%{safe_str(filter_query)}%'
936 934 parents_query = parents_query.filter(
937 935 Repository.repo_name.ilike(ilike_expression))
938 936 parents = parents_query.limit(20).all()
939 937
940 938 for parent in parents:
941 939 parent_vcs_obj = parent.scm_instance()
942 940 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 941 parent_target_repos.append(parent)
944 942
945 943 # get other forks, and repo itself
946 944 query = Repository.query() \
947 945 .order_by(func.length(Repository.repo_name)) \
948 946 .filter(
949 947 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 948 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 949 ) \
952 950 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 951
954 952 if filter_query:
955 ilike_expression = u'%{}%'.format(safe_str(filter_query))
953 ilike_expression = f'%{safe_str(filter_query)}%'
956 954 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 955
958 956 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 957 target_repos = query.limit(limit).all()
960 958
961 959 all_target_repos = target_repos + parent_target_repos
962 960
963 961 repos = []
964 962 # This checks permissions to the repositories
965 963 for obj in ScmModel().get_repos(all_target_repos):
966 964 repos.append({
967 965 'id': obj['name'],
968 966 'text': obj['name'],
969 967 'type': 'repo',
970 968 'repo_id': obj['dbrepo']['repo_id'],
971 969 'repo_type': obj['dbrepo']['repo_type'],
972 970 'private': obj['dbrepo']['private'],
973 971
974 972 })
975 973
976 974 data = {
977 975 'more': False,
978 976 'results': [{
979 977 'text': _('Repositories'),
980 978 'children': repos
981 979 }] if repos else []
982 980 }
983 981 return data
984 982
985 983 @classmethod
986 984 def get_comment_ids(cls, post_data):
987 985 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 986
989 987 @LoginRequired()
990 988 @NotAnonymous()
991 989 @HasRepoPermissionAnyDecorator(
992 990 'repository.read', 'repository.write', 'repository.admin')
993 991 def pullrequest_comments(self):
994 992 self.load_default_context()
995 993
996 994 pull_request = PullRequest.get_or_404(
997 995 self.request.matchdict['pull_request_id'])
998 996 pull_request_id = pull_request.pull_request_id
999 997 version = self.request.GET.get('version')
1000 998
1001 999 _render = self.request.get_partial_renderer(
1002 1000 'rhodecode:templates/base/sidebar.mako')
1003 1001 c = _render.get_call_context()
1004 1002
1005 1003 (pull_request_latest,
1006 1004 pull_request_at_ver,
1007 1005 pull_request_display_obj,
1008 1006 at_version) = PullRequestModel().get_pr_version(
1009 1007 pull_request_id, version=version)
1010 1008 versions = pull_request_display_obj.versions()
1011 1009 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 1010 c.versions = versions + [latest_ver]
1013 1011
1014 1012 c.at_version = at_version
1015 1013 c.at_version_num = (at_version
1016 1014 if at_version and at_version != PullRequest.LATEST_VER
1017 1015 else None)
1018 1016
1019 1017 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 1018 all_comments = c.inline_comments_flat + c.comments
1021 1019
1022 1020 existing_ids = self.get_comment_ids(self.request.POST)
1023 1021 return _render('comments_table', all_comments, len(all_comments),
1024 1022 existing_ids=existing_ids)
1025 1023
1026 1024 @LoginRequired()
1027 1025 @NotAnonymous()
1028 1026 @HasRepoPermissionAnyDecorator(
1029 1027 'repository.read', 'repository.write', 'repository.admin')
1030 1028 def pullrequest_todos(self):
1031 1029 self.load_default_context()
1032 1030
1033 1031 pull_request = PullRequest.get_or_404(
1034 1032 self.request.matchdict['pull_request_id'])
1035 1033 pull_request_id = pull_request.pull_request_id
1036 1034 version = self.request.GET.get('version')
1037 1035
1038 1036 _render = self.request.get_partial_renderer(
1039 1037 'rhodecode:templates/base/sidebar.mako')
1040 1038 c = _render.get_call_context()
1041 1039 (pull_request_latest,
1042 1040 pull_request_at_ver,
1043 1041 pull_request_display_obj,
1044 1042 at_version) = PullRequestModel().get_pr_version(
1045 1043 pull_request_id, version=version)
1046 1044 versions = pull_request_display_obj.versions()
1047 1045 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 1046 c.versions = versions + [latest_ver]
1049 1047
1050 1048 c.at_version = at_version
1051 1049 c.at_version_num = (at_version
1052 1050 if at_version and at_version != PullRequest.LATEST_VER
1053 1051 else None)
1054 1052
1055 1053 c.unresolved_comments = CommentsModel() \
1056 1054 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 1055 c.resolved_comments = CommentsModel() \
1058 1056 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 1057
1060 1058 all_comments = c.unresolved_comments + c.resolved_comments
1061 1059 existing_ids = self.get_comment_ids(self.request.POST)
1062 1060 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 1061 todo_comments=True, existing_ids=existing_ids)
1064 1062
1065 1063 @LoginRequired()
1066 1064 @NotAnonymous()
1067 1065 @HasRepoPermissionAnyDecorator(
1068 1066 'repository.read', 'repository.write', 'repository.admin')
1069 1067 def pullrequest_drafts(self):
1070 1068 self.load_default_context()
1071 1069
1072 1070 pull_request = PullRequest.get_or_404(
1073 1071 self.request.matchdict['pull_request_id'])
1074 1072 pull_request_id = pull_request.pull_request_id
1075 1073 version = self.request.GET.get('version')
1076 1074
1077 1075 _render = self.request.get_partial_renderer(
1078 1076 'rhodecode:templates/base/sidebar.mako')
1079 1077 c = _render.get_call_context()
1080 1078
1081 1079 (pull_request_latest,
1082 1080 pull_request_at_ver,
1083 1081 pull_request_display_obj,
1084 1082 at_version) = PullRequestModel().get_pr_version(
1085 1083 pull_request_id, version=version)
1086 1084 versions = pull_request_display_obj.versions()
1087 1085 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 1086 c.versions = versions + [latest_ver]
1089 1087
1090 1088 c.at_version = at_version
1091 1089 c.at_version_num = (at_version
1092 1090 if at_version and at_version != PullRequest.LATEST_VER
1093 1091 else None)
1094 1092
1095 1093 c.draft_comments = CommentsModel() \
1096 1094 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 1095
1098 1096 all_comments = c.draft_comments
1099 1097
1100 1098 existing_ids = self.get_comment_ids(self.request.POST)
1101 1099 return _render('comments_table', all_comments, len(all_comments),
1102 1100 existing_ids=existing_ids, draft_comments=True)
1103 1101
1104 1102 @LoginRequired()
1105 1103 @NotAnonymous()
1106 1104 @HasRepoPermissionAnyDecorator(
1107 1105 'repository.read', 'repository.write', 'repository.admin')
1108 1106 @CSRFRequired()
1109 1107 def pull_request_create(self):
1110 1108 _ = self.request.translate
1111 1109 self.assure_not_empty_repo()
1112 1110 self.load_default_context()
1113 1111
1114 1112 controls = peppercorn.parse(self.request.POST.items())
1115 1113
1116 1114 try:
1117 1115 form = PullRequestForm(
1118 1116 self.request.translate, self.db_repo.repo_id)()
1119 1117 _form = form.to_python(controls)
1120 1118 except formencode.Invalid as errors:
1121 1119 if errors.error_dict.get('revisions'):
1122 1120 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1123 1121 elif errors.error_dict.get('pullrequest_title'):
1124 1122 msg = errors.error_dict.get('pullrequest_title')
1125 1123 else:
1126 1124 msg = _('Error creating pull request: {}').format(errors)
1127 1125 log.exception(msg)
1128 1126 h.flash(msg, 'error')
1129 1127
1130 1128 # would rather just go back to form ...
1131 1129 raise HTTPFound(
1132 1130 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 1131
1134 1132 source_repo = _form['source_repo']
1135 1133 source_ref = _form['source_ref']
1136 1134 target_repo = _form['target_repo']
1137 1135 target_ref = _form['target_ref']
1138 1136 commit_ids = _form['revisions'][::-1]
1139 1137 common_ancestor_id = _form['common_ancestor']
1140 1138
1141 1139 # find the ancestor for this pr
1142 1140 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 1141 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 1142
1145 1143 if not (source_db_repo or target_db_repo):
1146 1144 h.flash(_('source_repo or target repo not found'), category='error')
1147 1145 raise HTTPFound(
1148 1146 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 1147
1150 1148 # re-check permissions again here
1151 1149 # source_repo we must have read permissions
1152 1150
1153 1151 source_perm = HasRepoPermissionAny(
1154 1152 'repository.read', 'repository.write', 'repository.admin')(
1155 1153 source_db_repo.repo_name)
1156 1154 if not source_perm:
1157 1155 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 1156 source_db_repo.repo_name))
1159 1157 h.flash(msg, category='error')
1160 1158 # copy the args back to redirect
1161 1159 org_query = self.request.GET.mixed()
1162 1160 raise HTTPFound(
1163 1161 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 1162 _query=org_query))
1165 1163
1166 1164 # target repo we must have read permissions, and also later on
1167 1165 # we want to check branch permissions here
1168 1166 target_perm = HasRepoPermissionAny(
1169 1167 'repository.read', 'repository.write', 'repository.admin')(
1170 1168 target_db_repo.repo_name)
1171 1169 if not target_perm:
1172 1170 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 1171 target_db_repo.repo_name))
1174 1172 h.flash(msg, category='error')
1175 1173 # copy the args back to redirect
1176 1174 org_query = self.request.GET.mixed()
1177 1175 raise HTTPFound(
1178 1176 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 1177 _query=org_query))
1180 1178
1181 1179 source_scm = source_db_repo.scm_instance()
1182 1180 target_scm = target_db_repo.scm_instance()
1183 1181
1184 1182 source_ref_obj = unicode_to_reference(source_ref)
1185 1183 target_ref_obj = unicode_to_reference(target_ref)
1186 1184
1187 1185 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 1186 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 1187
1190 1188 ancestor = source_scm.get_common_ancestor(
1191 1189 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 1190
1193 1191 # recalculate target ref based on ancestor
1194 1192 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 1193
1196 1194 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 1195 PullRequestModel().get_reviewer_functions()
1198 1196
1199 1197 # recalculate reviewers logic, to make sure we can validate this
1200 1198 reviewer_rules = get_default_reviewers_data(
1201 1199 self._rhodecode_db_user,
1202 1200 source_db_repo,
1203 1201 source_ref_obj,
1204 1202 target_db_repo,
1205 1203 target_ref_obj,
1206 1204 include_diff_info=False)
1207 1205
1208 1206 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 1207 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 1208
1211 1209 pullrequest_title = _form['pullrequest_title']
1212 1210 title_source_ref = source_ref_obj.name
1213 1211 if not pullrequest_title:
1214 1212 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 1213 source=source_repo,
1216 1214 source_ref=title_source_ref,
1217 1215 target=target_repo
1218 1216 )
1219 1217
1220 1218 description = _form['pullrequest_desc']
1221 1219 description_renderer = _form['description_renderer']
1222 1220
1223 1221 try:
1224 1222 pull_request = PullRequestModel().create(
1225 1223 created_by=self._rhodecode_user.user_id,
1226 1224 source_repo=source_repo,
1227 1225 source_ref=source_ref,
1228 1226 target_repo=target_repo,
1229 1227 target_ref=target_ref,
1230 1228 revisions=commit_ids,
1231 1229 common_ancestor_id=common_ancestor_id,
1232 1230 reviewers=reviewers,
1233 1231 observers=observers,
1234 1232 title=pullrequest_title,
1235 1233 description=description,
1236 1234 description_renderer=description_renderer,
1237 1235 reviewer_data=reviewer_rules,
1238 1236 auth_user=self._rhodecode_user
1239 1237 )
1240 1238 Session().commit()
1241 1239
1242 1240 h.flash(_('Successfully opened new pull request'),
1243 1241 category='success')
1244 1242 except Exception:
1245 1243 msg = _('Error occurred during creation of this pull request.')
1246 1244 log.exception(msg)
1247 1245 h.flash(msg, category='error')
1248 1246
1249 1247 # copy the args back to redirect
1250 1248 org_query = self.request.GET.mixed()
1251 1249 raise HTTPFound(
1252 1250 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 1251 _query=org_query))
1254 1252
1255 1253 raise HTTPFound(
1256 1254 h.route_path('pullrequest_show', repo_name=target_repo,
1257 1255 pull_request_id=pull_request.pull_request_id))
1258 1256
1259 1257 @LoginRequired()
1260 1258 @NotAnonymous()
1261 1259 @HasRepoPermissionAnyDecorator(
1262 1260 'repository.read', 'repository.write', 'repository.admin')
1263 1261 @CSRFRequired()
1264 1262 def pull_request_update(self):
1265 1263 pull_request = PullRequest.get_or_404(
1266 1264 self.request.matchdict['pull_request_id'])
1267 1265 _ = self.request.translate
1268 1266
1269 1267 c = self.load_default_context()
1270 1268 redirect_url = None
1271 1269 # we do this check as first, because we want to know ASAP in the flow that
1272 1270 # pr is updating currently
1273 1271 is_state_changing = pull_request.is_state_changing()
1274 1272
1275 1273 if pull_request.is_closed():
1276 1274 log.debug('update: forbidden because pull request is closed')
1277 msg = _(u'Cannot update closed pull requests.')
1275 msg = _('Cannot update closed pull requests.')
1278 1276 h.flash(msg, category='error')
1279 1277 return {'response': True,
1280 1278 'redirect_url': redirect_url}
1281 1279
1282 1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283 1281
1284 1282 # only owner or admin can update it
1285 1283 allowed_to_update = PullRequestModel().check_user_update(
1286 1284 pull_request, self._rhodecode_user)
1287 1285
1288 1286 if allowed_to_update:
1289 1287 controls = peppercorn.parse(self.request.POST.items())
1290 1288 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1291 1289 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1292 1290
1293 1291 if 'review_members' in controls:
1294 1292 self._update_reviewers(
1295 1293 c,
1296 1294 pull_request, controls['review_members'],
1297 1295 pull_request.reviewer_data,
1298 1296 PullRequestReviewers.ROLE_REVIEWER)
1299 1297 elif 'observer_members' in controls:
1300 1298 self._update_reviewers(
1301 1299 c,
1302 1300 pull_request, controls['observer_members'],
1303 1301 pull_request.reviewer_data,
1304 1302 PullRequestReviewers.ROLE_OBSERVER)
1305 1303 elif do_update_commits:
1306 1304 if is_state_changing:
1307 1305 log.debug('commits update: forbidden because pull request is in state %s',
1308 1306 pull_request.pull_request_state)
1309 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1310 u'Current state is: `{}`').format(
1307 msg = _('Cannot update pull requests commits in state other than `{}`. '
1308 'Current state is: `{}`').format(
1311 1309 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1312 1310 h.flash(msg, category='error')
1313 1311 return {'response': True,
1314 1312 'redirect_url': redirect_url}
1315 1313
1316 1314 self._update_commits(c, pull_request)
1317 1315 if force_refresh:
1318 1316 redirect_url = h.route_path(
1319 1317 'pullrequest_show', repo_name=self.db_repo_name,
1320 1318 pull_request_id=pull_request.pull_request_id,
1321 1319 _query={"force_refresh": 1})
1322 1320 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1323 1321 self._edit_pull_request(pull_request)
1324 1322 else:
1325 1323 log.error('Unhandled update data.')
1326 1324 raise HTTPBadRequest()
1327 1325
1328 1326 return {'response': True,
1329 1327 'redirect_url': redirect_url}
1330 1328 raise HTTPForbidden()
1331 1329
1332 1330 def _edit_pull_request(self, pull_request):
1333 1331 """
1334 1332 Edit title and description
1335 1333 """
1336 1334 _ = self.request.translate
1337 1335
1338 1336 try:
1339 1337 PullRequestModel().edit(
1340 1338 pull_request,
1341 1339 self.request.POST.get('title'),
1342 1340 self.request.POST.get('description'),
1343 1341 self.request.POST.get('description_renderer'),
1344 1342 self._rhodecode_user)
1345 1343 except ValueError:
1346 msg = _(u'Cannot update closed pull requests.')
1344 msg = _('Cannot update closed pull requests.')
1347 1345 h.flash(msg, category='error')
1348 1346 return
1349 1347 else:
1350 1348 Session().commit()
1351 1349
1352 msg = _(u'Pull request title & description updated.')
1350 msg = _('Pull request title & description updated.')
1353 1351 h.flash(msg, category='success')
1354 1352 return
1355 1353
1356 1354 def _update_commits(self, c, pull_request):
1357 1355 _ = self.request.translate
1358 1356 log.debug('pull-request: running update commits actions')
1359 1357
1360 1358 @retry(exception=Exception, n_tries=3, delay=2)
1361 1359 def commits_update():
1362 1360 return PullRequestModel().update_commits(
1363 1361 pull_request, self._rhodecode_db_user)
1364 1362
1365 1363 with pull_request.set_state(PullRequest.STATE_UPDATING):
1366 1364 resp = commits_update() # retry x3
1367 1365
1368 1366 if resp.executed:
1369 1367
1370 1368 if resp.target_changed and resp.source_changed:
1371 1369 changed = 'target and source repositories'
1372 1370 elif resp.target_changed and not resp.source_changed:
1373 1371 changed = 'target repository'
1374 1372 elif not resp.target_changed and resp.source_changed:
1375 1373 changed = 'source repository'
1376 1374 else:
1377 1375 changed = 'nothing'
1378 1376
1379 msg = _(u'Pull request updated to "{source_commit_id}" with '
1380 u'{count_added} added, {count_removed} removed commits. '
1381 u'Source of changes: {change_source}.')
1377 msg = _('Pull request updated to "{source_commit_id}" with '
1378 '{count_added} added, {count_removed} removed commits. '
1379 'Source of changes: {change_source}.')
1382 1380 msg = msg.format(
1383 1381 source_commit_id=pull_request.source_ref_parts.commit_id,
1384 1382 count_added=len(resp.changes.added),
1385 1383 count_removed=len(resp.changes.removed),
1386 1384 change_source=changed)
1387 1385 h.flash(msg, category='success')
1388 1386 channelstream.pr_update_channelstream_push(
1389 1387 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 1388 else:
1391 1389 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1392 1390 warning_reasons = [
1393 1391 UpdateFailureReason.NO_CHANGE,
1394 1392 UpdateFailureReason.WRONG_REF_TYPE,
1395 1393 ]
1396 1394 category = 'warning' if resp.reason in warning_reasons else 'error'
1397 1395 h.flash(msg, category=category)
1398 1396
1399 1397 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1400 1398 _ = self.request.translate
1401 1399
1402 1400 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1403 1401 PullRequestModel().get_reviewer_functions()
1404 1402
1405 1403 if role == PullRequestReviewers.ROLE_REVIEWER:
1406 1404 try:
1407 1405 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1408 1406 except ValueError as e:
1409 log.error('Reviewers Validation: {}'.format(e))
1407 log.error(f'Reviewers Validation: {e}')
1410 1408 h.flash(e, category='error')
1411 1409 return
1412 1410
1413 1411 old_calculated_status = pull_request.calculated_review_status()
1414 1412 PullRequestModel().update_reviewers(
1415 1413 pull_request, reviewers, self._rhodecode_db_user)
1416 1414
1417 1415 Session().commit()
1418 1416
1419 1417 msg = _('Pull request reviewers updated.')
1420 1418 h.flash(msg, category='success')
1421 1419 channelstream.pr_update_channelstream_push(
1422 1420 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1423 1421
1424 1422 # trigger status changed if change in reviewers changes the status
1425 1423 calculated_status = pull_request.calculated_review_status()
1426 1424 if old_calculated_status != calculated_status:
1427 1425 PullRequestModel().trigger_pull_request_hook(
1428 1426 pull_request, self._rhodecode_user, 'review_status_change',
1429 1427 data={'status': calculated_status})
1430 1428
1431 1429 elif role == PullRequestReviewers.ROLE_OBSERVER:
1432 1430 try:
1433 1431 observers = validate_observers(review_members, reviewer_rules)
1434 1432 except ValueError as e:
1435 log.error('Observers Validation: {}'.format(e))
1433 log.error(f'Observers Validation: {e}')
1436 1434 h.flash(e, category='error')
1437 1435 return
1438 1436
1439 1437 PullRequestModel().update_observers(
1440 1438 pull_request, observers, self._rhodecode_db_user)
1441 1439
1442 1440 Session().commit()
1443 1441 msg = _('Pull request observers updated.')
1444 1442 h.flash(msg, category='success')
1445 1443 channelstream.pr_update_channelstream_push(
1446 1444 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1447 1445
1448 1446 @LoginRequired()
1449 1447 @NotAnonymous()
1450 1448 @HasRepoPermissionAnyDecorator(
1451 1449 'repository.read', 'repository.write', 'repository.admin')
1452 1450 @CSRFRequired()
1453 1451 def pull_request_merge(self):
1454 1452 """
1455 1453 Merge will perform a server-side merge of the specified
1456 1454 pull request, if the pull request is approved and mergeable.
1457 1455 After successful merging, the pull request is automatically
1458 1456 closed, with a relevant comment.
1459 1457 """
1460 1458 pull_request = PullRequest.get_or_404(
1461 1459 self.request.matchdict['pull_request_id'])
1462 1460 _ = self.request.translate
1463 1461
1464 1462 if pull_request.is_state_changing():
1465 1463 log.debug('show: forbidden because pull request is in state %s',
1466 1464 pull_request.pull_request_state)
1467 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1468 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1465 msg = _('Cannot merge pull requests in state other than `{}`. '
1466 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1469 1467 pull_request.pull_request_state)
1470 1468 h.flash(msg, category='error')
1471 1469 raise HTTPFound(
1472 1470 h.route_path('pullrequest_show',
1473 1471 repo_name=pull_request.target_repo.repo_name,
1474 1472 pull_request_id=pull_request.pull_request_id))
1475 1473
1476 1474 self.load_default_context()
1477 1475
1478 1476 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 1477 check = MergeCheck.validate(
1480 1478 pull_request, auth_user=self._rhodecode_user,
1481 1479 translator=self.request.translate)
1482 1480 merge_possible = not check.failed
1483 1481
1484 1482 for err_type, error_msg in check.errors:
1485 1483 h.flash(error_msg, category=err_type)
1486 1484
1487 1485 if merge_possible:
1488 1486 log.debug("Pre-conditions checked, trying to merge.")
1489 1487 extras = vcs_operation_context(
1490 1488 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1491 1489 username=self._rhodecode_db_user.username, action='push',
1492 1490 scm=pull_request.target_repo.repo_type)
1493 1491 with pull_request.set_state(PullRequest.STATE_UPDATING):
1494 1492 self._merge_pull_request(
1495 1493 pull_request, self._rhodecode_db_user, extras)
1496 1494 else:
1497 1495 log.debug("Pre-conditions failed, NOT merging.")
1498 1496
1499 1497 raise HTTPFound(
1500 1498 h.route_path('pullrequest_show',
1501 1499 repo_name=pull_request.target_repo.repo_name,
1502 1500 pull_request_id=pull_request.pull_request_id))
1503 1501
1504 1502 def _merge_pull_request(self, pull_request, user, extras):
1505 1503 _ = self.request.translate
1506 1504 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1507 1505
1508 1506 if merge_resp.executed:
1509 1507 log.debug("The merge was successful, closing the pull request.")
1510 1508 PullRequestModel().close_pull_request(
1511 1509 pull_request.pull_request_id, user)
1512 1510 Session().commit()
1513 1511 msg = _('Pull request was successfully merged and closed.')
1514 1512 h.flash(msg, category='success')
1515 1513 else:
1516 1514 log.debug(
1517 1515 "The merge was not successful. Merge response: %s", merge_resp)
1518 1516 msg = merge_resp.merge_status_message
1519 1517 h.flash(msg, category='error')
1520 1518
1521 1519 @LoginRequired()
1522 1520 @NotAnonymous()
1523 1521 @HasRepoPermissionAnyDecorator(
1524 1522 'repository.read', 'repository.write', 'repository.admin')
1525 1523 @CSRFRequired()
1526 1524 def pull_request_delete(self):
1527 1525 _ = self.request.translate
1528 1526
1529 1527 pull_request = PullRequest.get_or_404(
1530 1528 self.request.matchdict['pull_request_id'])
1531 1529 self.load_default_context()
1532 1530
1533 1531 pr_closed = pull_request.is_closed()
1534 1532 allowed_to_delete = PullRequestModel().check_user_delete(
1535 1533 pull_request, self._rhodecode_user) and not pr_closed
1536 1534
1537 1535 # only owner can delete it !
1538 1536 if allowed_to_delete:
1539 1537 PullRequestModel().delete(pull_request, self._rhodecode_user)
1540 1538 Session().commit()
1541 1539 h.flash(_('Successfully deleted pull request'),
1542 1540 category='success')
1543 1541 raise HTTPFound(h.route_path('pullrequest_show_all',
1544 1542 repo_name=self.db_repo_name))
1545 1543
1546 1544 log.warning('user %s tried to delete pull request without access',
1547 1545 self._rhodecode_user)
1548 1546 raise HTTPNotFound()
1549 1547
1550 1548 def _pull_request_comments_create(self, pull_request, comments):
1551 1549 _ = self.request.translate
1552 1550 data = {}
1553 1551 if not comments:
1554 1552 return
1555 1553 pull_request_id = pull_request.pull_request_id
1556 1554
1557 1555 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1558 1556
1559 1557 for entry in comments:
1560 1558 c = self.load_default_context()
1561 1559 comment_type = entry['comment_type']
1562 1560 text = entry['text']
1563 1561 status = entry['status']
1564 1562 is_draft = str2bool(entry['is_draft'])
1565 1563 resolves_comment_id = entry['resolves_comment_id']
1566 1564 close_pull_request = entry['close_pull_request']
1567 1565 f_path = entry['f_path']
1568 1566 line_no = entry['line']
1569 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
1567 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
1570 1568
1571 1569 # the logic here should work like following, if we submit close
1572 1570 # pr comment, use `close_pull_request_with_comment` function
1573 1571 # else handle regular comment logic
1574 1572
1575 1573 if close_pull_request:
1576 1574 # only owner or admin or person with write permissions
1577 1575 allowed_to_close = PullRequestModel().check_user_update(
1578 1576 pull_request, self._rhodecode_user)
1579 1577 if not allowed_to_close:
1580 1578 log.debug('comment: forbidden because not allowed to close '
1581 1579 'pull request %s', pull_request_id)
1582 1580 raise HTTPForbidden()
1583 1581
1584 1582 # This also triggers `review_status_change`
1585 1583 comment, status = PullRequestModel().close_pull_request_with_comment(
1586 1584 pull_request, self._rhodecode_user, self.db_repo, message=text,
1587 1585 auth_user=self._rhodecode_user)
1588 1586 Session().flush()
1589 1587 is_inline = comment.is_inline
1590 1588
1591 1589 PullRequestModel().trigger_pull_request_hook(
1592 1590 pull_request, self._rhodecode_user, 'comment',
1593 1591 data={'comment': comment})
1594 1592
1595 1593 else:
1596 1594 # regular comment case, could be inline, or one with status.
1597 1595 # for that one we check also permissions
1598 1596 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1599 1597 allowed_to_change_status = PullRequestModel().check_user_change_status(
1600 1598 pull_request, self._rhodecode_user) and not is_draft
1601 1599
1602 1600 if status and allowed_to_change_status:
1603 1601 message = (_('Status change %(transition_icon)s %(status)s')
1604 1602 % {'transition_icon': '>',
1605 1603 'status': ChangesetStatus.get_status_lbl(status)})
1606 1604 text = text or message
1607 1605
1608 1606 comment = CommentsModel().create(
1609 1607 text=text,
1610 1608 repo=self.db_repo.repo_id,
1611 1609 user=self._rhodecode_user.user_id,
1612 1610 pull_request=pull_request,
1613 1611 f_path=f_path,
1614 1612 line_no=line_no,
1615 1613 status_change=(ChangesetStatus.get_status_lbl(status)
1616 1614 if status and allowed_to_change_status else None),
1617 1615 status_change_type=(status
1618 1616 if status and allowed_to_change_status else None),
1619 1617 comment_type=comment_type,
1620 1618 is_draft=is_draft,
1621 1619 resolves_comment_id=resolves_comment_id,
1622 1620 auth_user=self._rhodecode_user,
1623 1621 send_email=not is_draft, # skip notification for draft comments
1624 1622 )
1625 1623 is_inline = comment.is_inline
1626 1624
1627 1625 if allowed_to_change_status:
1628 1626 # calculate old status before we change it
1629 1627 old_calculated_status = pull_request.calculated_review_status()
1630 1628
1631 1629 # get status if set !
1632 1630 if status:
1633 1631 ChangesetStatusModel().set_status(
1634 1632 self.db_repo.repo_id,
1635 1633 status,
1636 1634 self._rhodecode_user.user_id,
1637 1635 comment,
1638 1636 pull_request=pull_request
1639 1637 )
1640 1638
1641 1639 Session().flush()
1642 1640 # this is somehow required to get access to some relationship
1643 1641 # loaded on comment
1644 1642 Session().refresh(comment)
1645 1643
1646 1644 # skip notifications for drafts
1647 1645 if not is_draft:
1648 1646 PullRequestModel().trigger_pull_request_hook(
1649 1647 pull_request, self._rhodecode_user, 'comment',
1650 1648 data={'comment': comment})
1651 1649
1652 1650 # we now calculate the status of pull request, and based on that
1653 1651 # calculation we set the commits status
1654 1652 calculated_status = pull_request.calculated_review_status()
1655 1653 if old_calculated_status != calculated_status:
1656 1654 PullRequestModel().trigger_pull_request_hook(
1657 1655 pull_request, self._rhodecode_user, 'review_status_change',
1658 1656 data={'status': calculated_status})
1659 1657
1660 1658 comment_id = comment.comment_id
1661 1659 data[comment_id] = {
1662 1660 'target_id': target_elem_id
1663 1661 }
1664 1662 Session().flush()
1665 1663
1666 1664 c.co = comment
1667 1665 c.at_version_num = None
1668 1666 c.is_new = True
1669 1667 rendered_comment = render(
1670 1668 'rhodecode:templates/changeset/changeset_comment_block.mako',
1671 1669 self._get_template_context(c), self.request)
1672 1670
1673 1671 data[comment_id].update(comment.get_dict())
1674 1672 data[comment_id].update({'rendered_text': rendered_comment})
1675 1673
1676 1674 Session().commit()
1677 1675
1678 1676 # skip channelstream for draft comments
1679 1677 if not all_drafts:
1680 1678 comment_broadcast_channel = channelstream.comment_channel(
1681 1679 self.db_repo_name, pull_request_obj=pull_request)
1682 1680
1683 1681 comment_data = data
1684 1682 posted_comment_type = 'inline' if is_inline else 'general'
1685 1683 if len(data) == 1:
1686 1684 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1687 1685 else:
1688 1686 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1689 1687
1690 1688 channelstream.comment_channelstream_push(
1691 1689 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1692 1690 comment_data=comment_data)
1693 1691
1694 1692 return data
1695 1693
1696 1694 @LoginRequired()
1697 1695 @NotAnonymous()
1698 1696 @HasRepoPermissionAnyDecorator(
1699 1697 'repository.read', 'repository.write', 'repository.admin')
1700 1698 @CSRFRequired()
1701 1699 def pull_request_comment_create(self):
1702 1700 _ = self.request.translate
1703 1701
1704 1702 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1705 1703
1706 1704 if pull_request.is_closed():
1707 1705 log.debug('comment: forbidden because pull request is closed')
1708 1706 raise HTTPForbidden()
1709 1707
1710 1708 allowed_to_comment = PullRequestModel().check_user_comment(
1711 1709 pull_request, self._rhodecode_user)
1712 1710 if not allowed_to_comment:
1713 1711 log.debug('comment: forbidden because pull request is from forbidden repo')
1714 1712 raise HTTPForbidden()
1715 1713
1716 1714 comment_data = {
1717 1715 'comment_type': self.request.POST.get('comment_type'),
1718 1716 'text': self.request.POST.get('text'),
1719 1717 'status': self.request.POST.get('changeset_status', None),
1720 1718 'is_draft': self.request.POST.get('draft'),
1721 1719 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1722 1720 'close_pull_request': self.request.POST.get('close_pull_request'),
1723 1721 'f_path': self.request.POST.get('f_path'),
1724 1722 'line': self.request.POST.get('line'),
1725 1723 }
1726 1724 data = self._pull_request_comments_create(pull_request, [comment_data])
1727 1725
1728 1726 return data
1729 1727
1730 1728 @LoginRequired()
1731 1729 @NotAnonymous()
1732 1730 @HasRepoPermissionAnyDecorator(
1733 1731 'repository.read', 'repository.write', 'repository.admin')
1734 1732 @CSRFRequired()
1735 1733 def pull_request_comment_delete(self):
1736 1734 pull_request = PullRequest.get_or_404(
1737 1735 self.request.matchdict['pull_request_id'])
1738 1736
1739 1737 comment = ChangesetComment.get_or_404(
1740 1738 self.request.matchdict['comment_id'])
1741 1739 comment_id = comment.comment_id
1742 1740
1743 1741 if comment.immutable:
1744 1742 # don't allow deleting comments that are immutable
1745 1743 raise HTTPForbidden()
1746 1744
1747 1745 if pull_request.is_closed():
1748 1746 log.debug('comment: forbidden because pull request is closed')
1749 1747 raise HTTPForbidden()
1750 1748
1751 1749 if not comment:
1752 1750 log.debug('Comment with id:%s not found, skipping', comment_id)
1753 1751 # comment already deleted in another call probably
1754 1752 return True
1755 1753
1756 1754 if comment.pull_request.is_closed():
1757 1755 # don't allow deleting comments on closed pull request
1758 1756 raise HTTPForbidden()
1759 1757
1760 1758 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1761 1759 super_admin = h.HasPermissionAny('hg.admin')()
1762 1760 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1763 1761 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1764 1762 comment_repo_admin = is_repo_admin and is_repo_comment
1765 1763
1766 1764 if comment.draft and not comment_owner:
1767 1765 # We never allow to delete draft comments for other than owners
1768 1766 raise HTTPNotFound()
1769 1767
1770 1768 if super_admin or comment_owner or comment_repo_admin:
1771 1769 old_calculated_status = comment.pull_request.calculated_review_status()
1772 1770 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1773 1771 Session().commit()
1774 1772 calculated_status = comment.pull_request.calculated_review_status()
1775 1773 if old_calculated_status != calculated_status:
1776 1774 PullRequestModel().trigger_pull_request_hook(
1777 1775 comment.pull_request, self._rhodecode_user, 'review_status_change',
1778 1776 data={'status': calculated_status})
1779 1777 return True
1780 1778 else:
1781 1779 log.warning('No permissions for user %s to delete comment_id: %s',
1782 1780 self._rhodecode_db_user, comment_id)
1783 1781 raise HTTPNotFound()
1784 1782
1785 1783 @LoginRequired()
1786 1784 @NotAnonymous()
1787 1785 @HasRepoPermissionAnyDecorator(
1788 1786 'repository.read', 'repository.write', 'repository.admin')
1789 1787 @CSRFRequired()
1790 1788 def pull_request_comment_edit(self):
1791 1789 self.load_default_context()
1792 1790
1793 1791 pull_request = PullRequest.get_or_404(
1794 1792 self.request.matchdict['pull_request_id']
1795 1793 )
1796 1794 comment = ChangesetComment.get_or_404(
1797 1795 self.request.matchdict['comment_id']
1798 1796 )
1799 1797 comment_id = comment.comment_id
1800 1798
1801 1799 if comment.immutable:
1802 1800 # don't allow deleting comments that are immutable
1803 1801 raise HTTPForbidden()
1804 1802
1805 1803 if pull_request.is_closed():
1806 1804 log.debug('comment: forbidden because pull request is closed')
1807 1805 raise HTTPForbidden()
1808 1806
1809 1807 if comment.pull_request.is_closed():
1810 1808 # don't allow deleting comments on closed pull request
1811 1809 raise HTTPForbidden()
1812 1810
1813 1811 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1814 1812 super_admin = h.HasPermissionAny('hg.admin')()
1815 1813 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1816 1814 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1817 1815 comment_repo_admin = is_repo_admin and is_repo_comment
1818 1816
1819 1817 if super_admin or comment_owner or comment_repo_admin:
1820 1818 text = self.request.POST.get('text')
1821 1819 version = self.request.POST.get('version')
1822 1820 if text == comment.text:
1823 1821 log.warning(
1824 1822 'Comment(PR): '
1825 1823 'Trying to create new version '
1826 1824 'with the same comment body {}'.format(
1827 1825 comment_id,
1828 1826 )
1829 1827 )
1830 1828 raise HTTPNotFound()
1831 1829
1832 1830 if version.isdigit():
1833 1831 version = int(version)
1834 1832 else:
1835 1833 log.warning(
1836 1834 'Comment(PR): Wrong version type {} {} '
1837 1835 'for comment {}'.format(
1838 1836 version,
1839 1837 type(version),
1840 1838 comment_id,
1841 1839 )
1842 1840 )
1843 1841 raise HTTPNotFound()
1844 1842
1845 1843 try:
1846 1844 comment_history = CommentsModel().edit(
1847 1845 comment_id=comment_id,
1848 1846 text=text,
1849 1847 auth_user=self._rhodecode_user,
1850 1848 version=version,
1851 1849 )
1852 1850 except CommentVersionMismatch:
1853 1851 raise HTTPConflict()
1854 1852
1855 1853 if not comment_history:
1856 1854 raise HTTPNotFound()
1857 1855
1858 1856 Session().commit()
1859 1857 if not comment.draft:
1860 1858 PullRequestModel().trigger_pull_request_hook(
1861 1859 pull_request, self._rhodecode_user, 'comment_edit',
1862 1860 data={'comment': comment})
1863 1861
1864 1862 return {
1865 1863 'comment_history_id': comment_history.comment_history_id,
1866 1864 'comment_id': comment.comment_id,
1867 1865 'comment_version': comment_history.version,
1868 1866 'comment_author_username': comment_history.author.username,
1869 1867 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1870 1868 'comment_created_on': h.age_component(comment_history.created_on,
1871 1869 time_is_local=True),
1872 1870 }
1873 1871 else:
1874 1872 log.warning('No permissions for user %s to edit comment_id: %s',
1875 1873 self._rhodecode_db_user, comment_id)
1876 1874 raise HTTPNotFound()
@@ -1,83 +1,81 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 from rhodecode.lib.vcs.backends.base import Reference
29 27 from rhodecode.model.db import Repository
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class RepoReviewRulesView(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_review_rules(self):
42 40 c = self.load_default_context()
43 41 c.active = 'reviewers'
44 42
45 43 return self._get_template_context(c)
46 44
47 45 @LoginRequired()
48 46 @HasRepoPermissionAnyDecorator(
49 47 'repository.read', 'repository.write', 'repository.admin')
50 48 def repo_default_reviewers_data(self):
51 49 self.load_default_context()
52 50
53 51 request = self.request
54 52 source_repo = self.db_repo
55 53 source_repo_name = source_repo.repo_name
56 54 target_repo_name = request.GET.get('target_repo', source_repo_name)
57 55 target_repo = Repository.get_by_repo_name(target_repo_name)
58 56
59 57 current_user = request.user.get_instance()
60 58
61 59 source_commit_id = request.GET['source_ref']
62 60 source_type = request.GET['source_ref_type']
63 61 source_name = request.GET['source_ref_name']
64 62
65 63 target_commit_id = request.GET['target_ref']
66 64 target_type = request.GET['target_ref_type']
67 65 target_name = request.GET['target_ref_name']
68 66
69 67 try:
70 68 review_data = get_default_reviewers_data(
71 69 current_user,
72 70 source_repo,
73 71 Reference(source_type, source_name, source_commit_id),
74 72 target_repo,
75 73 Reference(target_type, target_name, target_commit_id)
76 74 )
77 75 except ValueError:
78 76 # No common ancestor
79 77 msg = "No Common ancestor found between target and source reference"
80 78 log.exception(msg)
81 79 return {'diff_info': {'error': msg}}
82 80
83 81 return review_data
@@ -1,277 +1,275 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 import deform
24 22 from pyramid.httpexceptions import HTTPFound
25 23
26 24 from rhodecode import events
27 25 from rhodecode.apps._base import RepoAppView
28 26 from rhodecode.forms import RcForm
29 27 from rhodecode.lib import helpers as h
30 28 from rhodecode.lib import audit_logger
31 29 from rhodecode.lib.auth import (
32 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 31 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 32 from rhodecode.model.meta import Session
35 33 from rhodecode.model.permission import PermissionModel
36 34 from rhodecode.model.repo import RepoModel
37 35 from rhodecode.model.scm import RepoGroupList, ScmModel
38 36 from rhodecode.model.validation_schema.schemas import repo_schema
39 37
40 38 log = logging.getLogger(__name__)
41 39
42 40
43 41 class RepoSettingsView(RepoAppView):
44 42
45 43 def load_default_context(self):
46 44 c = self._get_local_tmpl_context()
47 45
48 46 acl_groups = RepoGroupList(
49 47 RepoGroup.query().all(),
50 48 perm_set=['group.write', 'group.admin'])
51 49 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
52 50 c.repo_groups_choices = list(map(lambda k: k[0], c.repo_groups))
53 51
54 52 # in case someone no longer have a group.write access to a repository
55 53 # pre fill the list with this entry, we don't care if this is the same
56 54 # but it will allow saving repo data properly.
57 55 repo_group = self.db_repo.group
58 56 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 57 c.repo_groups_choices.append(repo_group.group_id)
60 58 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61 59
62 60 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 61 # we might be in missing requirement state, so we load things
64 62 # without touching scm_instance()
65 63 c.landing_revs_choices, c.landing_revs = \
66 64 ScmModel().get_repo_landing_revs(self.request.translate)
67 65 else:
68 66 c.landing_revs_choices, c.landing_revs = \
69 67 ScmModel().get_repo_landing_revs(
70 68 self.request.translate, self.db_repo)
71 69
72 70 c.personal_repo_group = c.auth_user.personal_repo_group
73 71 c.repo_fields = RepositoryField.query()\
74 72 .filter(RepositoryField.repository == self.db_repo).all()
75 73 return c
76 74
77 75 def _get_schema(self, c, old_values=None):
78 76 return repo_schema.RepoSettingsSchema().bind(
79 77 repo_type=self.db_repo.repo_type,
80 78 repo_type_options=[self.db_repo.repo_type],
81 79 repo_ref_options=c.landing_revs_choices,
82 80 repo_ref_items=c.landing_revs,
83 81 repo_repo_group_options=c.repo_groups_choices,
84 82 repo_repo_group_items=c.repo_groups,
85 83 # user caller
86 84 user=self._rhodecode_user,
87 85 old_values=old_values
88 86 )
89 87
90 88 @LoginRequired()
91 89 @HasRepoPermissionAnyDecorator('repository.admin')
92 90 def edit_settings(self):
93 91 c = self.load_default_context()
94 92 c.active = 'settings'
95 93
96 94 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 95 defaults['repo_owner'] = defaults['user']
98 96 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99 97
100 98 schema = self._get_schema(c)
101 99 c.form = RcForm(schema, appstruct=defaults)
102 100 return self._get_template_context(c)
103 101
104 102 @LoginRequired()
105 103 @HasRepoPermissionAnyDecorator('repository.admin')
106 104 @CSRFRequired()
107 105 def edit_settings_update(self):
108 106 _ = self.request.translate
109 107 c = self.load_default_context()
110 108 c.active = 'settings'
111 109 old_repo_name = self.db_repo_name
112 110
113 111 old_values = self.db_repo.get_api_data()
114 112 schema = self._get_schema(c, old_values=old_values)
115 113
116 114 c.form = RcForm(schema)
117 115 pstruct = list(self.request.POST.items())
118 116 pstruct.append(('repo_type', self.db_repo.repo_type))
119 117 try:
120 118 schema_data = c.form.validate(pstruct)
121 119 except deform.ValidationFailure as err_form:
122 120 return self._get_template_context(c)
123 121
124 122 # data is now VALID, proceed with updates
125 123 # save validated data back into the updates dict
126 124 validated_updates = dict(
127 125 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 126 repo_group=schema_data['repo_group']['repo_group_id'],
129 127
130 128 user=schema_data['repo_owner'],
131 129 repo_description=schema_data['repo_description'],
132 130 repo_private=schema_data['repo_private'],
133 131 clone_uri=schema_data['repo_clone_uri'],
134 132 push_uri=schema_data['repo_push_uri'],
135 133 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 134 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 135 repo_enable_locking=schema_data['repo_enable_locking'],
138 136 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 137 )
140 138 # detect if SYNC URI changed, if we get OLD means we keep old values
141 139 if schema_data['repo_clone_uri_change'] == 'OLD':
142 140 validated_updates['clone_uri'] = self.db_repo.clone_uri
143 141
144 142 if schema_data['repo_push_uri_change'] == 'OLD':
145 143 validated_updates['push_uri'] = self.db_repo.push_uri
146 144
147 145 # use the new full name for redirect
148 146 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149 147
150 148 # save extra fields into our validated data
151 149 for key, value in pstruct:
152 150 if key.startswith(RepositoryField.PREFIX):
153 151 validated_updates[key] = value
154 152
155 153 try:
156 154 RepoModel().update(self.db_repo, **validated_updates)
157 155 ScmModel().mark_for_invalidation(new_repo_name)
158 156
159 157 audit_logger.store_web(
160 158 'repo.edit', action_data={'old_data': old_values},
161 159 user=self._rhodecode_user, repo=self.db_repo)
162 160
163 161 Session().commit()
164 162
165 163 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 164 category='success')
167 165 except Exception:
168 166 log.exception("Exception during update of repository")
169 167 h.flash(_('Error occurred during update of repository {}').format(
170 168 old_repo_name), category='error')
171 169
172 170 name_changed = old_repo_name != new_repo_name
173 171 if name_changed:
174 172 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 173 affected_user_ids = [perm['user_id'] for perm in current_perms]
176 174
177 175 # NOTE(marcink): also add owner maybe it has changed
178 176 owner = User.get_by_username(schema_data['repo_owner'])
179 177 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 178 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 179 PermissionModel().trigger_permission_flush(affected_user_ids)
182 180
183 181 raise HTTPFound(
184 182 h.route_path('edit_repo', repo_name=new_repo_name))
185 183
186 184 @LoginRequired()
187 185 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 186 def toggle_locking(self):
189 187 """
190 188 Toggle locking of repository by simple GET call to url
191 189 """
192 190 _ = self.request.translate
193 191 repo = self.db_repo
194 192
195 193 try:
196 194 if repo.enable_locking:
197 195 if repo.locked[0]:
198 196 Repository.unlock(repo)
199 197 action = _('Unlocked')
200 198 else:
201 199 Repository.lock(
202 200 repo, self._rhodecode_user.user_id,
203 201 lock_reason=Repository.LOCK_WEB)
204 202 action = _('Locked')
205 203
206 204 h.flash(_('Repository has been %s') % action,
207 205 category='success')
208 206 except Exception:
209 207 log.exception("Exception during unlocking")
210 208 h.flash(_('An error occurred during unlocking'),
211 209 category='error')
212 210 raise HTTPFound(
213 211 h.route_path('repo_summary', repo_name=self.db_repo_name))
214 212
215 213 @LoginRequired()
216 214 @HasRepoPermissionAnyDecorator('repository.admin')
217 215 def edit_statistics_form(self):
218 216 c = self.load_default_context()
219 217
220 218 if self.db_repo.stats:
221 219 # this is on what revision we ended up so we add +1 for count
222 220 last_rev = self.db_repo.stats.stat_on_revision + 1
223 221 else:
224 222 last_rev = 0
225 223
226 224 c.active = 'statistics'
227 225 c.stats_revision = last_rev
228 226 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229 227
230 228 if last_rev == 0 or c.repo_last_rev == 0:
231 229 c.stats_percentage = 0
232 230 else:
233 231 c.stats_percentage = '%.2f' % (
234 (float((last_rev)) / c.repo_last_rev) * 100)
232 (float(last_rev) / c.repo_last_rev) * 100)
235 233 return self._get_template_context(c)
236 234
237 235 @LoginRequired()
238 236 @HasRepoPermissionAnyDecorator('repository.admin')
239 237 @CSRFRequired()
240 238 def repo_statistics_reset(self):
241 239 _ = self.request.translate
242 240
243 241 try:
244 242 RepoModel().delete_stats(self.db_repo_name)
245 243 Session().commit()
246 244 except Exception:
247 245 log.exception('Edit statistics failure')
248 246 h.flash(_('An error occurred during deletion of repository stats'),
249 247 category='error')
250 248 raise HTTPFound(
251 249 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
252 250
253 251 @LoginRequired()
254 252 @HasRepoPermissionAnyDecorator('repository.admin')
255 253 def repo_settings_quick_actions(self):
256 254 _ = self.request.translate
257 255
258 256 set_lock = self.request.GET.get('set_lock')
259 257 set_unlock = self.request.GET.get('set_unlock')
260 258
261 259 try:
262 260 if set_lock:
263 261 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
264 262 lock_reason=Repository.LOCK_WEB)
265 263 h.flash(_('Locked repository'), category='success')
266 264 elif set_unlock:
267 265 Repository.unlock(self.db_repo)
268 266 h.flash(_('Unlocked repository'), category='success')
269 267 except Exception as e:
270 268 log.exception("Exception during unlocking")
271 269 h.flash(_('An error occurred during unlocking'), category='error')
272 270
273 271 raise HTTPFound(
274 272 h.route_path('repo_summary', repo_name=self.db_repo_name))
275 273
276 274
277 275
@@ -1,304 +1,302 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 from packaging.version import Version
26 24
27 25 from rhodecode import events
28 26 from rhodecode.apps._base import RepoAppView
29 27 from rhodecode.lib import helpers as h
30 28 from rhodecode.lib import audit_logger
31 29 from rhodecode.lib.auth import (
32 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
33 31 HasRepoPermissionAny)
34 32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
35 33 from rhodecode.lib.utils2 import safe_int
36 34 from rhodecode.lib.vcs import RepositoryError
37 35 from rhodecode.model.db import Session, UserFollowing, User, Repository
38 36 from rhodecode.model.permission import PermissionModel
39 37 from rhodecode.model.repo import RepoModel
40 38 from rhodecode.model.scm import ScmModel
41 39
42 40 log = logging.getLogger(__name__)
43 41
44 42
45 43 class RepoSettingsAdvancedView(RepoAppView):
46 44
47 45 def load_default_context(self):
48 46 c = self._get_local_tmpl_context()
49 47 return c
50 48
51 49 def _get_users_with_permissions(self):
52 50 user_permissions = {}
53 51 for perm in self.db_repo.permissions():
54 52 user_permissions[perm.user_id] = perm
55 53
56 54 return user_permissions
57 55
58 56 @LoginRequired()
59 57 @HasRepoPermissionAnyDecorator('repository.admin')
60 58 def edit_advanced(self):
61 59 _ = self.request.translate
62 60 c = self.load_default_context()
63 61 c.active = 'advanced'
64 62
65 63 c.default_user_id = User.get_default_user_id()
66 64 c.in_public_journal = UserFollowing.query() \
67 65 .filter(UserFollowing.user_id == c.default_user_id) \
68 66 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
69 67
70 68 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
71 69 c.hooks_outdated = False
72 70
73 71 try:
74 72 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
75 73 c.hooks_outdated = True
76 74 except Exception:
77 75 pass
78 76
79 77 # update commit cache if GET flag is present
80 78 if self.request.GET.get('update_commit_cache'):
81 79 self.db_repo.update_commit_cache()
82 80 h.flash(_('updated commit cache'), category='success')
83 81
84 82 return self._get_template_context(c)
85 83
86 84 @LoginRequired()
87 85 @HasRepoPermissionAnyDecorator('repository.admin')
88 86 @CSRFRequired()
89 87 def edit_advanced_archive(self):
90 88 """
91 89 Archives the repository. It will become read-only, and not visible in search
92 90 or other queries. But still visible for super-admins.
93 91 """
94 92
95 93 _ = self.request.translate
96 94
97 95 try:
98 96 old_data = self.db_repo.get_api_data()
99 97 RepoModel().archive(self.db_repo)
100 98
101 99 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
102 100 audit_logger.store_web(
103 101 'repo.archive', action_data={'old_data': old_data},
104 102 user=self._rhodecode_user, repo=repo)
105 103
106 104 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
107 105 h.flash(
108 106 _('Archived repository `%s`') % self.db_repo_name,
109 107 category='success')
110 108 Session().commit()
111 109 except Exception:
112 110 log.exception("Exception during archiving of repository")
113 111 h.flash(_('An error occurred during archiving of `%s`')
114 112 % self.db_repo_name, category='error')
115 113 # redirect to advanced for more deletion options
116 114 raise HTTPFound(
117 115 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
118 116 _anchor='advanced-archive'))
119 117
120 118 # flush permissions for all users defined in permissions
121 119 affected_user_ids = self._get_users_with_permissions().keys()
122 120 PermissionModel().trigger_permission_flush(affected_user_ids)
123 121
124 122 raise HTTPFound(h.route_path('home'))
125 123
126 124 @LoginRequired()
127 125 @HasRepoPermissionAnyDecorator('repository.admin')
128 126 @CSRFRequired()
129 127 def edit_advanced_delete(self):
130 128 """
131 129 Deletes the repository, or shows warnings if deletion is not possible
132 130 because of attached forks or other errors.
133 131 """
134 132 _ = self.request.translate
135 133 handle_forks = self.request.POST.get('forks', None)
136 134 if handle_forks == 'detach_forks':
137 135 handle_forks = 'detach'
138 136 elif handle_forks == 'delete_forks':
139 137 handle_forks = 'delete'
140 138
141 139 try:
142 140 old_data = self.db_repo.get_api_data()
143 141 RepoModel().delete(self.db_repo, forks=handle_forks)
144 142
145 143 _forks = self.db_repo.forks.count()
146 144 if _forks and handle_forks:
147 145 if handle_forks == 'detach_forks':
148 146 h.flash(_('Detached %s forks') % _forks, category='success')
149 147 elif handle_forks == 'delete_forks':
150 148 h.flash(_('Deleted %s forks') % _forks, category='success')
151 149
152 150 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
153 151 audit_logger.store_web(
154 152 'repo.delete', action_data={'old_data': old_data},
155 153 user=self._rhodecode_user, repo=repo)
156 154
157 155 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
158 156 h.flash(
159 157 _('Deleted repository `%s`') % self.db_repo_name,
160 158 category='success')
161 159 Session().commit()
162 160 except AttachedForksError:
163 161 repo_advanced_url = h.route_path(
164 162 'edit_repo_advanced', repo_name=self.db_repo_name,
165 163 _anchor='advanced-delete')
166 164 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
167 165 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
168 166 'Try using {delete_or_detach} option.')
169 167 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
170 168 category='warning')
171 169
172 170 # redirect to advanced for forks handle action ?
173 171 raise HTTPFound(repo_advanced_url)
174 172
175 173 except AttachedPullRequestsError:
176 174 repo_advanced_url = h.route_path(
177 175 'edit_repo_advanced', repo_name=self.db_repo_name,
178 176 _anchor='advanced-delete')
179 177 attached_prs = len(self.db_repo.pull_requests_source +
180 178 self.db_repo.pull_requests_target)
181 179 h.flash(
182 180 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
183 181 'Consider archiving the repository instead.').format(
184 182 repo=self.db_repo_name, num=attached_prs), category='warning')
185 183
186 184 # redirect to advanced for forks handle action ?
187 185 raise HTTPFound(repo_advanced_url)
188 186
189 187 except Exception:
190 188 log.exception("Exception during deletion of repository")
191 189 h.flash(_('An error occurred during deletion of `%s`')
192 190 % self.db_repo_name, category='error')
193 191 # redirect to advanced for more deletion options
194 192 raise HTTPFound(
195 193 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
196 194 _anchor='advanced-delete'))
197 195
198 196 raise HTTPFound(h.route_path('home'))
199 197
200 198 @LoginRequired()
201 199 @HasRepoPermissionAnyDecorator('repository.admin')
202 200 @CSRFRequired()
203 201 def edit_advanced_journal(self):
204 202 """
205 203 Set's this repository to be visible in public journal,
206 204 in other words making default user to follow this repo
207 205 """
208 206 _ = self.request.translate
209 207
210 208 try:
211 209 user_id = User.get_default_user_id()
212 210 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
213 211 h.flash(_('Updated repository visibility in public journal'),
214 212 category='success')
215 213 Session().commit()
216 214 except Exception:
217 215 h.flash(_('An error occurred during setting this '
218 216 'repository in public journal'),
219 217 category='error')
220 218
221 219 raise HTTPFound(
222 220 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
223 221
224 222 @LoginRequired()
225 223 @HasRepoPermissionAnyDecorator('repository.admin')
226 224 @CSRFRequired()
227 225 def edit_advanced_fork(self):
228 226 """
229 227 Mark given repository as a fork of another
230 228 """
231 229 _ = self.request.translate
232 230
233 231 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
234 232
235 233 # valid repo, re-check permissions
236 234 if new_fork_id:
237 235 repo = Repository.get(new_fork_id)
238 236 # ensure we have at least read access to the repo we mark
239 237 perm_check = HasRepoPermissionAny(
240 238 'repository.read', 'repository.write', 'repository.admin')
241 239
242 240 if repo and perm_check(repo_name=repo.repo_name):
243 241 new_fork_id = repo.repo_id
244 242 else:
245 243 new_fork_id = None
246 244
247 245 try:
248 246 repo = ScmModel().mark_as_fork(
249 247 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
250 248 fork = repo.fork.repo_name if repo.fork else _('Nothing')
251 249 Session().commit()
252 250 h.flash(
253 251 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
254 252 category='success')
255 253 except RepositoryError as e:
256 254 log.exception("Repository Error occurred")
257 255 h.flash(str(e), category='error')
258 256 except Exception:
259 257 log.exception("Exception while editing fork")
260 258 h.flash(_('An error occurred during this operation'),
261 259 category='error')
262 260
263 261 raise HTTPFound(
264 262 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
265 263
266 264 @LoginRequired()
267 265 @HasRepoPermissionAnyDecorator('repository.admin')
268 266 @CSRFRequired()
269 267 def edit_advanced_toggle_locking(self):
270 268 """
271 269 Toggle locking of repository
272 270 """
273 271 _ = self.request.translate
274 272 set_lock = self.request.POST.get('set_lock')
275 273 set_unlock = self.request.POST.get('set_unlock')
276 274
277 275 try:
278 276 if set_lock:
279 277 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
280 278 lock_reason=Repository.LOCK_WEB)
281 279 h.flash(_('Locked repository'), category='success')
282 280 elif set_unlock:
283 281 Repository.unlock(self.db_repo)
284 282 h.flash(_('Unlocked repository'), category='success')
285 283 except Exception as e:
286 284 log.exception("Exception during unlocking")
287 285 h.flash(_('An error occurred during unlocking'), category='error')
288 286
289 287 raise HTTPFound(
290 288 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
291 289
292 290 @LoginRequired()
293 291 @HasRepoPermissionAnyDecorator('repository.admin')
294 292 def edit_advanced_install_hooks(self):
295 293 """
296 294 Install Hooks for repository
297 295 """
298 296 _ = self.request.translate
299 297 self.load_default_context()
300 298 self.rhodecode_vcs_repo.install_hooks(force=True)
301 299 h.flash(_('installed updated hooks into this repository'),
302 300 category='success')
303 301 raise HTTPFound(
304 302 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,102 +1,100 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 import formencode
24 22 import formencode.htmlfill
25 23
26 24 from pyramid.httpexceptions import HTTPFound
27 25
28 26 from rhodecode.apps._base import RepoAppView
29 27 from rhodecode.lib import audit_logger
30 28 from rhodecode.lib import helpers as h
31 29 from rhodecode.lib.auth import (
32 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 31 from rhodecode.model.db import RepositoryField
34 32 from rhodecode.model.forms import RepoFieldForm
35 33 from rhodecode.model.meta import Session
36 34 from rhodecode.model.repo import RepoModel
37 35
38 36 log = logging.getLogger(__name__)
39 37
40 38
41 39 class RepoSettingsFieldsView(RepoAppView):
42 40 def load_default_context(self):
43 41 c = self._get_local_tmpl_context()
44 42
45 43
46 44 return c
47 45
48 46 @LoginRequired()
49 47 @HasRepoPermissionAnyDecorator('repository.admin')
50 48 def repo_field_edit(self):
51 49 c = self.load_default_context()
52 50
53 51 c.active = 'fields'
54 52 c.repo_fields = RepositoryField.query() \
55 53 .filter(RepositoryField.repository == self.db_repo).all()
56 54
57 55 return self._get_template_context(c)
58 56
59 57 @LoginRequired()
60 58 @HasRepoPermissionAnyDecorator('repository.admin')
61 59 @CSRFRequired()
62 60 def repo_field_create(self):
63 61 _ = self.request.translate
64 62
65 63 try:
66 64 form = RepoFieldForm(self.request.translate)()
67 65 form_result = form.to_python(dict(self.request.POST))
68 66 RepoModel().add_repo_field(
69 67 self.db_repo_name,
70 68 form_result['new_field_key'],
71 69 field_type=form_result['new_field_type'],
72 70 field_value=form_result['new_field_value'],
73 71 field_label=form_result['new_field_label'],
74 72 field_desc=form_result['new_field_desc'])
75 73
76 74 Session().commit()
77 75 except Exception as e:
78 76 log.exception("Exception creating field")
79 77 msg = _('An error occurred during creation of field')
80 78 if isinstance(e, formencode.Invalid):
81 79 msg += ". " + e.msg
82 80 h.flash(msg, category='error')
83 81
84 82 raise HTTPFound(
85 83 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
86 84
87 85 @LoginRequired()
88 86 @HasRepoPermissionAnyDecorator('repository.admin')
89 87 @CSRFRequired()
90 88 def repo_field_delete(self):
91 89 _ = self.request.translate
92 90 field = RepositoryField.get_or_404(self.request.matchdict['field_id'])
93 91 try:
94 92 RepoModel().delete_repo_field(self.db_repo_name, field.field_key)
95 93 Session().commit()
96 94 except Exception:
97 95 log.exception('Exception during removal of field')
98 96 msg = _('An error occurred during removal of field')
99 97 h.flash(msg, category='error')
100 98
101 99 raise HTTPFound(
102 100 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
@@ -1,125 +1,123 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 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 22
25 23 import formencode
26 24
27 25 from rhodecode.apps._base import RepoAppView
28 26 from rhodecode.lib import audit_logger
29 27 from rhodecode.lib import helpers as h
30 28 from rhodecode.lib.auth import (
31 29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 30 from rhodecode.model.forms import IssueTrackerPatternsForm
33 31 from rhodecode.model.meta import Session
34 32 from rhodecode.model.settings import SettingsModel
35 33
36 34 log = logging.getLogger(__name__)
37 35
38 36
39 37 class RepoSettingsIssueTrackersView(RepoAppView):
40 38 def load_default_context(self):
41 39 c = self._get_local_tmpl_context()
42 40 return c
43 41
44 42 @LoginRequired()
45 43 @HasRepoPermissionAnyDecorator('repository.admin')
46 44 def repo_issuetracker(self):
47 45 c = self.load_default_context()
48 46 c.active = 'issuetracker'
49 47 c.data = 'data'
50 48
51 49 c.settings_model = self.db_repo_patterns
52 50 c.global_patterns = c.settings_model.get_global_settings()
53 51 c.repo_patterns = c.settings_model.get_repo_settings()
54 52
55 53 return self._get_template_context(c)
56 54
57 55 @LoginRequired()
58 56 @HasRepoPermissionAnyDecorator('repository.admin')
59 57 @CSRFRequired()
60 58 def repo_issuetracker_test(self):
61 59 return h.urlify_commit_message(
62 60 self.request.POST.get('test_text', ''),
63 61 self.db_repo_name)
64 62
65 63 @LoginRequired()
66 64 @HasRepoPermissionAnyDecorator('repository.admin')
67 65 @CSRFRequired()
68 66 def repo_issuetracker_delete(self):
69 67 _ = self.request.translate
70 68 uid = self.request.POST.get('uid')
71 69 repo_settings = self.db_repo_patterns
72 70 try:
73 71 repo_settings.delete_entries(uid)
74 72 except Exception:
75 73 h.flash(_('Error occurred during deleting issue tracker entry'),
76 74 category='error')
77 75 raise HTTPNotFound()
78 76
79 77 SettingsModel().invalidate_settings_cache()
80 78 h.flash(_('Removed issue tracker entry.'), category='success')
81 79
82 80 return {'deleted': uid}
83 81
84 82 def _update_patterns(self, form, repo_settings):
85 83 for uid in form['delete_patterns']:
86 84 repo_settings.delete_entries(uid)
87 85
88 86 for pattern_data in form['patterns']:
89 87 for setting_key, pattern, type_ in pattern_data:
90 88 sett = repo_settings.create_or_update_setting(
91 89 setting_key, pattern.strip(), type_)
92 90 Session().add(sett)
93 91
94 92 Session().commit()
95 93
96 94 @LoginRequired()
97 95 @HasRepoPermissionAnyDecorator('repository.admin')
98 96 @CSRFRequired()
99 97 def repo_issuetracker_update(self):
100 98 _ = self.request.translate
101 99 # Save inheritance
102 100 repo_settings = self.db_repo_patterns
103 101 inherited = (
104 102 self.request.POST.get('inherit_global_issuetracker') == "inherited")
105 103 repo_settings.inherit_global_settings = inherited
106 104 Session().commit()
107 105
108 106 try:
109 107 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
110 108 except formencode.Invalid as errors:
111 109 log.exception('Failed to add new pattern')
112 110 error = errors
113 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
111 h.flash(_(f'Invalid issue tracker pattern: {error}'),
114 112 category='error')
115 113 raise HTTPFound(
116 114 h.route_path('edit_repo_issuetracker',
117 115 repo_name=self.db_repo_name))
118 116
119 117 if form:
120 118 self._update_patterns(form, repo_settings)
121 119
122 120 h.flash(_('Updated issue tracker entries'), category='success')
123 121 raise HTTPFound(
124 122 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
125 123
@@ -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 from pyramid.httpexceptions import HTTPFound
24 22
25 23
26 24 from rhodecode.apps._base import RepoAppView
27 25 from rhodecode.lib import helpers as h
28 26 from rhodecode.lib.auth import (
29 27 LoginRequired, CSRFRequired, HasRepoPermissionAnyDecorator)
30 28 from rhodecode.model.scm import ScmModel
31 29
32 30 log = logging.getLogger(__name__)
33 31
34 32
35 33 class RepoSettingsRemoteView(RepoAppView):
36 34 def load_default_context(self):
37 35 c = self._get_local_tmpl_context()
38 36 return c
39 37
40 38 @LoginRequired()
41 39 @HasRepoPermissionAnyDecorator('repository.admin')
42 40 def repo_remote_edit_form(self):
43 41 c = self.load_default_context()
44 42 c.active = 'remote'
45 43
46 44 return self._get_template_context(c)
47 45
48 46 @LoginRequired()
49 47 @HasRepoPermissionAnyDecorator('repository.admin')
50 48 @CSRFRequired()
51 49 def repo_remote_pull_changes(self):
52 50 _ = self.request.translate
53 51 self.load_default_context()
54 52
55 53 try:
56 54 ScmModel().pull_changes(
57 55 self.db_repo_name, self._rhodecode_user.username)
58 56 h.flash(_('Pulled from remote location'), category='success')
59 57 except Exception:
60 58 log.exception("Exception during pull from remote")
61 59 h.flash(_('An error occurred during pull from remote location'),
62 60 category='error')
63 61 raise HTTPFound(
64 62 h.route_path('edit_repo_remote', repo_name=self.db_repo_name))
@@ -1,274 +1,272 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 string
23 21 import time
24 22
25 23 import rhodecode
26 24
27 25
28 26
29 27 from rhodecode.lib.view_utils import get_format_ref_id
30 28 from rhodecode.apps._base import RepoAppView
31 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 30 from rhodecode.lib import helpers as h, rc_cache
33 31 from rhodecode.lib.utils2 import safe_str, safe_int
34 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 33 from rhodecode.lib.ext_json import json
36 34 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 35 from rhodecode.lib.vcs.exceptions import (
38 36 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 37 from rhodecode.model.db import Statistics, CacheKey, User
40 38 from rhodecode.model.meta import Session
41 39 from rhodecode.model.scm import ScmModel
42 40
43 41 log = logging.getLogger(__name__)
44 42
45 43
46 44 class RepoSummaryView(RepoAppView):
47 45
48 46 def load_default_context(self):
49 47 c = self._get_local_tmpl_context(include_app_defaults=True)
50 48 c.rhodecode_repo = None
51 49 if not c.repository_requirements_missing:
52 50 c.rhodecode_repo = self.rhodecode_vcs_repo
53 51 return c
54 52
55 53 def _load_commits_context(self, c):
56 54 p = safe_int(self.request.GET.get('page'), 1)
57 55 size = safe_int(self.request.GET.get('size'), 10)
58 56
59 57 def url_generator(page_num):
60 58 query_params = {
61 59 'page': page_num,
62 60 'size': size
63 61 }
64 62 return h.route_path(
65 63 'repo_summary_commits',
66 64 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67 65
68 66 pre_load = self.get_commit_preload_attrs()
69 67
70 68 try:
71 69 collection = self.rhodecode_vcs_repo.get_commits(
72 70 pre_load=pre_load, translate_tags=False)
73 71 except EmptyRepositoryError:
74 72 collection = self.rhodecode_vcs_repo
75 73
76 74 c.repo_commits = h.RepoPage(
77 75 collection, page=p, items_per_page=size, url_maker=url_generator)
78 76 page_ids = [x.raw_id for x in c.repo_commits]
79 77 c.comments = self.db_repo.get_comments(page_ids)
80 78 c.statuses = self.db_repo.statuses(page_ids)
81 79
82 80 @LoginRequired()
83 81 @HasRepoPermissionAnyDecorator(
84 82 'repository.read', 'repository.write', 'repository.admin')
85 83 def summary_commits(self):
86 84 c = self.load_default_context()
87 85 self._prepare_and_set_clone_url(c)
88 86 self._load_commits_context(c)
89 87 return self._get_template_context(c)
90 88
91 89 @LoginRequired()
92 90 @HasRepoPermissionAnyDecorator(
93 91 'repository.read', 'repository.write', 'repository.admin')
94 92 def summary(self):
95 93 c = self.load_default_context()
96 94
97 95 # Prepare the clone URL
98 96 self._prepare_and_set_clone_url(c)
99 97
100 98 # If enabled, get statistics data
101 99 c.show_stats = bool(self.db_repo.enable_statistics)
102 100
103 101 stats = Session().query(Statistics) \
104 102 .filter(Statistics.repository == self.db_repo) \
105 103 .scalar()
106 104
107 105 c.stats_percentage = 0
108 106
109 107 if stats and stats.languages:
110 108 c.no_data = False is self.db_repo.enable_statistics
111 109 lang_stats_d = json.loads(stats.languages)
112 110
113 111 # Sort first by decreasing count and second by the file extension,
114 112 # so we have a consistent output.
115 113 lang_stats_items = sorted(lang_stats_d.items(),
116 114 key=lambda k: (-k[1], k[0]))[:10]
117 115 lang_stats = [(x, {"count": y,
118 116 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
119 117 for x, y in lang_stats_items]
120 118
121 119 c.trending_languages = json.dumps(lang_stats)
122 120 else:
123 121 c.no_data = True
124 122 c.trending_languages = json.dumps({})
125 123
126 124 scm_model = ScmModel()
127 125 c.enable_downloads = self.db_repo.enable_downloads
128 126 c.repository_followers = scm_model.get_followers(self.db_repo)
129 127 c.repository_forks = scm_model.get_forks(self.db_repo)
130 128
131 129 # first interaction with the VCS instance after here...
132 130 if c.repository_requirements_missing:
133 131 self.request.override_renderer = \
134 132 'rhodecode:templates/summary/missing_requirements.mako'
135 133 return self._get_template_context(c)
136 134
137 135 c.readme_data, c.readme_file = \
138 136 self._get_readme_data(self.db_repo, c.visual.default_renderer)
139 137
140 138 # loads the summary commits template context
141 139 self._load_commits_context(c)
142 140
143 141 return self._get_template_context(c)
144 142
145 143 @LoginRequired()
146 144 @HasRepoPermissionAnyDecorator(
147 145 'repository.read', 'repository.write', 'repository.admin')
148 146 def repo_stats(self):
149 147 show_stats = bool(self.db_repo.enable_statistics)
150 148 repo_id = self.db_repo.repo_id
151 149
152 150 landing_commit = self.db_repo.get_landing_commit()
153 151 if isinstance(landing_commit, EmptyCommit):
154 152 return {'size': 0, 'code_stats': {}}
155 153
156 154 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
157 155 cache_on = cache_seconds > 0
158 156
159 157 log.debug(
160 158 'Computing REPO STATS for repo_id %s commit_id `%s` '
161 159 'with caching: %s[TTL: %ss]' % (
162 160 repo_id, landing_commit, cache_on, cache_seconds or 0))
163 161
164 cache_namespace_uid = 'repo.{}'.format(repo_id)
162 cache_namespace_uid = f'repo.{repo_id}'
165 163 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166 164
167 165 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
168 166 condition=cache_on)
169 167 def compute_stats(repo_id, commit_id, _show_stats):
170 168 code_stats = {}
171 169 size = 0
172 170 try:
173 171 commit = self.db_repo.get_commit(commit_id)
174 172
175 173 for node in commit.get_filenodes_generator():
176 174 size += node.size
177 175 if not _show_stats:
178 176 continue
179 177 ext = node.extension.lower()
180 178 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
181 179 if ext_info:
182 180 if ext in code_stats:
183 181 code_stats[ext]['count'] += 1
184 182 else:
185 183 code_stats[ext] = {"count": 1, "desc": ext_info}
186 184 except (EmptyRepositoryError, CommitDoesNotExistError):
187 185 pass
188 186 return {'size': h.format_byte_size_binary(size),
189 187 'code_stats': code_stats}
190 188
191 189 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
192 190 return stats
193 191
194 192 @LoginRequired()
195 193 @HasRepoPermissionAnyDecorator(
196 194 'repository.read', 'repository.write', 'repository.admin')
197 195 def repo_refs_data(self):
198 196 _ = self.request.translate
199 197 self.load_default_context()
200 198
201 199 repo = self.rhodecode_vcs_repo
202 200 refs_to_create = [
203 201 (_("Branch"), repo.branches, 'branch'),
204 202 (_("Tag"), repo.tags, 'tag'),
205 203 (_("Bookmark"), repo.bookmarks, 'book'),
206 204 ]
207 205 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
208 206 data = {
209 207 'more': False,
210 208 'results': res
211 209 }
212 210 return data
213 211
214 212 @LoginRequired()
215 213 @HasRepoPermissionAnyDecorator(
216 214 'repository.read', 'repository.write', 'repository.admin')
217 215 def repo_refs_changelog_data(self):
218 216 _ = self.request.translate
219 217 self.load_default_context()
220 218
221 219 repo = self.rhodecode_vcs_repo
222 220
223 221 refs_to_create = [
224 222 (_("Branches"), repo.branches, 'branch'),
225 223 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
226 224 # TODO: enable when vcs can handle bookmarks filters
227 225 # (_("Bookmarks"), repo.bookmarks, "book"),
228 226 ]
229 227 res = self._create_reference_data(
230 228 repo, self.db_repo_name, refs_to_create)
231 229 data = {
232 230 'more': False,
233 231 'results': res
234 232 }
235 233 return data
236 234
237 235 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
238 236 format_ref_id = get_format_ref_id(repo)
239 237
240 238 result = []
241 239 for title, refs, ref_type in refs_to_create:
242 240 if refs:
243 241 result.append({
244 242 'text': title,
245 243 'children': self._create_reference_items(
246 244 repo, full_repo_name, refs, ref_type,
247 245 format_ref_id),
248 246 })
249 247 return result
250 248
251 249 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
252 250 result = []
253 251 is_svn = h.is_svn(repo)
254 252 for ref_name, raw_id in refs.items():
255 253 files_url = self._create_files_url(
256 254 repo, full_repo_name, ref_name, raw_id, is_svn)
257 255 result.append({
258 256 'text': ref_name,
259 257 'id': format_ref_id(ref_name, raw_id),
260 258 'raw_id': raw_id,
261 259 'type': ref_type,
262 260 'files_url': files_url,
263 261 'idx': 0,
264 262 })
265 263 return result
266 264
267 265 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
268 266 use_commit_id = '/' in ref_name or is_svn
269 267 return h.route_path(
270 268 'repo_files',
271 269 repo_name=full_repo_name,
272 270 f_path=ref_name if is_svn else '',
273 271 commit_id=raw_id if use_commit_id else ref_name,
274 272 _query=dict(at=ref_name))
@@ -1,48 +1,46 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 rhodecode.apps._base import BaseReferencesView
24 22 from rhodecode.lib import ext_json
25 23 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26 24 from rhodecode.model.scm import ScmModel
27 25
28 26 log = logging.getLogger(__name__)
29 27
30 28
31 29 class RepoTagsView(BaseReferencesView):
32 30
33 31 @LoginRequired()
34 32 @HasRepoPermissionAnyDecorator(
35 33 'repository.read', 'repository.write', 'repository.admin')
36 34 def tags(self):
37 35 c = self.load_default_context()
38 36 self._prepare_and_set_clone_url(c)
39 37 c.rhodecode_repo = self.rhodecode_vcs_repo
40 38 c.repository_forks = ScmModel().get_forks(self.db_repo)
41 39
42 40 ref_items = self.rhodecode_vcs_repo.tags.items()
43 41 data = self.load_refs_context(
44 42 ref_items=ref_items, partials_template='tags/tags_data.mako')
45 43
46 44 c.has_references = bool(data)
47 45 c.data = ext_json.str_json(data)
48 46 return self._get_template_context(c)
@@ -1,62 +1,60 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 ADMIN_PREFIX
21 19
22 20
23 21 def includeme(config):
24 22 from rhodecode.apps.search.views import (
25 23 SearchView, SearchRepoView, SearchRepoGroupView)
26 24
27 25 config.add_route(
28 26 name='search',
29 27 pattern=ADMIN_PREFIX + '/search')
30 28 config.add_view(
31 29 SearchView,
32 30 attr='search',
33 31 route_name='search', request_method='GET',
34 32 renderer='rhodecode:templates/search/search.mako')
35 33
36 34 config.add_route(
37 35 name='search_repo',
38 36 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
39 37 config.add_view(
40 38 SearchRepoView,
41 39 attr='search_repo',
42 40 route_name='search_repo', request_method='GET',
43 41 renderer='rhodecode:templates/search/search.mako')
44 42
45 43 config.add_route(
46 44 name='search_repo_alt',
47 45 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
48 46 config.add_view(
49 47 SearchRepoView,
50 48 attr='search_repo',
51 49 route_name='search_repo_alt', request_method='GET',
52 50 renderer='rhodecode:templates/search/search.mako')
53 51
54 52 config.add_route(
55 53 name='search_repo_group',
56 54 pattern='/{repo_group_name:.*?[^/]}/_search',
57 55 repo_group_route=True)
58 56 config.add_view(
59 57 SearchRepoGroupView,
60 58 attr='search_repo_group',
61 59 route_name='search_repo_group', request_method='GET',
62 60 renderer='rhodecode:templates/search/search.mako')
@@ -1,202 +1,201 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 os
21 20
22 21 import mock
23 22 import pytest
24 23 from whoosh import query
25 24
26 25 from rhodecode.tests import (
27 26 TestController, route_path_generator, HG_REPO,
28 27 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
29 28 from rhodecode.tests.utils import AssertResponse
30 29
31 30
32 31 def route_path(name, params=None, **kwargs):
33 32 from rhodecode.apps._base import ADMIN_PREFIX
34 33 url_defs = {
35 34 'search':
36 35 ADMIN_PREFIX + '/search',
37 36 'search_repo':
38 37 '/{repo_name}/search',
39 38 }
40 39 return route_path_generator(url_defs, name=name, params=params, **kwargs)
41 40
42 41
43 42 class TestSearchController(TestController):
44 43
45 44 def test_index(self):
46 45 self.log_user()
47 46 response = self.app.get(route_path('search'))
48 47 assert_response = response.assert_response()
49 48 assert_response.one_element_exists('input#q')
50 49
51 50 def test_search_files_empty_search(self):
52 51 if os.path.isdir(self.index_location):
53 52 pytest.skip('skipped due to existing index')
54 53 else:
55 54 self.log_user()
56 55 response = self.app.get(route_path('search'),
57 56 {'q': HG_REPO})
58 57 response.mustcontain('There is no index to search in. '
59 58 'Please run whoosh indexer')
60 59
61 60 def test_search_validation(self):
62 61 self.log_user()
63 62 response = self.app.get(route_path('search'),
64 63 {'q': query, 'type': 'content', 'page_limit': 1000})
65 64
66 65 response.mustcontain(
67 66 'page_limit - 1000 is greater than maximum value 500')
68 67
69 68 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
70 69 ('todo', 23, [
71 70 'vcs/backends/hg/inmemory.py',
72 71 'vcs/tests/test_git.py']),
73 72 ('extension:rst installation', 6, [
74 73 'docs/index.rst',
75 74 'docs/installation.rst']),
76 75 ('def repo', 87, [
77 76 'vcs/tests/test_git.py',
78 77 'vcs/tests/test_changesets.py']),
79 78 ('repository:%s def test' % HG_REPO, 18, [
80 79 'vcs/tests/test_git.py',
81 80 'vcs/tests/test_changesets.py']),
82 81 ('"def main"', 9, [
83 82 'vcs/__init__.py',
84 83 'vcs/tests/__init__.py',
85 84 'vcs/utils/progressbar.py']),
86 85 ('owner:test_admin', 358, [
87 86 'vcs/tests/base.py',
88 87 'MANIFEST.in',
89 88 'vcs/utils/termcolors.py',
90 89 'docs/theme/ADC/static/documentation.png']),
91 90 ('owner:test_admin def main', 72, [
92 91 'vcs/__init__.py',
93 92 'vcs/tests/test_utils_filesize.py',
94 93 'vcs/tests/test_cli.py']),
95 94 ('owner:michał test', 0, []),
96 95 ])
97 96 def test_search_files(self, query, expected_hits, expected_paths):
98 97 self.log_user()
99 98 response = self.app.get(route_path('search'),
100 99 {'q': query, 'type': 'content', 'page_limit': 500})
101 100
102 101 response.mustcontain('%s results' % expected_hits)
103 102 for path in expected_paths:
104 103 response.mustcontain(path)
105 104
106 105 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
107 106 ('bother to ask where to fetch repo during tests', 3, [
108 107 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
109 108 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
110 109 ('svn', '98')]),
111 110 ('michał', 0, []),
112 111 ('changed:tests/utils.py', 36, [
113 112 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
114 113 ('changed:vcs/utils/archivers.py', 11, [
115 114 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
116 115 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
117 116 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
118 117 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
119 118 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
120 119 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
121 120 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
122 121 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
123 122 ('added:README.rst', 3, [
124 123 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
125 124 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
126 125 ('svn', '8')]),
127 126 ('changed:lazy.py', 15, [
128 127 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
129 128 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
130 129 ('svn', '82'),
131 130 ('svn', '262'),
132 131 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
133 132 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
134 133 ]),
135 134 ('author:marcin@python-blog.com '
136 135 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
137 136 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
138 137 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
139 138 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
140 139 ('b986218b', 1, [
141 140 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
142 141 ])
143 142 def test_search_commit_messages(
144 143 self, query, expected_hits, expected_commits, enabled_backends):
145 144 self.log_user()
146 145 response = self.app.get(route_path('search'),
147 146 {'q': query, 'type': 'commit', 'page_limit': 500})
148 147
149 148 response.mustcontain('%s results' % expected_hits)
150 149 for backend, commit_id in expected_commits:
151 150 if backend in enabled_backends:
152 151 response.mustcontain(commit_id)
153 152
154 153 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
155 154 ('readme.rst', 3, []),
156 155 ('test*', 75, []),
157 156 ('*model*', 1, []),
158 157 ('extension:rst', 48, []),
159 158 ('extension:rst api', 24, []),
160 159 ])
161 160 def test_search_file_paths(self, query, expected_hits, expected_paths):
162 161 self.log_user()
163 162 response = self.app.get(route_path('search'),
164 163 {'q': query, 'type': 'path', 'page_limit': 500})
165 164
166 165 response.mustcontain('%s results' % expected_hits)
167 166 for path in expected_paths:
168 167 response.mustcontain(path)
169 168
170 169 def test_search_commit_message_specific_repo(self, backend):
171 170 self.log_user()
172 171 response = self.app.get(
173 172 route_path('search_repo',repo_name=backend.repo_name),
174 173 {'q': 'bother to ask where to fetch repo during tests',
175 174 'type': 'commit'})
176 175
177 176 response.mustcontain('1 results')
178 177
179 178 def test_filters_are_not_applied_for_admin_user(self):
180 179 self.log_user()
181 180 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
182 181
183 182 self.app.get(route_path('search'),
184 183 {'q': 'test query', 'type': 'commit'})
185 184 assert search_mock.call_count == 1
186 185 _, kwargs = search_mock.call_args
187 186 assert kwargs['filter'] is None
188 187
189 188 def test_filters_are_applied_for_normal_user(self, enabled_backends):
190 189 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
191 190 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
192 191 self.app.get(route_path('search'),
193 192 {'q': 'test query', 'type': 'commit'})
194 193 assert search_mock.call_count == 1
195 194 _, kwargs = search_mock.call_args
196 195 assert isinstance(kwargs['filter'], query.Or)
197 196 expected_repositories = [
198 'vcs_test_{}'.format(b) for b in enabled_backends]
197 f'vcs_test_{b}' for b in enabled_backends]
199 198 queried_repositories = [
200 199 name for type_, name in kwargs['filter'].all_terms()]
201 200 for repository in expected_repositories:
202 201 assert repository in queried_repositories
@@ -1,168 +1,166 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 urllib.request
23 21 import urllib.parse
24 22 import urllib.error
25 23
26 24 from webhelpers2.html.tools import update_params
27 25
28 26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
29 27 from rhodecode.lib.auth import (
30 28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
31 29 from rhodecode.lib.helpers import Page
32 30 from rhodecode.lib.utils2 import safe_str
33 31 from rhodecode.lib.index import searcher_from_config
34 32 from rhodecode.model import validation_schema
35 33 from rhodecode.model.validation_schema.schemas import search_schema
36 34
37 35 log = logging.getLogger(__name__)
38 36
39 37
40 38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
41 39 searcher = searcher_from_config(request.registry.settings)
42 40 formatted_results = []
43 41 execution_time = ''
44 42
45 43 schema = search_schema.SearchParamsSchema()
46 44 search_tags = []
47 45 search_params = {}
48 46 errors = []
49 47
50 48 try:
51 49 search_params = schema.deserialize(
52 50 dict(
53 51 search_query=request.GET.get('q'),
54 52 search_type=request.GET.get('type'),
55 53 search_sort=request.GET.get('sort'),
56 54 search_max_lines=request.GET.get('max_lines'),
57 55 page_limit=request.GET.get('page_limit'),
58 56 requested_page=request.GET.get('page'),
59 57 )
60 58 )
61 59 except validation_schema.Invalid as e:
62 60 errors = e.children
63 61
64 62 def url_generator(page_num):
65 63
66 64 query_params = {
67 65 'page': page_num,
68 66 'q': safe_str(search_query),
69 67 'type': safe_str(search_type),
70 68 'max_lines': search_max_lines,
71 69 'sort': search_sort
72 70 }
73 71
74 72 return '?' + urllib.parse.urlencode(query_params)
75 73
76 74 c = tmpl_context
77 75 search_query = search_params.get('search_query')
78 76 search_type = search_params.get('search_type')
79 77 search_sort = search_params.get('search_sort')
80 78 search_max_lines = search_params.get('search_max_lines')
81 79 if search_params.get('search_query'):
82 80 page_limit = search_params['page_limit']
83 81 requested_page = search_params['requested_page']
84 82
85 83 try:
86 84 search_result = searcher.search(
87 85 search_query, search_type, c.auth_user, repo_name, repo_group_name,
88 86 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
89 87
90 88 formatted_results = Page(
91 89 search_result['results'], page=requested_page,
92 90 item_count=search_result['count'],
93 91 items_per_page=page_limit, url_maker=url_generator)
94 92 finally:
95 93 searcher.cleanup()
96 94
97 95 search_tags = searcher.extract_search_tags(search_query)
98 96
99 97 if not search_result['error']:
100 execution_time = '%s results (%.4f seconds)' % (
98 execution_time = '{} results ({:.4f} seconds)'.format(
101 99 search_result['count'],
102 100 search_result['runtime'])
103 101 elif not errors:
104 102 node = schema['search_query']
105 103 errors = [
106 104 validation_schema.Invalid(node, search_result['error'])]
107 105
108 106 c.perm_user = c.auth_user
109 107 c.repo_name = repo_name
110 108 c.repo_group_name = repo_group_name
111 109 c.errors = errors
112 110 c.formatted_results = formatted_results
113 111 c.runtime = execution_time
114 112 c.cur_query = search_query
115 113 c.search_type = search_type
116 114 c.searcher = searcher
117 115 c.search_tags = search_tags
118 116
119 117 direction, sort_field = searcher.get_sort(search_type, search_sort)
120 118 sort_definition = searcher.sort_def(search_type, direction, sort_field)
121 119 c.sort = ''
122 120 c.sort_tag = None
123 121 c.sort_tag_dir = direction
124 122 if sort_definition:
125 c.sort = '{}:{}'.format(direction, sort_field)
123 c.sort = f'{direction}:{sort_field}'
126 124 c.sort_tag = sort_field
127 125
128 126
129 127 class SearchView(BaseAppView):
130 128 def load_default_context(self):
131 129 c = self._get_local_tmpl_context()
132 130 return c
133 131
134 132 @LoginRequired()
135 133 def search(self):
136 134 c = self.load_default_context()
137 135 perform_search(self.request, c)
138 136 return self._get_template_context(c)
139 137
140 138
141 139 class SearchRepoView(RepoAppView):
142 140 def load_default_context(self):
143 141 c = self._get_local_tmpl_context()
144 142 c.active = 'search'
145 143 return c
146 144
147 145 @LoginRequired()
148 146 @HasRepoPermissionAnyDecorator(
149 147 'repository.read', 'repository.write', 'repository.admin')
150 148 def search_repo(self):
151 149 c = self.load_default_context()
152 150 perform_search(self.request, c, repo_name=self.db_repo_name)
153 151 return self._get_template_context(c)
154 152
155 153
156 154 class SearchRepoGroupView(RepoGroupAppView):
157 155 def load_default_context(self):
158 156 c = self._get_local_tmpl_context()
159 157 c.active = 'search'
160 158 return c
161 159
162 160 @LoginRequired()
163 161 @HasRepoGroupPermissionAnyDecorator(
164 162 'group.read', 'group.write', 'group.admin')
165 163 def search_repo_group(self):
166 164 c = self.load_default_context()
167 165 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
168 166 return self._get_template_context(c)
@@ -1,61 +1,59 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 . import config_keys
24 22 from .events import SshKeyFileChangeEvent
25 23 from .subscribers import generate_ssh_authorized_keys_file_subscriber
26 24
27 25 from rhodecode.config.settings_maker import SettingsMaker
28 26
29 27 log = logging.getLogger(__name__)
30 28
31 29
32 30 def _sanitize_settings_and_apply_defaults(settings):
33 31 """
34 32 Set defaults, convert to python types and validate settings.
35 33 """
36 34 settings_maker = SettingsMaker(settings)
37 35
38 36 settings_maker.make_setting(config_keys.generate_authorized_keyfile, False, parser='bool')
39 37 settings_maker.make_setting(config_keys.wrapper_allow_shell, False, parser='bool')
40 38 settings_maker.make_setting(config_keys.enable_debug_logging, False, parser='bool')
41 39 settings_maker.make_setting(config_keys.ssh_key_generator_enabled, True, parser='bool')
42 40
43 41 settings_maker.make_setting(config_keys.authorized_keys_file_path, '~/.ssh/authorized_keys_rhodecode')
44 42 settings_maker.make_setting(config_keys.wrapper_cmd, '')
45 43 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
46 44
47 45 settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg')
48 46 settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git')
49 47 settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve')
50 48
51 49 settings_maker.env_expand()
52 50
53 51
54 52 def includeme(config):
55 53 settings = config.registry.settings
56 54 _sanitize_settings_and_apply_defaults(settings)
57 55
58 56 # if we have enable generation of file, subscribe to event
59 57 if settings[config_keys.generate_authorized_keyfile]:
60 58 config.add_subscriber(
61 59 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
@@ -1,34 +1,32 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
22 20 # Definition of setting keys used to configure this module. Defined here to
23 21 # avoid repetition of keys throughout the module.
24 22 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
25 23 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
26 24 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
27 25 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
28 26 wrapper_cmd = 'ssh.wrapper_cmd'
29 27 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
30 28 enable_debug_logging = 'ssh.enable_debug_logging'
31 29
32 30 ssh_hg_bin = 'ssh.executable.hg'
33 31 ssh_git_bin = 'ssh.executable.git'
34 32 ssh_svn_bin = 'ssh.executable.svn'
@@ -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,163 +1,161 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 os
22 20 import sys
23 21 import logging
24 22
25 23 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 24 from rhodecode.lib.ext_json import sjson as json
27 25 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 26 from rhodecode.model.scm import ScmModel
29 27
30 28 log = logging.getLogger(__name__)
31 29
32 30
33 31 class VcsServer(object):
34 32 repo_user_agent = None # set in child classes
35 33 _path = None # set executable path for hg/git/svn binary
36 34 backend = None # set in child classes
37 35 tunnel = None # subprocess handling tunnel
38 36 write_perms = ['repository.admin', 'repository.write']
39 37 read_perms = ['repository.read', 'repository.admin', 'repository.write']
40 38
41 39 def __init__(self, user, user_permissions, config, env):
42 40 self.user = user
43 41 self.user_permissions = user_permissions
44 42 self.config = config
45 43 self.env = env
46 44 self.stdin = sys.stdin
47 45
48 46 self.repo_name = None
49 47 self.repo_mode = None
50 48 self.store = ''
51 49 self.ini_path = ''
52 50
53 51 def _invalidate_cache(self, repo_name):
54 52 """
55 53 Set's cache for this repository for invalidation on next access
56 54
57 55 :param repo_name: full repo name, also a cache key
58 56 """
59 57 ScmModel().mark_for_invalidation(repo_name)
60 58
61 59 def has_write_perm(self):
62 60 permission = self.user_permissions.get(self.repo_name)
63 61 if permission in ['repository.write', 'repository.admin']:
64 62 return True
65 63
66 64 return False
67 65
68 66 def _check_permissions(self, action):
69 67 permission = self.user_permissions.get(self.repo_name)
70 68 log.debug('permission for %s on %s are: %s',
71 69 self.user, self.repo_name, permission)
72 70
73 71 if not permission:
74 72 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 73 self.user, self.repo_name)
76 74 return -2
77 75
78 76 if action == 'pull':
79 77 if permission in self.read_perms:
80 78 log.info(
81 79 'READ Permissions for User "%s" detected to repo "%s"!',
82 80 self.user, self.repo_name)
83 81 return 0
84 82 else:
85 83 if permission in self.write_perms:
86 84 log.info(
87 85 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
88 86 self.user, self.repo_name)
89 87 return 0
90 88
91 89 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 90 'Permissions: %s, vcs action: %s',
93 91 self.user, permission, action)
94 92 return -2
95 93
96 94 def update_environment(self, action, extras=None):
97 95
98 96 scm_data = {
99 97 'ip': os.environ['SSH_CLIENT'].split()[0],
100 98 'username': self.user.username,
101 99 'user_id': self.user.user_id,
102 100 'action': action,
103 101 'repository': self.repo_name,
104 102 'scm': self.backend,
105 103 'config': self.ini_path,
106 104 'repo_store': self.store,
107 105 'make_lock': None,
108 106 'locked_by': [None, None],
109 107 'server_url': None,
110 'user_agent': '{}/ssh-user-agent'.format(self.repo_user_agent),
108 'user_agent': f'{self.repo_user_agent}/ssh-user-agent',
111 109 'hooks': ['push', 'pull'],
112 110 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 111 'is_shadow_repo': False,
114 112 'detect_force_push': False,
115 113 'check_branch_perms': False,
116 114
117 115 'SSH': True,
118 116 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 117 }
120 118 if extras:
121 119 scm_data.update(extras)
122 120 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123 121
124 122 def get_root_store(self):
125 123 root_store = self.store
126 124 if not root_store.endswith('/'):
127 125 # always append trailing slash
128 126 root_store = root_store + '/'
129 127 return root_store
130 128
131 129 def _handle_tunnel(self, extras):
132 130 # pre-auth
133 131 action = 'pull'
134 132 exit_code = self._check_permissions(action)
135 133 if exit_code:
136 134 return exit_code, False
137 135
138 136 req = self.env['request']
139 137 server_url = req.host_url + req.script_name
140 138 extras['server_url'] = server_url
141 139
142 140 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 141 exit_code = self.tunnel.run(extras)
144 142
145 143 return exit_code, action == "push"
146 144
147 145 def run(self, tunnel_extras=None):
148 146 tunnel_extras = tunnel_extras or {}
149 147 extras = {}
150 148 extras.update(tunnel_extras)
151 149
152 150 callback_daemon, extras = prepare_callback_daemon(
153 151 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 152 host=vcs_settings.HOOKS_HOST,
155 153 use_direct_calls=False)
156 154
157 155 with callback_daemon:
158 156 try:
159 157 return self._handle_tunnel(extras)
160 158 finally:
161 159 log.debug('Running cleanup with cache invalidation')
162 160 if self.repo_name:
163 161 self._invalidate_cache(self.repo_name)
@@ -1,75 +1,73 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 os
22 20 import sys
23 21 import logging
24 22
25 23 from .base import VcsServer
26 24
27 25 log = logging.getLogger(__name__)
28 26
29 27
30 28 class GitTunnelWrapper(object):
31 29 process = None
32 30
33 31 def __init__(self, server):
34 32 self.server = server
35 33 self.stdin = sys.stdin
36 34 self.stdout = sys.stdout
37 35
38 36 def create_hooks_env(self):
39 37 pass
40 38
41 39 def command(self):
42 40 root = self.server.get_root_store()
43 41 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 42 root=root, git_path=self.server.git_path,
45 43 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 44 log.debug("Final CMD: %s", command)
47 45 return command
48 46
49 47 def run(self, extras):
50 48 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 49 exit_code = self.server._check_permissions(action)
52 50 if exit_code:
53 51 return exit_code
54 52
55 53 self.server.update_environment(action=action, extras=extras)
56 54 self.create_hooks_env()
57 55 return os.system(self.command())
58 56
59 57
60 58 class GitServer(VcsServer):
61 59 backend = 'git'
62 60 repo_user_agent = 'git'
63 61
64 62 def __init__(self, store, ini_path, repo_name, repo_mode,
65 63 user, user_permissions, config, env):
66 super(GitServer, self).\
64 super().\
67 65 __init__(user, user_permissions, config, env)
68 66
69 67 self.store = store
70 68 self.ini_path = ini_path
71 69 self.repo_name = repo_name
72 70 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
73 71
74 72 self.repo_mode = repo_mode
75 73 self.tunnel = GitTunnelWrapper(server=self)
@@ -1,148 +1,146 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 os
22 20 import sys
23 21 import logging
24 22 import tempfile
25 23 import textwrap
26 24 import collections
27 25 from .base import VcsServer
28 26 from rhodecode.model.db import RhodeCodeUi
29 27 from rhodecode.model.settings import VcsSettingsModel
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class MercurialTunnelWrapper(object):
35 33 process = None
36 34
37 35 def __init__(self, server):
38 36 self.server = server
39 37 self.stdin = sys.stdin
40 38 self.stdout = sys.stdout
41 39 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
42 40
43 41 def create_hooks_env(self):
44 42 repo_name = self.server.repo_name
45 43 hg_flags = self.server.config_to_hgrc(repo_name)
46 44
47 45 content = textwrap.dedent(
48 46 '''
49 47 # RhodeCode SSH hooks version=2.0.0
50 48 {custom}
51 49 '''
52 50 ).format(custom='\n'.join(hg_flags))
53 51
54 52 root = self.server.get_root_store()
55 53 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
56 54 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
57 55
58 56 # cleanup custom hgrc file
59 57 if os.path.isfile(hgrc_custom):
60 58 with open(hgrc_custom, 'wb') as f:
61 59 f.write('')
62 60 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
63 61
64 62 # write temp
65 63 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
66 64 hooks_env_file.write(content)
67 65
68 66 return self.hooks_env_path
69 67
70 68 def remove_configs(self):
71 69 os.remove(self.hooks_env_path)
72 70
73 71 def command(self, hgrc_path):
74 72 root = self.server.get_root_store()
75 73
76 74 command = (
77 75 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
78 76 "serve --stdio".format(
79 77 root=root, hg_path=self.server.hg_path,
80 78 repo_name=self.server.repo_name, hgrc=hgrc_path))
81 79 log.debug("Final CMD: %s", command)
82 80 return command
83 81
84 82 def run(self, extras):
85 83 # at this point we cannot tell, we do further ACL checks
86 84 # inside the hooks
87 85 action = '?'
88 86 # permissions are check via `pre_push_ssh_auth` hook
89 87 self.server.update_environment(action=action, extras=extras)
90 88 custom_hgrc_file = self.create_hooks_env()
91 89
92 90 try:
93 91 return os.system(self.command(custom_hgrc_file))
94 92 finally:
95 93 self.remove_configs()
96 94
97 95
98 96 class MercurialServer(VcsServer):
99 97 backend = 'hg'
100 98 repo_user_agent = 'mercurial'
101 99 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
102 100
103 101 def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
104 super(MercurialServer, self).__init__(user, user_permissions, config, env)
102 super().__init__(user, user_permissions, config, env)
105 103
106 104 self.store = store
107 105 self.ini_path = ini_path
108 106 self.repo_name = repo_name
109 107 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
110 108 self.tunnel = MercurialTunnelWrapper(server=self)
111 109
112 110 def config_to_hgrc(self, repo_name):
113 111 ui_sections = collections.defaultdict(list)
114 112 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
115 113
116 114 # write default hooks
117 115 default_hooks = [
118 116 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
119 117 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
120 118 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
121 119
122 120 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
123 121 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
124 122 ]
125 123
126 124 for k, v in default_hooks:
127 125 ui_sections['hooks'].append((k, v))
128 126
129 127 for entry in ui:
130 128 if not entry.active:
131 129 continue
132 130 sec = entry.section
133 131 key = entry.key
134 132
135 133 if sec in self.cli_flags:
136 134 # we want only custom hooks, so we skip builtins
137 135 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
138 136 continue
139 137
140 138 ui_sections[sec].append([key, entry.value])
141 139
142 140 flags = []
143 141 for _sec, key_val in ui_sections.items():
144 142 flags.append(' ')
145 flags.append('[{}]'.format(_sec))
143 flags.append(f'[{_sec}]')
146 144 for key, val in key_val:
147 flags.append('{}= {}'.format(key, val))
145 flags.append(f'{key}= {val}')
148 146 return flags
@@ -1,258 +1,256 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 os
22 20 import re
23 21 import sys
24 22 import logging
25 23 import signal
26 24 import tempfile
27 25 from subprocess import Popen, PIPE
28 26 import urllib.parse
29 27
30 28 from .base import VcsServer
31 29
32 30 log = logging.getLogger(__name__)
33 31
34 32
35 33 class SubversionTunnelWrapper(object):
36 34 process = None
37 35
38 36 def __init__(self, server):
39 37 self.server = server
40 38 self.timeout = 30
41 39 self.stdin = sys.stdin
42 40 self.stdout = sys.stdout
43 41 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 42 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45 43
46 44 self.read_only = True # flag that we set to make the hooks readonly
47 45
48 46 def create_svn_config(self):
49 47 content = (
50 48 '[general]\n'
51 49 'hooks-env = {}\n').format(self.hooks_env_path)
52 50 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 51 config_file.write(content)
54 52
55 53 def create_hooks_env(self):
56 54 content = (
57 55 '[default]\n'
58 56 'LANG = en_US.UTF-8\n')
59 57 if self.read_only:
60 58 content += 'SSH_READ_ONLY = 1\n'
61 59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 60 hooks_env_file.write(content)
63 61
64 62 def remove_configs(self):
65 63 os.remove(self.svn_conf_path)
66 64 os.remove(self.hooks_env_path)
67 65
68 66 def command(self):
69 67 root = self.server.get_root_store()
70 68 username = self.server.user.username
71 69
72 70 command = [
73 71 self.server.svn_path, '-t',
74 72 '--config-file', self.svn_conf_path,
75 73 '--tunnel-user', username,
76 74 '-r', root]
77 75 log.debug("Final CMD: %s", ' '.join(command))
78 76 return command
79 77
80 78 def start(self):
81 79 command = self.command()
82 80 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
83 81
84 82 def sync(self):
85 83 while self.process.poll() is None:
86 84 next_byte = self.stdin.read(1)
87 85 if not next_byte:
88 86 break
89 87 self.process.stdin.write(next_byte)
90 88 self.remove_configs()
91 89
92 90 @property
93 91 def return_code(self):
94 92 return self.process.returncode
95 93
96 94 def get_first_client_response(self):
97 95 signal.signal(signal.SIGALRM, self.interrupt)
98 96 signal.alarm(self.timeout)
99 97 first_response = self._read_first_client_response()
100 98 signal.alarm(0)
101 99 return (self._parse_first_client_response(first_response)
102 100 if first_response else None)
103 101
104 102 def patch_first_client_response(self, response, **kwargs):
105 103 self.create_hooks_env()
106 104 data = response.copy()
107 105 data.update(kwargs)
108 106 data['url'] = self._svn_string(data['url'])
109 107 data['ra_client'] = self._svn_string(data['ra_client'])
110 108 data['client'] = data['client'] or ''
111 109 buffer_ = (
112 110 "( {version} ( {capabilities} ) {url}{ra_client}"
113 111 "( {client}) ) ".format(**data))
114 112 self.process.stdin.write(buffer_)
115 113
116 114 def fail(self, message):
117 115 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
118 116 message=self._svn_string(message)))
119 117 self.remove_configs()
120 118 self.process.kill()
121 119 return 1
122 120
123 121 def interrupt(self, signum, frame):
124 122 self.fail("Exited by timeout")
125 123
126 124 def _svn_string(self, str_):
127 125 if not str_:
128 126 return ''
129 return '{length}:{string} '.format(length=len(str_), string=str_)
127 return f'{len(str_)}:{str_} '
130 128
131 129 def _read_first_client_response(self):
132 130 buffer_ = ""
133 131 brackets_stack = []
134 132 while True:
135 133 next_byte = self.stdin.read(1)
136 134 buffer_ += next_byte
137 135 if next_byte == "(":
138 136 brackets_stack.append(next_byte)
139 137 elif next_byte == ")":
140 138 brackets_stack.pop()
141 139 elif next_byte == " " and not brackets_stack:
142 140 break
143 141
144 142 return buffer_
145 143
146 144 def _parse_first_client_response(self, buffer_):
147 145 """
148 146 According to the Subversion RA protocol, the first request
149 147 should look like:
150 148
151 149 ( version:number ( cap:word ... ) url:string ? ra-client:string
152 150 ( ? client:string ) )
153 151
154 152 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
155 153 """
156 154 version_re = r'(?P<version>\d+)'
157 155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
158 156 url_re = r'\d+\:(?P<url>[\W\w]+)'
159 157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
160 158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
161 159 regex = re.compile(
162 160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
163 161 r'\(\s{client}\)\s\)\s*$'.format(
164 162 version=version_re, capabilities=capabilities_re,
165 163 url=url_re, ra_client=ra_client_re, client=client_re))
166 164 matcher = regex.match(buffer_)
167 165
168 166 return matcher.groupdict() if matcher else None
169 167
170 168 def _match_repo_name(self, url):
171 169 """
172 170 Given an server url, try to match it against ALL known repository names.
173 171 This handles a tricky SVN case for SSH and subdir commits.
174 172 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
175 173 result in the url with this subdir added.
176 174 """
177 175 # case 1 direct match, we don't do any "heavy" lookups
178 176 if url in self.server.user_permissions:
179 177 return url
180 178
181 179 log.debug('Extracting repository name from subdir path %s', url)
182 180 # case 2 we check all permissions, and match closes possible case...
183 181 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
184 182 # to assume that it will have the repo name as prefix, we ensure the prefix
185 183 # for similar repositories isn't matched by adding a /
186 184 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
187 185 for repo_name in self.server.user_permissions:
188 186 repo_name_prefix = repo_name + '/'
189 187 if url.startswith(repo_name_prefix):
190 188 log.debug('Found prefix %s match, returning proper repository name',
191 189 repo_name_prefix)
192 190 return repo_name
193 191
194 192 return
195 193
196 194 def run(self, extras):
197 195 action = 'pull'
198 196 self.create_svn_config()
199 197 self.start()
200 198
201 199 first_response = self.get_first_client_response()
202 200 if not first_response:
203 201 return self.fail("Repository name cannot be extracted")
204 202
205 203 url_parts = urllib.parse.urlparse(first_response['url'])
206 204
207 205 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
208 206
209 207 exit_code = self.server._check_permissions(action)
210 208 if exit_code:
211 209 return exit_code
212 210
213 211 # set the readonly flag to False if we have proper permissions
214 212 if self.server.has_write_perm():
215 213 self.read_only = False
216 214 self.server.update_environment(action=action, extras=extras)
217 215
218 216 self.patch_first_client_response(first_response)
219 217 self.sync()
220 218 return self.return_code
221 219
222 220
223 221 class SubversionServer(VcsServer):
224 222 backend = 'svn'
225 223 repo_user_agent = 'svn'
226 224
227 225 def __init__(self, store, ini_path, repo_name,
228 226 user, user_permissions, config, env):
229 super(SubversionServer, self)\
227 super()\
230 228 .__init__(user, user_permissions, config, env)
231 229 self.store = store
232 230 self.ini_path = ini_path
233 231 # NOTE(dan): repo_name at this point is empty,
234 232 # this is set later in .run() based from parsed input stream
235 233 self.repo_name = repo_name
236 234 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
237 235
238 236 self.tunnel = SubversionTunnelWrapper(server=self)
239 237
240 238 def _handle_tunnel(self, extras):
241 239
242 240 # pre-auth
243 241 action = 'pull'
244 242 # Special case for SVN, we extract repo name at later stage
245 243 # exit_code = self._check_permissions(action)
246 244 # if exit_code:
247 245 # return exit_code, False
248 246
249 247 req = self.env['request']
250 248 server_url = req.host_url + req.script_name
251 249 extras['server_url'] = server_url
252 250
253 251 log.debug('Using %s binaries from path %s', self.backend, self._path)
254 252 exit_code = self.tunnel.run(extras)
255 253
256 254 return exit_code, action == "push"
257 255
258 256
@@ -1,81 +1,79 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 os
22 20 import sys
23 21 import logging
24 22
25 23 import click
26 24
27 25 from pyramid.paster import setup_logging
28 26
29 27 from rhodecode.lib.pyramid_utils import bootstrap
30 28 from .backends import SshWrapper
31 29
32 30 log = logging.getLogger(__name__)
33 31
34 32
35 33 def setup_custom_logging(ini_path, debug):
36 34 if debug:
37 35 # enabled rhodecode.ini controlled logging setup
38 36 setup_logging(ini_path)
39 37 else:
40 38 # configure logging in a mode that doesn't print anything.
41 39 # in case of regularly configured logging it gets printed out back
42 40 # to the client doing an SSH command.
43 41 logger = logging.getLogger('')
44 42 null = logging.NullHandler()
45 43 # add the handler to the root logger
46 44 logger.handlers = [null]
47 45
48 46
49 47 @click.command()
50 48 @click.argument('ini_path', type=click.Path(exists=True))
51 49 @click.option(
52 50 '--mode', '-m', required=False, default='auto',
53 51 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
54 52 help='mode of operation')
55 53 @click.option('--user', help='Username for which the command will be executed')
56 54 @click.option('--user-id', help='User ID for which the command will be executed')
57 55 @click.option('--key-id', help='ID of the key from the database')
58 56 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
59 57 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
60 58 def main(ini_path, mode, user, user_id, key_id, shell, debug):
61 59 setup_custom_logging(ini_path, debug)
62 60
63 61 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
64 62 if not command and mode not in ['test']:
65 63 raise ValueError(
66 64 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
67 65 'Please make sure this is set and available during execution '
68 66 'of this script.')
69 67 connection_info = os.environ.get('SSH_CONNECTION', '')
70 68
71 69 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
72 70 try:
73 71 ssh_wrapper = SshWrapper(
74 72 command, connection_info, mode,
75 73 user, user_id, key_id, shell, ini_path, env)
76 74 except Exception:
77 75 log.exception('Failed to execute SshWrapper')
78 76 sys.exit(-5)
79 77
80 78 return_code = ssh_wrapper.wrap()
81 79 sys.exit(return_code)
@@ -1,36 +1,34 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 .utils import generate_ssh_authorized_keys_file
25 23
26 24
27 25 log = logging.getLogger(__name__)
28 26
29 27
30 28 def generate_ssh_authorized_keys_file_subscriber(event):
31 29 """
32 30 Subscriber to the `SshKeyFileChangeEvent`. This triggers the
33 31 automatic generation of authorized_keys file on any change in
34 32 ssh keys management
35 33 """
36 34 generate_ssh_authorized_keys_file(event.request.registry)
@@ -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,70 +1,68 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 os
22 20 import pytest
23 21 import configparser
24 22
25 23 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26 24 from rhodecode.lib.utils2 import AttributeDict
27 25
28 26
29 27 @pytest.fixture()
30 28 def dummy_conf_file(tmpdir):
31 29 conf = configparser.ConfigParser()
32 30 conf.add_section('app:main')
33 31 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
34 32 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
35 33 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
36 34
37 35 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
38 36 with open(f_path, 'wt') as f:
39 37 conf.write(f)
40 38
41 39 return os.path.join(f_path)
42 40
43 41
44 42 def plain_dummy_env():
45 43 return {
46 44 'request':
47 45 AttributeDict(host_url='http://localhost', script_name='/')
48 46 }
49 47
50 48
51 49 @pytest.fixture()
52 50 def dummy_env():
53 51 return plain_dummy_env()
54 52
55 53
56 54 def plain_dummy_user():
57 55 return AttributeDict(username='test_user')
58 56
59 57
60 58 @pytest.fixture()
61 59 def dummy_user():
62 60 return plain_dummy_user()
63 61
64 62
65 63 @pytest.fixture()
66 64 def ssh_wrapper(app, dummy_conf_file, dummy_env):
67 65 conn_info = '127.0.0.1 22 10.0.0.1 443'
68 66 return SshWrapper(
69 67 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
70 68 shell=False, ini_path=dummy_conf_file, env=dummy_env)
@@ -1,155 +1,153 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 os
22 20
23 21 import mock
24 22 import pytest
25 23
26 24 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
27 25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
28 26 from rhodecode.lib.ext_json import json
29 27
30 28 class GitServerCreator(object):
31 29 root = '/tmp/repo/path/'
32 30 git_path = '/usr/local/bin/git'
33 31 config_data = {
34 32 'app:main': {
35 33 'ssh.executable.git': git_path,
36 34 'vcs.hooks.protocol': 'http',
37 35 }
38 36 }
39 37 repo_name = 'test_git'
40 38 repo_mode = 'receive-pack'
41 39 user = plain_dummy_user()
42 40
43 41 def __init__(self):
44 42 def config_get(part, key):
45 43 return self.config_data.get(part, {}).get(key)
46 44 self.config_mock = mock.Mock()
47 45 self.config_mock.get = mock.Mock(side_effect=config_get)
48 46
49 47 def create(self, **kwargs):
50 48 parameters = {
51 49 'store': self.root,
52 50 'ini_path': '',
53 51 'user': self.user,
54 52 'repo_name': self.repo_name,
55 53 'repo_mode': self.repo_mode,
56 54 'user_permissions': {
57 55 self.repo_name: 'repository.admin'
58 56 },
59 57 'config': self.config_mock,
60 58 'env': plain_dummy_env()
61 59 }
62 60 parameters.update(kwargs)
63 61 server = GitServer(**parameters)
64 62 return server
65 63
66 64
67 65 @pytest.fixture()
68 66 def git_server(app):
69 67 return GitServerCreator()
70 68
71 69
72 70 class TestGitServer(object):
73 71
74 72 def test_command(self, git_server):
75 73 server = git_server.create()
76 74 expected_command = (
77 75 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
78 76 root=git_server.root, git_path=git_server.git_path,
79 77 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
80 78 )
81 79 assert expected_command == server.tunnel.command()
82 80
83 81 @pytest.mark.parametrize('permissions, action, code', [
84 82 ({}, 'pull', -2),
85 83 ({'test_git': 'repository.read'}, 'pull', 0),
86 84 ({'test_git': 'repository.read'}, 'push', -2),
87 85 ({'test_git': 'repository.write'}, 'push', 0),
88 86 ({'test_git': 'repository.admin'}, 'push', 0),
89 87
90 88 ])
91 89 def test_permission_checks(self, git_server, permissions, action, code):
92 90 server = git_server.create(user_permissions=permissions)
93 91 result = server._check_permissions(action)
94 92 assert result is code
95 93
96 94 @pytest.mark.parametrize('permissions, value', [
97 95 ({}, False),
98 96 ({'test_git': 'repository.read'}, False),
99 97 ({'test_git': 'repository.write'}, True),
100 98 ({'test_git': 'repository.admin'}, True),
101 99
102 100 ])
103 101 def test_has_write_permissions(self, git_server, permissions, value):
104 102 server = git_server.create(user_permissions=permissions)
105 103 result = server.has_write_perm()
106 104 assert result is value
107 105
108 106 def test_run_returns_executes_command(self, git_server):
109 107 server = git_server.create()
110 108 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
111 109
112 110 os.environ['SSH_CLIENT'] = '127.0.0.1'
113 111 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
114 112 _patch.return_value = 0
115 113 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
116 114 exit_code = server.run()
117 115
118 116 assert exit_code == (0, False)
119 117
120 118 @pytest.mark.parametrize(
121 119 'repo_mode, action', [
122 120 ['receive-pack', 'push'],
123 121 ['upload-pack', 'pull']
124 122 ])
125 123 def test_update_environment(self, git_server, repo_mode, action):
126 124 server = git_server.create(repo_mode=repo_mode)
127 125 store = server.store
128 126
129 127 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
130 128 with mock.patch('os.putenv') as putenv_mock:
131 129 server.update_environment(action)
132 130
133 131 expected_data = {
134 132 'username': git_server.user.username,
135 133 'user_id': git_server.user.user_id,
136 134 'scm': 'git',
137 135 'repository': git_server.repo_name,
138 136 'make_lock': None,
139 137 'action': action,
140 138 'ip': '10.10.10.10',
141 139 'locked_by': [None, None],
142 140 'config': '',
143 141 'repo_store': store,
144 142 'server_url': None,
145 143 'hooks': ['push', 'pull'],
146 144 'is_shadow_repo': False,
147 145 'hooks_module': 'rhodecode.lib.hooks_daemon',
148 146 'check_branch_perms': False,
149 147 'detect_force_push': False,
150 148 'user_agent': u'git/ssh-user-agent',
151 149 'SSH': True,
152 150 'SSH_PERMISSIONS': 'repository.admin',
153 151 }
154 152 args, kwargs = putenv_mock.call_args
155 153 assert json.loads(args[1]) == expected_data
@@ -1,120 +1,118 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 os
22 20 import mock
23 21 import pytest
24 22
25 23 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
26 24 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
27 25
28 26
29 27 class MercurialServerCreator(object):
30 28 root = '/tmp/repo/path/'
31 29 hg_path = '/usr/local/bin/hg'
32 30
33 31 config_data = {
34 32 'app:main': {
35 33 'ssh.executable.hg': hg_path,
36 34 'vcs.hooks.protocol': 'http',
37 35 }
38 36 }
39 37 repo_name = 'test_hg'
40 38 user = plain_dummy_user()
41 39
42 40 def __init__(self):
43 41 def config_get(part, key):
44 42 return self.config_data.get(part, {}).get(key)
45 43 self.config_mock = mock.Mock()
46 44 self.config_mock.get = mock.Mock(side_effect=config_get)
47 45
48 46 def create(self, **kwargs):
49 47 parameters = {
50 48 'store': self.root,
51 49 'ini_path': '',
52 50 'user': self.user,
53 51 'repo_name': self.repo_name,
54 52 'user_permissions': {
55 53 'test_hg': 'repository.admin'
56 54 },
57 55 'config': self.config_mock,
58 56 'env': plain_dummy_env()
59 57 }
60 58 parameters.update(kwargs)
61 59 server = MercurialServer(**parameters)
62 60 return server
63 61
64 62
65 63 @pytest.fixture()
66 64 def hg_server(app):
67 65 return MercurialServerCreator()
68 66
69 67
70 68 class TestMercurialServer(object):
71 69
72 70 def test_command(self, hg_server, tmpdir):
73 71 server = hg_server.create()
74 72 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
75 73 expected_command = (
76 74 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
77 75 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
78 76 repo_name=hg_server.repo_name)
79 77 )
80 78 server_command = server.tunnel.command(custom_hgrc)
81 79 assert expected_command == server_command
82 80
83 81 @pytest.mark.parametrize('permissions, action, code', [
84 82 ({}, 'pull', -2),
85 83 ({'test_hg': 'repository.read'}, 'pull', 0),
86 84 ({'test_hg': 'repository.read'}, 'push', -2),
87 85 ({'test_hg': 'repository.write'}, 'push', 0),
88 86 ({'test_hg': 'repository.admin'}, 'push', 0),
89 87
90 88 ])
91 89 def test_permission_checks(self, hg_server, permissions, action, code):
92 90 server = hg_server.create(user_permissions=permissions)
93 91 result = server._check_permissions(action)
94 92 assert result is code
95 93
96 94 @pytest.mark.parametrize('permissions, value', [
97 95 ({}, False),
98 96 ({'test_hg': 'repository.read'}, False),
99 97 ({'test_hg': 'repository.write'}, True),
100 98 ({'test_hg': 'repository.admin'}, True),
101 99
102 100 ])
103 101 def test_has_write_permissions(self, hg_server, permissions, value):
104 102 server = hg_server.create(user_permissions=permissions)
105 103 result = server.has_write_perm()
106 104 assert result is value
107 105
108 106 def test_run_returns_executes_command(self, hg_server):
109 107 server = hg_server.create()
110 108 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
111 109 os.environ['SSH_CLIENT'] = '127.0.0.1'
112 110 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
113 111 _patch.return_value = 0
114 112 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
115 113 exit_code = server.run()
116 114
117 115 assert exit_code == (0, False)
118 116
119 117
120 118
@@ -1,207 +1,205 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 os
21 19 import mock
22 20 import pytest
23 21
24 22 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 23 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26 24
27 25
28 26 class SubversionServerCreator(object):
29 27 root = '/tmp/repo/path/'
30 28 svn_path = '/usr/local/bin/svnserve'
31 29 config_data = {
32 30 'app:main': {
33 31 'ssh.executable.svn': svn_path,
34 32 'vcs.hooks.protocol': 'http',
35 33 }
36 34 }
37 35 repo_name = 'test-svn'
38 36 user = plain_dummy_user()
39 37
40 38 def __init__(self):
41 39 def config_get(part, key):
42 40 return self.config_data.get(part, {}).get(key)
43 41 self.config_mock = mock.Mock()
44 42 self.config_mock.get = mock.Mock(side_effect=config_get)
45 43
46 44 def create(self, **kwargs):
47 45 parameters = {
48 46 'store': self.root,
49 47 'repo_name': self.repo_name,
50 48 'ini_path': '',
51 49 'user': self.user,
52 50 'user_permissions': {
53 51 self.repo_name: 'repository.admin'
54 52 },
55 53 'config': self.config_mock,
56 54 'env': plain_dummy_env()
57 55 }
58 56
59 57 parameters.update(kwargs)
60 58 server = SubversionServer(**parameters)
61 59 return server
62 60
63 61
64 62 @pytest.fixture()
65 63 def svn_server(app):
66 64 return SubversionServerCreator()
67 65
68 66
69 67 class TestSubversionServer(object):
70 68 def test_command(self, svn_server):
71 69 server = svn_server.create()
72 70 expected_command = [
73 71 svn_server.svn_path, '-t',
74 72 '--config-file', server.tunnel.svn_conf_path,
75 73 '--tunnel-user', svn_server.user.username,
76 74 '-r', svn_server.root
77 75 ]
78 76
79 77 assert expected_command == server.tunnel.command()
80 78
81 79 @pytest.mark.parametrize('permissions, action, code', [
82 80 ({}, 'pull', -2),
83 81 ({'test-svn': 'repository.read'}, 'pull', 0),
84 82 ({'test-svn': 'repository.read'}, 'push', -2),
85 83 ({'test-svn': 'repository.write'}, 'push', 0),
86 84 ({'test-svn': 'repository.admin'}, 'push', 0),
87 85
88 86 ])
89 87 def test_permission_checks(self, svn_server, permissions, action, code):
90 88 server = svn_server.create(user_permissions=permissions)
91 89 result = server._check_permissions(action)
92 90 assert result is code
93 91
94 92 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
95 93 # not matched repository name
96 94 ({
97 95 'test-svn': ''
98 96 }, ['test-svn-1', 'test-svn-1/subpath'],
99 97 None),
100 98
101 99 # exact match
102 100 ({
103 101 'test-svn': ''
104 102 },
105 103 ['test-svn'],
106 104 'test-svn'),
107 105
108 106 # subdir commits
109 107 ({
110 108 'test-svn': ''
111 109 },
112 110 ['test-svn/foo',
113 111 'test-svn/foo/test-svn',
114 112 'test-svn/trunk/development.txt',
115 113 ],
116 114 'test-svn'),
117 115
118 116 # subgroups + similar patterns
119 117 ({
120 118 'test-svn': '',
121 119 'test-svn-1': '',
122 120 'test-svn-subgroup/test-svn': '',
123 121
124 122 },
125 123 ['test-svn-1',
126 124 'test-svn-1/foo/test-svn',
127 125 'test-svn-1/test-svn',
128 126 ],
129 127 'test-svn-1'),
130 128
131 129 # subgroups + similar patterns
132 130 ({
133 131 'test-svn-1': '',
134 132 'test-svn-10': '',
135 133 'test-svn-100': '',
136 134 },
137 135 ['test-svn-10',
138 136 'test-svn-10/foo/test-svn',
139 137 'test-svn-10/test-svn',
140 138 ],
141 139 'test-svn-10'),
142 140
143 141 # subgroups + similar patterns
144 142 ({
145 143 'name': '',
146 144 'nameContains': '',
147 145 'nameContainsThis': '',
148 146 },
149 147 ['nameContains',
150 148 'nameContains/This',
151 149 'nameContains/This/test-svn',
152 150 ],
153 151 'nameContains'),
154 152
155 153 # subgroups + similar patterns
156 154 ({
157 155 'test-svn': '',
158 156 'test-svn-1': '',
159 157 'test-svn-subgroup/test-svn': '',
160 158
161 159 },
162 160 ['test-svn-subgroup/test-svn',
163 161 'test-svn-subgroup/test-svn/foo/test-svn',
164 162 'test-svn-subgroup/test-svn/trunk/example.txt',
165 163 ],
166 164 'test-svn-subgroup/test-svn'),
167 165 ])
168 166 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
169 167 server = svn_server.create(user_permissions=permissions)
170 168 for path in access_paths:
171 169 repo_name = server.tunnel._match_repo_name(path)
172 170 assert repo_name == expected_match
173 171
174 172 def test_run_returns_executes_command(self, svn_server):
175 173 server = svn_server.create()
176 174 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
177 175 os.environ['SSH_CLIENT'] = '127.0.0.1'
178 176 with mock.patch.object(
179 177 SubversionTunnelWrapper, 'get_first_client_response',
180 178 return_value={'url': 'http://server/test-svn'}):
181 179 with mock.patch.object(
182 180 SubversionTunnelWrapper, 'patch_first_client_response',
183 181 return_value=0):
184 182 with mock.patch.object(
185 183 SubversionTunnelWrapper, 'sync',
186 184 return_value=0):
187 185 with mock.patch.object(
188 186 SubversionTunnelWrapper, 'command',
189 187 return_value=['date']):
190 188
191 189 exit_code = server.run()
192 190 # SVN has this differently configured, and we get in our mock env
193 191 # None as return code
194 192 assert exit_code == (None, False)
195 193
196 194 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
197 195 server = svn_server.create()
198 196 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
199 197 with mock.patch.object(
200 198 SubversionTunnelWrapper, 'command',
201 199 return_value=['date']):
202 200 with mock.patch.object(
203 201 SubversionTunnelWrapper, 'get_first_client_response',
204 202 return_value=None):
205 203 exit_code = server.run()
206 204
207 205 assert exit_code == (1, False)
@@ -1,71 +1,69 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 os
22 20 import pytest
23 21 import mock
24 22
25 23 from rhodecode.apps.ssh_support import utils
26 24 from rhodecode.lib.utils2 import AttributeDict
27 25
28 26
29 27 class TestSshKeyFileGeneration(object):
30 28 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
31 29 @pytest.mark.parametrize('allow_shell', [True, False])
32 30 @pytest.mark.parametrize('debug', [True, False])
33 31 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
34 32 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, debug, ssh_opts):
35 33
36 34 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
37 35
38 36 def keys():
39 37 return [
40 38 AttributeDict({'user': AttributeDict(username='admin'),
41 39 'ssh_key_data': 'ssh-rsa ADMIN_KEY'}),
42 40 AttributeDict({'user': AttributeDict(username='user'),
43 41 'ssh_key_data': 'ssh-rsa USER_KEY'}),
44 42 ]
45 43 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
46 44 return_value=keys()):
47 45 with mock.patch.dict('rhodecode.CONFIG', {'__file__': '/tmp/file.ini'}):
48 46 utils._generate_ssh_authorized_keys_file(
49 47 authorized_keys_file_path, ssh_wrapper_cmd,
50 48 allow_shell, ssh_opts, debug
51 49 )
52 50
53 51 assert os.path.isfile(authorized_keys_file_path)
54 52 with open(authorized_keys_file_path) as f:
55 53 content = f.read()
56 54
57 55 assert 'command="/tmp/sshwrapper.py' in content
58 56 assert 'This file is managed by RhodeCode, ' \
59 57 'please do not edit it manually.' in content
60 58
61 59 if allow_shell:
62 60 assert '--shell' in content
63 61
64 62 if debug:
65 63 assert '--debug' in content
66 64
67 65 assert '--user' in content
68 66 assert '--user-id' in content
69 67
70 68 if ssh_opts:
71 69 assert ssh_opts in content
@@ -1,54 +1,52 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 pytest
22 20
23 21
24 22 class TestSSHWrapper(object):
25 23
26 24 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
27 25 with pytest.raises(Exception) as exc_info:
28 26 ssh_wrapper.serve(
29 27 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
30 28 permissions={}, branch_permissions={})
31 29 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
32 30
33 31 def test_parse_config(self, ssh_wrapper):
34 32 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
35 33 assert config
36 34
37 35 def test_get_connection_info(self, ssh_wrapper):
38 36 conn_info = ssh_wrapper.get_connection_info()
39 37 assert {'client_ip': '127.0.0.1',
40 38 'client_port': '22',
41 39 'server_ip': '10.0.0.1',
42 40 'server_port': '443'} == conn_info
43 41
44 42 @pytest.mark.parametrize('command, vcs', [
45 43 ('xxx', None),
46 44 ('svnserve -t', 'svn'),
47 45 ('hg -R repo serve --stdio', 'hg'),
48 46 ('git-receive-pack \'repo.git\'', 'git'),
49 47
50 48 ])
51 49 def test_get_repo_details(self, ssh_wrapper, command, vcs):
52 50 ssh_wrapper.command = command
53 51 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
54 52 assert vcs_type == vcs
@@ -1,138 +1,136 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 os
22 20 import stat
23 21 import logging
24 22 import tempfile
25 23 import datetime
26 24
27 25 from . import config_keys
28 26 from rhodecode.model.db import true, joinedload, User, UserSshKeys
29 27
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31 HEADER = \
34 32 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
35 33 "# Current entries: {}, create date: UTC:{}.\n"
36 34
37 35 # Default SSH options for authorized_keys file, can be override via .ini
38 36 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
39 37
40 38
41 39 def get_all_active_keys():
42 40 result = UserSshKeys.query() \
43 41 .join(User) \
44 42 .filter(User != User.get_default_user()) \
45 43 .filter(User.active == true()) \
46 44 .all()
47 45 return result
48 46
49 47
50 48 def _generate_ssh_authorized_keys_file(
51 49 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52 50 import rhodecode
53 51
54 52 authorized_keys_file_path = os.path.abspath(
55 53 os.path.expanduser(authorized_keys_file_path))
56 54 tmp_file_dir = os.path.dirname(authorized_keys_file_path)
57 55
58 56 if not os.path.exists(tmp_file_dir):
59 57 log.debug('SSH authorized_keys file dir does not exist, creating one now...')
60 58 os.makedirs(tmp_file_dir)
61 59
62 60 all_active_keys = get_all_active_keys()
63 61
64 62 if allow_shell:
65 63 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
66 64 if debug:
67 65 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
68 66
69 67 if not os.path.isfile(authorized_keys_file_path):
70 68 log.debug('Creating file at %s', authorized_keys_file_path)
71 69 with open(authorized_keys_file_path, 'w'):
72 70 # create a file with write access
73 71 pass
74 72
75 73 if not os.access(authorized_keys_file_path, os.R_OK):
76 74 raise OSError('Access to file {} is without read access'.format(
77 75 authorized_keys_file_path))
78 76
79 77 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
80 78
81 79 fd, tmp_authorized_keys = tempfile.mkstemp(
82 80 '.authorized_keys_write_operation',
83 81 dir=tmp_file_dir)
84 82
85 83 now = datetime.datetime.utcnow().isoformat()
86 84 keys_file = os.fdopen(fd, 'wt')
87 85 keys_file.write(HEADER.format(len(all_active_keys), now))
88 86 ini_path = rhodecode.CONFIG['__file__']
89 87
90 88 for user_key in all_active_keys:
91 89 username = user_key.user.username
92 90 user_id = user_key.user.user_id
93 91 # replace all newline from ends and inside
94 92 safe_key_data = user_key.ssh_key_data\
95 93 .strip()\
96 94 .replace('\n', ' ') \
97 95 .replace('\t', ' ') \
98 96 .replace('\r', ' ')
99 97
100 98 line = line_tmpl.format(
101 99 ssh_opts=ssh_opts or SSH_OPTS,
102 100 wrapper_command=ssh_wrapper_cmd,
103 101 ini_path=ini_path,
104 102 user_id=user_id,
105 103 user=username,
106 104 user_key_id=user_key.ssh_key_id,
107 105 key=safe_key_data)
108 106
109 107 keys_file.write(line)
110 108 log.debug('addkey: Key added for user: `%s`', username)
111 109 keys_file.close()
112 110
113 111 # Explicitly setting read-only permissions to authorized_keys
114 112 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
115 113 # Rename is atomic operation
116 114 os.rename(tmp_authorized_keys, authorized_keys_file_path)
117 115
118 116
119 117 def generate_ssh_authorized_keys_file(registry):
120 118 log.info('Generating new authorized key file')
121 119
122 120 authorized_keys_file_path = registry.settings.get(
123 121 config_keys.authorized_keys_file_path)
124 122
125 123 ssh_wrapper_cmd = registry.settings.get(
126 124 config_keys.wrapper_cmd)
127 125 allow_shell = registry.settings.get(
128 126 config_keys.wrapper_allow_shell)
129 127 ssh_opts = registry.settings.get(
130 128 config_keys.authorized_keys_line_ssh_opts)
131 129 debug = registry.settings.get(
132 130 config_keys.enable_debug_logging)
133 131
134 132 _generate_ssh_authorized_keys_file(
135 133 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
136 134 debug)
137 135
138 136 return 0
@@ -1,90 +1,88 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 os
21 19 import logging
22 20
23 21 # Do not use `from rhodecode import events` here, it will be overridden by the
24 22 # events module in this package due to pythons import mechanism.
25 23 from rhodecode.events import RepoGroupEvent
26 24 from rhodecode.subscribers import AsyncSubprocessSubscriber
27 25 from rhodecode.config.settings_maker import SettingsMaker
28 26
29 27 from .events import ModDavSvnConfigChange
30 28 from .subscribers import generate_config_subscriber
31 29 from . import config_keys
32 30
33 31
34 32 log = logging.getLogger(__name__)
35 33
36 34
37 35 def _sanitize_settings_and_apply_defaults(settings):
38 36 """
39 37 Set defaults, convert to python types and validate settings.
40 38 """
41 39 settings_maker = SettingsMaker(settings)
42 40 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
43 41 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
44 42 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
45 43 settings_maker.make_setting(config_keys.config_file_path, '')
46 44 settings_maker.make_setting(config_keys.location_root, '/')
47 45 settings_maker.make_setting(config_keys.reload_command, '')
48 46 settings_maker.make_setting(config_keys.template, '')
49 47
50 48 settings_maker.env_expand()
51 49
52 50 # Convert negative timeout values to zero.
53 51 if settings[config_keys.reload_timeout] < 0:
54 52 settings[config_keys.reload_timeout] = 0
55 53
56 54 # Append path separator to location root.
57 55 settings[config_keys.location_root] = _append_path_sep(
58 56 settings[config_keys.location_root])
59 57
60 58 # Validate settings.
61 59 if settings[config_keys.generate_config]:
62 60 assert len(settings[config_keys.config_file_path]) > 0
63 61
64 62
65 63 def _append_path_sep(path):
66 64 """
67 65 Append the path separator if missing.
68 66 """
69 67 if isinstance(path, str) and not path.endswith(os.path.sep):
70 68 path += os.path.sep
71 69 return path
72 70
73 71
74 72 def includeme(config):
75 73 settings = config.registry.settings
76 74 _sanitize_settings_and_apply_defaults(settings)
77 75
78 76 if settings[config_keys.generate_config]:
79 77 # Add subscriber to generate the Apache mod dav svn configuration on
80 78 # repository group events.
81 79 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
82 80
83 81 # If a reload command is set add a subscriber to execute it on
84 82 # configuration changes.
85 83 reload_cmd = settings[config_keys.reload_command]
86 84 if reload_cmd:
87 85 reload_timeout = settings[config_keys.reload_timeout] or None
88 86 reload_subscriber = AsyncSubprocessSubscriber(
89 87 cmd=reload_cmd, timeout=reload_timeout)
90 88 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
@@ -1,30 +1,28 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
22 20 # Definition of setting keys used to configure this module. Defined here to
23 21 # avoid repetition of keys throughout the module.
24 22 config_file_path = 'svn.proxy.config_file_path'
25 23 generate_config = 'svn.proxy.generate_config'
26 24 list_parent_path = 'svn.proxy.list_parent_path'
27 25 location_root = 'svn.proxy.location_root'
28 26 reload_command = 'svn.proxy.reload_cmd'
29 27 reload_timeout = 'svn.proxy.reload_timeout'
30 28 template = 'svn.proxy.config_template'
@@ -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
24 22 from .utils import generate_mod_dav_svn_config
25 23
26 24
27 25 log = logging.getLogger(__name__)
28 26
29 27
30 28 def generate_config_subscriber(event):
31 29 """
32 30 Subscriber to the `rhodcode.events.RepoGroupEvent`. This triggers the
33 31 automatic generation of mod_dav_svn config file on repository group
34 32 changes.
35 33 """
36 34 try:
37 35 generate_mod_dav_svn_config(event.request.registry)
38 36 except Exception:
39 37 log.exception(
40 38 'Exception while generating subversion mod_dav_svn configuration.')
@@ -1,121 +1,119 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 os
23 21 import mock
24 22 import pytest
25 23
26 24 from rhodecode.apps.svn_support import utils
27 25
28 26
29 27 @pytest.mark.usefixtures('config_stub')
30 28 class TestModDavSvnConfig(object):
31 29
32 30 @classmethod
33 31 def setup_class(cls):
34 cls.location_root = u'/location/root/çµäö'
35 cls.parent_path_root = u'/parent/path/çµäö'
36 cls.realm = u'Dummy Realm (äöüçµ)'
32 cls.location_root = '/location/root/çµäö'
33 cls.parent_path_root = '/parent/path/çµäö'
34 cls.realm = 'Dummy Realm (äöüçµ)'
37 35
38 36 @classmethod
39 37 def get_repo_group_mocks(cls, count=1):
40 38 repo_groups = []
41 39 for num in range(0, count):
42 40 full_path = f'/path/to/RepöGröúp-°µ {num}'
43 41 repo_group_mock = mock.MagicMock()
44 42 repo_group_mock.full_path = full_path
45 43 repo_group_mock.full_path_splitted = full_path.split('/')
46 44 repo_groups.append(repo_group_mock)
47 45 return repo_groups
48 46
49 47 def assert_root_location_directive(self, config):
50 pattern = u'<Location "{location}">'.format(
48 pattern = '<Location "{location}">'.format(
51 49 location=self.location_root)
52 50 assert len(re.findall(pattern, config)) == 1
53 51
54 52 def assert_group_location_directive(self, config, group_path):
55 pattern = u'<Location "{location}{group_path}">'.format(
53 pattern = '<Location "{location}{group_path}">'.format(
56 54 location=self.location_root, group_path=group_path)
57 55 assert len(re.findall(pattern, config)) == 1
58 56
59 57 def test_render_mod_dav_svn_config(self):
60 58 repo_groups = self.get_repo_group_mocks(count=10)
61 59 generated_config = utils._render_mod_dav_svn_config(
62 60 parent_path_root=self.parent_path_root,
63 61 list_parent_path=True,
64 62 location_root=self.location_root,
65 63 repo_groups=repo_groups,
66 64 realm=self.realm,
67 65 use_ssl=True,
68 66 template=''
69 67 )
70 68 # Assert that one location directive exists for each repository group.
71 69 for group in repo_groups:
72 70 self.assert_group_location_directive(
73 71 generated_config, group.full_path)
74 72
75 73 # Assert that the root location directive exists.
76 74 self.assert_root_location_directive(generated_config)
77 75
78 76 def test_render_mod_dav_svn_config_with_alternative_template(self, tmpdir):
79 77 repo_groups = self.get_repo_group_mocks(count=10)
80 78 test_file_path = os.path.join(str(tmpdir), 'example.mako')
81 with open(test_file_path, 'wt') as f:
79 with open(test_file_path, 'w') as f:
82 80 f.write('TEST_EXAMPLE\n')
83 81
84 82 generated_config = utils._render_mod_dav_svn_config(
85 83 parent_path_root=self.parent_path_root,
86 84 list_parent_path=True,
87 85 location_root=self.location_root,
88 86 repo_groups=repo_groups,
89 87 realm=self.realm,
90 88 use_ssl=True,
91 89 template=test_file_path
92 90 )
93 91 assert 'TEST_EXAMPLE' in generated_config
94 92
95 93 @pytest.mark.parametrize('list_parent_path', [True, False])
96 94 @pytest.mark.parametrize('use_ssl', [True, False])
97 95 def test_list_parent_path(self, list_parent_path, use_ssl):
98 96 generated_config = utils._render_mod_dav_svn_config(
99 97 parent_path_root=self.parent_path_root,
100 98 list_parent_path=list_parent_path,
101 99 location_root=self.location_root,
102 100 repo_groups=self.get_repo_group_mocks(count=10),
103 101 realm=self.realm,
104 102 use_ssl=use_ssl,
105 103 template=''
106 104 )
107 105
108 106 # Assert that correct configuration directive is present.
109 107 if list_parent_path:
110 108 assert not re.search(r'SVNListParentPath\s+Off', generated_config)
111 109 assert re.search(r'SVNListParentPath\s+On', generated_config)
112 110 else:
113 111 assert re.search(r'SVNListParentPath\s+Off', generated_config)
114 112 assert not re.search(r'SVNListParentPath\s+On', generated_config)
115 113
116 114 if use_ssl:
117 115 assert 'RequestHeader edit Destination ^https: http: early' \
118 116 in generated_config
119 117 else:
120 118 assert '#RequestHeader edit Destination ^https: http: early' \
121 119 in generated_config
@@ -1,99 +1,97 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 codecs
22 20 import logging
23 21 import os
24 22 from pyramid.renderers import render
25 23
26 24 from rhodecode.events import trigger
27 25 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path
28 26 from rhodecode.lib.utils2 import str2bool
29 27 from rhodecode.model.db import RepoGroup
30 28
31 29 from . import config_keys
32 30 from .events import ModDavSvnConfigChange
33 31
34 32
35 33 log = logging.getLogger(__name__)
36 34
37 35
38 36 def write_mod_dav_svn_config(settings):
39 37 use_ssl = str2bool(settings['force_https'])
40 38 file_path = settings[config_keys.config_file_path]
41 39 config = _render_mod_dav_svn_config(
42 40 use_ssl=use_ssl,
43 41 parent_path_root=get_rhodecode_base_path(),
44 42 list_parent_path=settings[config_keys.list_parent_path],
45 43 location_root=settings[config_keys.location_root],
46 44 repo_groups=RepoGroup.get_all_repo_groups(),
47 45 realm=get_rhodecode_realm(), template=settings[config_keys.template])
48 46 _write_mod_dav_svn_config(config, file_path)
49 47 return file_path
50 48
51 49
52 50 def generate_mod_dav_svn_config(registry):
53 51 """
54 52 Generate the configuration file for use with subversion's mod_dav_svn
55 53 module. The configuration has to contain a <Location> block for each
56 54 available repository group because the mod_dav_svn module does not support
57 55 repositories organized in sub folders.
58 56 """
59 57 settings = registry.settings
60 58 file_path = write_mod_dav_svn_config(settings)
61 59
62 60 # Trigger an event on mod dav svn configuration change.
63 61 trigger(ModDavSvnConfigChange(), registry)
64 62 return file_path
65 63
66 64
67 65 def _render_mod_dav_svn_config(
68 66 parent_path_root, list_parent_path, location_root, repo_groups, realm,
69 67 use_ssl, template):
70 68 """
71 69 Render mod_dav_svn configuration to string.
72 70 """
73 71 repo_group_paths = []
74 72 for repo_group in repo_groups:
75 73 group_path = repo_group.full_path_splitted
76 74 location = os.path.join(location_root, *group_path)
77 75 parent_path = os.path.join(parent_path_root, *group_path)
78 76 repo_group_paths.append((location, parent_path))
79 77
80 78 context = {
81 79 'location_root': location_root,
82 80 'parent_path_root': parent_path_root,
83 81 'repo_group_paths': repo_group_paths,
84 82 'svn_list_parent_path': list_parent_path,
85 83 'rhodecode_realm': realm,
86 84 'use_https': use_ssl,
87 85 }
88 86 template = template or \
89 87 'rhodecode:apps/svn_support/templates/mod-dav-svn.conf.mako'
90 88 # Render the configuration template to string.
91 89 return render(template, context)
92 90
93 91
94 92 def _write_mod_dav_svn_config(config, filepath):
95 93 """
96 94 Write mod_dav_svn config to file.
97 95 """
98 96 with codecs.open(filepath, 'w', encoding='utf-8') as f:
99 97 f.write(config)
@@ -1,161 +1,159 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
22 20 from rhodecode.apps._base.navigation import NavigationRegistry
23 21 from rhodecode.apps._base import ADMIN_PREFIX
24 22 from rhodecode.lib.utils2 import str2bool
25 23
26 24
27 25 def admin_routes(config):
28 26 """
29 27 User groups /_admin prefixed routes
30 28 """
31 29 from rhodecode.apps.user_group.views import UserGroupsView
32 30
33 31 config.add_route(
34 32 name='user_group_members_data',
35 33 pattern='/user_groups/{user_group_id:\d+}/members',
36 34 user_group_route=True)
37 35 config.add_view(
38 36 UserGroupsView,
39 37 attr='user_group_members',
40 38 route_name='user_group_members_data', request_method='GET',
41 39 renderer='json_ext', xhr=True)
42 40
43 41 # user groups perms
44 42 config.add_route(
45 43 name='edit_user_group_perms_summary',
46 44 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary',
47 45 user_group_route=True)
48 46 config.add_view(
49 47 UserGroupsView,
50 48 attr='user_group_perms_summary',
51 49 route_name='edit_user_group_perms_summary', request_method='GET',
52 50 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
53 51
54 52 config.add_route(
55 53 name='edit_user_group_perms_summary_json',
56 54 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary/json',
57 55 user_group_route=True)
58 56 config.add_view(
59 57 UserGroupsView,
60 58 attr='user_group_perms_summary_json',
61 59 route_name='edit_user_group_perms_summary_json', request_method='GET',
62 60 renderer='json_ext')
63 61
64 62 # user groups edit
65 63 config.add_route(
66 64 name='edit_user_group',
67 65 pattern='/user_groups/{user_group_id:\d+}/edit',
68 66 user_group_route=True)
69 67 config.add_view(
70 68 UserGroupsView,
71 69 attr='user_group_edit',
72 70 route_name='edit_user_group', request_method='GET',
73 71 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
74 72
75 73 # user groups update
76 74 config.add_route(
77 75 name='user_groups_update',
78 76 pattern='/user_groups/{user_group_id:\d+}/update',
79 77 user_group_route=True)
80 78 config.add_view(
81 79 UserGroupsView,
82 80 attr='user_group_update',
83 81 route_name='user_groups_update', request_method='POST',
84 82 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
85 83
86 84 config.add_route(
87 85 name='edit_user_group_global_perms',
88 86 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions',
89 87 user_group_route=True)
90 88 config.add_view(
91 89 UserGroupsView,
92 90 attr='user_group_global_perms_edit',
93 91 route_name='edit_user_group_global_perms', request_method='GET',
94 92 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
95 93
96 94 config.add_route(
97 95 name='edit_user_group_global_perms_update',
98 96 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions/update',
99 97 user_group_route=True)
100 98 config.add_view(
101 99 UserGroupsView,
102 100 attr='user_group_global_perms_update',
103 101 route_name='edit_user_group_global_perms_update', request_method='POST',
104 102 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
105 103
106 104 config.add_route(
107 105 name='edit_user_group_perms',
108 106 pattern='/user_groups/{user_group_id:\d+}/edit/permissions',
109 107 user_group_route=True)
110 108 config.add_view(
111 109 UserGroupsView,
112 110 attr='user_group_edit_perms',
113 111 route_name='edit_user_group_perms', request_method='GET',
114 112 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
115 113
116 114 config.add_route(
117 115 name='edit_user_group_perms_update',
118 116 pattern='/user_groups/{user_group_id:\d+}/edit/permissions/update',
119 117 user_group_route=True)
120 118 config.add_view(
121 119 UserGroupsView,
122 120 attr='user_group_update_perms',
123 121 route_name='edit_user_group_perms_update', request_method='POST',
124 122 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
125 123
126 124 config.add_route(
127 125 name='edit_user_group_advanced',
128 126 pattern='/user_groups/{user_group_id:\d+}/edit/advanced',
129 127 user_group_route=True)
130 128 config.add_view(
131 129 UserGroupsView,
132 130 attr='user_group_edit_advanced',
133 131 route_name='edit_user_group_advanced', request_method='GET',
134 132 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
135 133
136 134 config.add_route(
137 135 name='edit_user_group_advanced_sync',
138 136 pattern='/user_groups/{user_group_id:\d+}/edit/advanced/sync',
139 137 user_group_route=True)
140 138 config.add_view(
141 139 UserGroupsView,
142 140 attr='user_group_edit_advanced_set_synchronization',
143 141 route_name='edit_user_group_advanced_sync', request_method='POST',
144 142 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
145 143
146 144 # user groups delete
147 145 config.add_route(
148 146 name='user_groups_delete',
149 147 pattern='/user_groups/{user_group_id:\d+}/delete',
150 148 user_group_route=True)
151 149 config.add_view(
152 150 UserGroupsView,
153 151 attr='user_group_delete',
154 152 route_name='user_groups_delete', request_method='POST',
155 153 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
156 154
157 155
158 156 def includeme(config):
159 157 # main admin routes
160 158 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
161 159
@@ -1,81 +1,80 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 from rhodecode.apps._base import ADMIN_PREFIX
30 29
31 30 base_url = {
32 31 'edit_user_group_perms':
33 32 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
34 33 'edit_user_group_perms_update':
35 34 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
36 35 }[name].format(**kwargs)
37 36
38 37 if params:
39 38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 39 return base_url
41 40
42 41
43 42 @pytest.mark.usefixtures("app")
44 43 class TestUserGroupPermissionsView(object):
45 44
46 45 def test_edit_perms_view(self, user_util, autologin_user):
47 46 user_group = user_util.create_user_group()
48 47 self.app.get(
49 48 route_path('edit_user_group_perms',
50 49 user_group_id=user_group.users_group_id), status=200)
51 50
52 51 def test_update_permissions(self, csrf_token, user_util):
53 52 user_group = user_util.create_user_group()
54 53 user_group_id = user_group.users_group_id
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='usergroup.write',
63 62 grant=[(user_id, 'usergroup.write', username, 'user')])
64 63
65 64 response = self.app.post(
66 65 route_path('edit_user_group_perms_update',
67 66 user_group_id=user_group_id), form_data).follow()
68 67
69 68 assert 'User Group permissions updated' in response
70 69
71 70 # revoke given
72 71 form_data = permission_update_data_generator(
73 72 csrf_token,
74 73 default='usergroup.read',
75 74 revoke=[(user_id, 'user')])
76 75
77 76 response = self.app.post(
78 77 route_path('edit_user_group_perms_update',
79 78 user_group_id=user_group_id), form_data).follow()
80 79
81 80 assert 'User Group permissions updated' in response
@@ -1,514 +1,512 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 peppercorn
24 22 import formencode
25 23 import formencode.htmlfill
26 24 from pyramid.httpexceptions import HTTPFound
27 25
28 26 from pyramid.response import Response
29 27 from pyramid.renderers import render
30 28
31 29 from rhodecode import events
32 30 from rhodecode.lib.exceptions import (
33 31 RepoGroupAssignmentError, UserGroupAssignedException)
34 32 from rhodecode.model.forms import (
35 33 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
36 34 UserPermissionsForm)
37 35 from rhodecode.model.permission import PermissionModel
38 36
39 37 from rhodecode.apps._base import UserGroupAppView
40 38 from rhodecode.lib.auth import (
41 39 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
42 40 from rhodecode.lib import helpers as h, audit_logger
43 41 from rhodecode.lib.utils2 import str2bool, safe_int
44 42 from rhodecode.model.db import User, UserGroup
45 43 from rhodecode.model.meta import Session
46 44 from rhodecode.model.user_group import UserGroupModel
47 45
48 46 log = logging.getLogger(__name__)
49 47
50 48
51 49 class UserGroupsView(UserGroupAppView):
52 50
53 51 def load_default_context(self):
54 52 c = self._get_local_tmpl_context()
55 53
56 54 PermissionModel().set_global_permission_choices(
57 55 c, gettext_translator=self.request.translate)
58 56
59 57 return c
60 58
61 59 @LoginRequired()
62 60 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
63 61 def user_group_members(self):
64 62 """
65 63 Return members of given user group
66 64 """
67 65 self.load_default_context()
68 66 user_group = self.db_user_group
69 67 group_members_obj = sorted((x.user for x in user_group.members),
70 68 key=lambda u: u.username.lower())
71 69
72 70 group_members = [
73 71 {
74 72 'id': user.user_id,
75 73 'first_name': user.first_name,
76 74 'last_name': user.last_name,
77 75 'username': user.username,
78 76 'icon_link': h.gravatar_url(user.email, 30, request=self.request),
79 77 'value_display': h.person(user.email),
80 78 'value': user.username,
81 79 'value_type': 'user',
82 80 'active': user.active,
83 81 }
84 82 for user in group_members_obj
85 83 ]
86 84
87 85 return {
88 86 'members': group_members
89 87 }
90 88
91 89 @LoginRequired()
92 90 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
93 91 def user_group_perms_summary(self):
94 92 c = self.load_default_context()
95 93 c.user_group = self.db_user_group
96 94 c.active = 'perms_summary'
97 95 c.permissions = UserGroupModel().get_perms_summary(
98 96 c.user_group.users_group_id)
99 97 return self._get_template_context(c)
100 98
101 99 @LoginRequired()
102 100 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
103 101 def user_group_perms_summary_json(self):
104 102 self.load_default_context()
105 103 user_group = self.db_user_group
106 104 return UserGroupModel().get_perms_summary(user_group.users_group_id)
107 105
108 106 def _revoke_perms_on_yourself(self, form_result):
109 107 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
110 108 form_result['perm_updates'])
111 109 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
112 110 form_result['perm_additions'])
113 111 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
114 112 form_result['perm_deletions'])
115 113 admin_perm = 'usergroup.admin'
116 114 if _updates and _updates[0][1] != admin_perm or \
117 115 _additions and _additions[0][1] != admin_perm or \
118 116 _deletions and _deletions[0][1] != admin_perm:
119 117 return True
120 118 return False
121 119
122 120 @LoginRequired()
123 121 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
124 122 @CSRFRequired()
125 123 def user_group_update(self):
126 124 _ = self.request.translate
127 125
128 126 user_group = self.db_user_group
129 127 user_group_id = user_group.users_group_id
130 128
131 129 old_user_group_name = self.db_user_group_name
132 130 new_user_group_name = old_user_group_name
133 131
134 132 c = self.load_default_context()
135 133 c.user_group = user_group
136 134 c.group_members_obj = [x.user for x in c.user_group.members]
137 135 c.group_members_obj.sort(key=lambda u: u.username.lower())
138 136 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
139 137 c.active = 'settings'
140 138
141 139 users_group_form = UserGroupForm(
142 140 self.request.translate, edit=True,
143 141 old_data=c.user_group.get_dict(), allow_disabled=True)()
144 142
145 143 old_values = c.user_group.get_api_data()
146 144
147 145 try:
148 146 form_result = users_group_form.to_python(self.request.POST)
149 147 pstruct = peppercorn.parse(self.request.POST.items())
150 148 form_result['users_group_members'] = pstruct['user_group_members']
151 149
152 150 user_group, added_members, removed_members = \
153 151 UserGroupModel().update(c.user_group, form_result)
154 152 new_user_group_name = form_result['users_group_name']
155 153
156 154 for user_id in added_members:
157 155 user = User.get(user_id)
158 156 user_data = user.get_api_data()
159 157 audit_logger.store_web(
160 158 'user_group.edit.member.add',
161 159 action_data={'user': user_data, 'old_data': old_values},
162 160 user=self._rhodecode_user)
163 161
164 162 for user_id in removed_members:
165 163 user = User.get(user_id)
166 164 user_data = user.get_api_data()
167 165 audit_logger.store_web(
168 166 'user_group.edit.member.delete',
169 167 action_data={'user': user_data, 'old_data': old_values},
170 168 user=self._rhodecode_user)
171 169
172 170 audit_logger.store_web(
173 171 'user_group.edit', action_data={'old_data': old_values},
174 172 user=self._rhodecode_user)
175 173
176 174 h.flash(_('Updated user group %s') % new_user_group_name,
177 175 category='success')
178 176
179 177 affected_user_ids = []
180 178 for user_id in added_members + removed_members:
181 179 affected_user_ids.append(user_id)
182 180
183 181 name_changed = old_user_group_name != new_user_group_name
184 182 if name_changed:
185 183 owner = User.get_by_username(form_result['user'])
186 184 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
187 185 affected_user_ids.append(self._rhodecode_user.user_id)
188 186 affected_user_ids.append(owner_id)
189 187
190 188 PermissionModel().trigger_permission_flush(affected_user_ids)
191 189
192 190 Session().commit()
193 191 except formencode.Invalid as errors:
194 192 defaults = errors.value
195 193 e = errors.error_dict or {}
196 194
197 195 data = render(
198 196 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
199 197 self._get_template_context(c), self.request)
200 198 html = formencode.htmlfill.render(
201 199 data,
202 200 defaults=defaults,
203 201 errors=e,
204 202 prefix_error=False,
205 203 encoding="UTF-8",
206 204 force_defaults=False
207 205 )
208 206 return Response(html)
209 207
210 208 except Exception:
211 209 log.exception("Exception during update of user group")
212 210 h.flash(_('Error occurred during update of user group %s')
213 211 % new_user_group_name, category='error')
214 212
215 213 raise HTTPFound(
216 214 h.route_path('edit_user_group', user_group_id=user_group_id))
217 215
218 216 @LoginRequired()
219 217 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
220 218 @CSRFRequired()
221 219 def user_group_delete(self):
222 220 _ = self.request.translate
223 221 user_group = self.db_user_group
224 222
225 223 self.load_default_context()
226 224 force = str2bool(self.request.POST.get('force'))
227 225
228 226 old_values = user_group.get_api_data()
229 227 try:
230 228 UserGroupModel().delete(user_group, force=force)
231 229 audit_logger.store_web(
232 230 'user.delete', action_data={'old_data': old_values},
233 231 user=self._rhodecode_user)
234 232 Session().commit()
235 233 h.flash(_('Successfully deleted user group'), category='success')
236 234 except UserGroupAssignedException as e:
237 235 h.flash(str(e), category='error')
238 236 except Exception:
239 237 log.exception("Exception during deletion of user group")
240 238 h.flash(_('An error occurred during deletion of user group'),
241 239 category='error')
242 240 raise HTTPFound(h.route_path('user_groups'))
243 241
244 242 @LoginRequired()
245 243 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
246 244 def user_group_edit(self):
247 245 user_group = self.db_user_group
248 246
249 247 c = self.load_default_context()
250 248 c.user_group = user_group
251 249 c.group_members_obj = [x.user for x in c.user_group.members]
252 250 c.group_members_obj.sort(key=lambda u: u.username.lower())
253 251 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
254 252
255 253 c.active = 'settings'
256 254
257 255 defaults = user_group.get_dict()
258 256 # fill owner
259 257 if user_group.user:
260 258 defaults.update({'user': user_group.user.username})
261 259 else:
262 260 replacement_user = User.get_first_super_admin().username
263 261 defaults.update({'user': replacement_user})
264 262
265 263 data = render(
266 264 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
267 265 self._get_template_context(c), self.request)
268 266 html = formencode.htmlfill.render(
269 267 data,
270 268 defaults=defaults,
271 269 encoding="UTF-8",
272 270 force_defaults=False
273 271 )
274 272 return Response(html)
275 273
276 274 @LoginRequired()
277 275 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
278 276 def user_group_edit_perms(self):
279 277 user_group = self.db_user_group
280 278 c = self.load_default_context()
281 279 c.user_group = user_group
282 280 c.active = 'perms'
283 281
284 282 defaults = {}
285 283 # fill user group users
286 284 for p in c.user_group.user_user_group_to_perm:
287 285 defaults.update({'u_perm_%s' % p.user.user_id:
288 286 p.permission.permission_name})
289 287
290 288 for p in c.user_group.user_group_user_group_to_perm:
291 289 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
292 290 p.permission.permission_name})
293 291
294 292 data = render(
295 293 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
296 294 self._get_template_context(c), self.request)
297 295 html = formencode.htmlfill.render(
298 296 data,
299 297 defaults=defaults,
300 298 encoding="UTF-8",
301 299 force_defaults=False
302 300 )
303 301 return Response(html)
304 302
305 303 @LoginRequired()
306 304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
307 305 @CSRFRequired()
308 306 def user_group_update_perms(self):
309 307 """
310 308 grant permission for given user group
311 309 """
312 310 _ = self.request.translate
313 311
314 312 user_group = self.db_user_group
315 313 user_group_id = user_group.users_group_id
316 314 c = self.load_default_context()
317 315 c.user_group = user_group
318 316 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
319 317
320 318 if not self._rhodecode_user.is_admin:
321 319 if self._revoke_perms_on_yourself(form):
322 320 msg = _('Cannot change permission for yourself as admin')
323 321 h.flash(msg, category='warning')
324 322 raise HTTPFound(
325 323 h.route_path('edit_user_group_perms',
326 324 user_group_id=user_group_id))
327 325
328 326 try:
329 327 changes = UserGroupModel().update_permissions(
330 328 user_group,
331 329 form['perm_additions'], form['perm_updates'],
332 330 form['perm_deletions'])
333 331
334 332 except RepoGroupAssignmentError:
335 333 h.flash(_('Target group cannot be the same'), category='error')
336 334 raise HTTPFound(
337 335 h.route_path('edit_user_group_perms',
338 336 user_group_id=user_group_id))
339 337
340 338 action_data = {
341 339 'added': changes['added'],
342 340 'updated': changes['updated'],
343 341 'deleted': changes['deleted'],
344 342 }
345 343 audit_logger.store_web(
346 344 'user_group.edit.permissions', action_data=action_data,
347 345 user=self._rhodecode_user)
348 346
349 347 Session().commit()
350 348 h.flash(_('User Group permissions updated'), category='success')
351 349
352 350 affected_user_ids = []
353 351 for change in changes['added'] + changes['updated'] + changes['deleted']:
354 352 if change['type'] == 'user':
355 353 affected_user_ids.append(change['id'])
356 354 if change['type'] == 'user_group':
357 355 user_group = UserGroup.get(safe_int(change['id']))
358 356 if user_group:
359 357 group_members_ids = [x.user_id for x in user_group.members]
360 358 affected_user_ids.extend(group_members_ids)
361 359
362 360 PermissionModel().trigger_permission_flush(affected_user_ids)
363 361
364 362 raise HTTPFound(
365 363 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
366 364
367 365 @LoginRequired()
368 366 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
369 367 def user_group_global_perms_edit(self):
370 368 user_group = self.db_user_group
371 369 c = self.load_default_context()
372 370 c.user_group = user_group
373 371 c.active = 'global_perms'
374 372
375 373 c.default_user = User.get_default_user()
376 374 defaults = c.user_group.get_dict()
377 375 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
378 376 defaults.update(c.user_group.get_default_perms())
379 377
380 378 data = render(
381 379 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
382 380 self._get_template_context(c), self.request)
383 381 html = formencode.htmlfill.render(
384 382 data,
385 383 defaults=defaults,
386 384 encoding="UTF-8",
387 385 force_defaults=False
388 386 )
389 387 return Response(html)
390 388
391 389 @LoginRequired()
392 390 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
393 391 @CSRFRequired()
394 392 def user_group_global_perms_update(self):
395 393 _ = self.request.translate
396 394 user_group = self.db_user_group
397 395 user_group_id = self.db_user_group.users_group_id
398 396
399 397 c = self.load_default_context()
400 398 c.user_group = user_group
401 399 c.active = 'global_perms'
402 400
403 401 try:
404 402 # first stage that verifies the checkbox
405 403 _form = UserIndividualPermissionsForm(self.request.translate)
406 404 form_result = _form.to_python(dict(self.request.POST))
407 405 inherit_perms = form_result['inherit_default_permissions']
408 406 user_group.inherit_default_permissions = inherit_perms
409 407 Session().add(user_group)
410 408
411 409 if not inherit_perms:
412 410 # only update the individual ones if we un check the flag
413 411 _form = UserPermissionsForm(
414 412 self.request.translate,
415 413 [x[0] for x in c.repo_create_choices],
416 414 [x[0] for x in c.repo_create_on_write_choices],
417 415 [x[0] for x in c.repo_group_create_choices],
418 416 [x[0] for x in c.user_group_create_choices],
419 417 [x[0] for x in c.fork_choices],
420 418 [x[0] for x in c.inherit_default_permission_choices])()
421 419
422 420 form_result = _form.to_python(dict(self.request.POST))
423 421 form_result.update(
424 422 {'perm_user_group_id': user_group.users_group_id})
425 423
426 424 PermissionModel().update_user_group_permissions(form_result)
427 425
428 426 Session().commit()
429 427 h.flash(_('User Group global permissions updated successfully'),
430 428 category='success')
431 429
432 430 except formencode.Invalid as errors:
433 431 defaults = errors.value
434 432
435 433 data = render(
436 434 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
437 435 self._get_template_context(c), self.request)
438 436 html = formencode.htmlfill.render(
439 437 data,
440 438 defaults=defaults,
441 439 errors=errors.error_dict or {},
442 440 prefix_error=False,
443 441 encoding="UTF-8",
444 442 force_defaults=False
445 443 )
446 444 return Response(html)
447 445 except Exception:
448 446 log.exception("Exception during permissions saving")
449 447 h.flash(_('An error occurred during permissions saving'),
450 448 category='error')
451 449
452 450 raise HTTPFound(
453 451 h.route_path('edit_user_group_global_perms',
454 452 user_group_id=user_group_id))
455 453
456 454 @LoginRequired()
457 455 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
458 456 def user_group_edit_advanced(self):
459 457 user_group = self.db_user_group
460 458
461 459 c = self.load_default_context()
462 460 c.user_group = user_group
463 461 c.active = 'advanced'
464 462 c.group_members_obj = sorted(
465 463 (x.user for x in c.user_group.members),
466 464 key=lambda u: u.username.lower())
467 465
468 466 c.group_to_repos = sorted(
469 467 (x.repository for x in c.user_group.users_group_repo_to_perm),
470 468 key=lambda u: u.repo_name.lower())
471 469
472 470 c.group_to_repo_groups = sorted(
473 471 (x.group for x in c.user_group.users_group_repo_group_to_perm),
474 472 key=lambda u: u.group_name.lower())
475 473
476 474 c.group_to_review_rules = sorted(
477 475 (x.users_group for x in c.user_group.user_group_review_rules),
478 476 key=lambda u: u.users_group_name.lower())
479 477
480 478 return self._get_template_context(c)
481 479
482 480 @LoginRequired()
483 481 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 482 @CSRFRequired()
485 483 def user_group_edit_advanced_set_synchronization(self):
486 484 _ = self.request.translate
487 485 user_group = self.db_user_group
488 486 user_group_id = user_group.users_group_id
489 487
490 488 existing = user_group.group_data.get('extern_type')
491 489
492 490 if existing:
493 491 new_state = user_group.group_data
494 492 new_state['extern_type'] = None
495 493 else:
496 494 new_state = user_group.group_data
497 495 new_state['extern_type'] = 'manual'
498 496 new_state['extern_type_set_by'] = self._rhodecode_user.username
499 497
500 498 try:
501 499 user_group.group_data = new_state
502 500 Session().add(user_group)
503 501 Session().commit()
504 502
505 503 h.flash(_('User Group synchronization updated successfully'),
506 504 category='success')
507 505 except Exception:
508 506 log.exception("Exception during sync settings saving")
509 507 h.flash(_('An error occurred during synchronization update'),
510 508 category='error')
511 509
512 510 raise HTTPFound(
513 511 h.route_path('edit_user_group_advanced',
514 512 user_group_id=user_group_id))
@@ -1,32 +1,30 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
22 20 def includeme(config):
23 21 from rhodecode.apps.user_group_profile.views import UserGroupProfileView
24 22
25 23 config.add_route(
26 24 name='user_group_profile',
27 25 pattern='/_profile_user_group/{user_group_name}')
28 26 config.add_view(
29 27 UserGroupProfileView,
30 28 attr='user_group_profile',
31 29 route_name='user_group_profile', request_method='GET',
32 30 renderer='rhodecode:templates/user_group/user_group.mako')
@@ -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,75 +1,74 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 from rhodecode.model.user_group import UserGroupModel
20 19 from rhodecode.tests import (
21 20 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
22 21 from rhodecode.tests.fixture import Fixture
23 22 from rhodecode.tests.utils import AssertResponse
24 23
25 24 fixture = Fixture()
26 25
27 26
28 27 def route_path(name, **kwargs):
29 28 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
30 29
31 30
32 31 class TestUsersController(TestController):
33 32
34 33 def test_user_group_profile(self, user_util):
35 34 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
36 35 user, usergroup = user_util.create_user_with_group()
37 36
38 37 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
39 38 response.mustcontain(usergroup.users_group_name)
40 39 response.mustcontain(user.username)
41 40
42 41 def test_user_can_check_own_group(self, user_util):
43 42 user = user_util.create_user(
44 43 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
45 44 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
46 45 usergroup = user_util.create_user_group(owner=user)
47 46 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
48 47 response.mustcontain(usergroup.users_group_name)
49 48 response.mustcontain(user.username)
50 49
51 50 def test_user_can_not_check_other_group(self, user_util):
52 51 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
53 52 user_group = user_util.create_user_group()
54 53 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
55 54 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
56 55 assert response.status_code == 404
57 56
58 57 def test_another_user_can_check_if_he_is_in_group(self, user_util):
59 58 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
60 59 user = user_util.create_user(
61 60 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
62 61 user_group = user_util.create_user_group()
63 62 UserGroupModel().add_user_to_group(user_group, user)
64 63 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
65 64 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
66 65 response.mustcontain(user_group.users_group_name)
67 66 response.mustcontain(user.username)
68 67
69 68 def test_with_anonymous_user(self, user_util):
70 69 user = user_util.create_user(
71 70 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
72 71 user_group = user_util.create_user_group()
73 72 UserGroupModel().add_user_to_group(user_group, user)
74 73 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
75 74 assert response.status_code == 302 No newline at end of file
@@ -1,50 +1,48 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
26 24 from rhodecode.apps._base import BaseAppView
27 25 from rhodecode.lib.auth import HasUserGroupPermissionAnyDecorator, LoginRequired, NotAnonymous
28 26 from rhodecode.model.db import UserGroup, User
29 27
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class UserGroupProfileView(BaseAppView):
35 33
36 34 @LoginRequired()
37 35 @NotAnonymous()
38 36 @HasUserGroupPermissionAnyDecorator('usergroup.read', 'usergroup.write', 'usergroup.admin',)
39 37 def user_group_profile(self):
40 38 c = self._get_local_tmpl_context()
41 39 c.active = 'profile'
42 40 self.db_user_group_name = self.request.matchdict.get('user_group_name')
43 41 c.user_group = UserGroup().get_by_group_name(self.db_user_group_name)
44 42 if not c.user_group:
45 43 raise HTTPNotFound()
46 44 group_members_obj = sorted((x.user for x in c.user_group.members),
47 45 key=lambda u: u.username.lower())
48 46 c.group_members = group_members_obj
49 47 c.anonymous = self._rhodecode_user.username == User.DEFAULT_USER
50 48 return self._get_template_context(c)
@@ -1,32 +1,30 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
22 20 def includeme(config):
23 21 from rhodecode.apps.user_profile.views import UserProfileView
24 22
25 23 config.add_route(
26 24 name='user_profile',
27 25 pattern='/_profiles/{username}')
28 26 config.add_view(
29 27 UserProfileView,
30 28 attr='user_profile',
31 29 route_name='user_profile', request_method='GET',
32 30 renderer='rhodecode:templates/users/user.mako')
@@ -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,74 +1,73 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.model.db import User
23 22 from rhodecode.tests import (
24 23 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
25 24 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
26 25 from rhodecode.tests.fixture import Fixture
27 26 from rhodecode.tests.utils import AssertResponse
28 27
29 28 fixture = Fixture()
30 29
31 30
32 31 def route_path(name, **kwargs):
33 32 return '/_profiles/{username}'.format(**kwargs)
34 33
35 34
36 35 class TestUsersController(TestController):
37 36
38 37 def test_user_profile(self, user_util):
39 38 edit_link_css = '.user-profile .panel-edit'
40 39 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
41 40 user = user_util.create_user(
42 41 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
43 42 username = user.username
44 43
45 44 response = self.app.get(route_path('user_profile', username=username))
46 45 response.mustcontain('testme')
47 46 response.mustcontain('testme@rhodecode.org')
48 47 assert_response = response.assert_response()
49 48 assert_response.no_element_exists(edit_link_css)
50 49
51 50 # edit should be available to superadmin users
52 51 self.logout_user()
53 52 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
54 53 response = self.app.get(route_path('user_profile', username=username))
55 54 assert_response = response.assert_response()
56 55 assert_response.element_contains(edit_link_css, 'Edit')
57 56
58 57 def test_user_profile_not_available(self, user_util):
59 58 user = user_util.create_user()
60 59 username = user.username
61 60
62 61 # not logged in, redirect
63 62 self.app.get(route_path('user_profile', username=username), status=302)
64 63
65 64 self.log_user()
66 65 # after log-in show
67 66 self.app.get(route_path('user_profile', username=username), status=200)
68 67
69 68 # default user, not allowed to show it
70 69 self.app.get(
71 70 route_path('user_profile', username=User.DEFAULT_USER), status=404)
72 71
73 72 # actual 404
74 73 self.app.get(route_path('user_profile', username='unknown'), status=404)
@@ -1,49 +1,47 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.lib.auth import LoginRequired, NotAnonymous
27 25
28 26 from rhodecode.model.db import User
29 27 from rhodecode.model.user import UserModel
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class UserProfileView(BaseAppView):
35 33
36 34 @LoginRequired()
37 35 @NotAnonymous()
38 36 def user_profile(self):
39 37 # register local template context
40 38 c = self._get_local_tmpl_context()
41 39 c.active = 'user_profile'
42 40
43 41 username = self.request.matchdict.get('username')
44 42
45 43 c.user = UserModel().get_by_username(username)
46 44 if not c.user or c.user.username == User.DEFAULT_USER:
47 45 raise HTTPNotFound()
48 46
49 47 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now