##// 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 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,860 +1,858 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import time
19 import time
22 import logging
20 import logging
23 import operator
21 import operator
24
22
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26
24
27 from rhodecode.lib import helpers as h, diffs, rc_cache
25 from rhodecode.lib import helpers as h, diffs, rc_cache
28 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.lib.str_utils import safe_str
29 from rhodecode.lib.utils import repo_name_slug
27 from rhodecode.lib.utils import repo_name_slug
30 from rhodecode.lib.utils2 import (
28 from rhodecode.lib.utils2 import (
31 StrictAttributeDict, str2bool, safe_int, datetime_to_time)
29 StrictAttributeDict, str2bool, safe_int, datetime_to_time)
32 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
30 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
33 from rhodecode.lib.vcs.backends.base import EmptyCommit
31 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
32 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
35 from rhodecode.model import repo
33 from rhodecode.model import repo
36 from rhodecode.model import repo_group
34 from rhodecode.model import repo_group
37 from rhodecode.model import user_group
35 from rhodecode.model import user_group
38 from rhodecode.model import user
36 from rhodecode.model import user
39 from rhodecode.model.db import User
37 from rhodecode.model.db import User
40 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
39 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
42 from rhodecode.model.repo import ReadmeFinder
40 from rhodecode.model.repo import ReadmeFinder
43
41
44 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
45
43
46
44
47 ADMIN_PREFIX = '/_admin'
45 ADMIN_PREFIX = '/_admin'
48 STATIC_FILE_PREFIX = '/_static'
46 STATIC_FILE_PREFIX = '/_static'
49
47
50 URL_NAME_REQUIREMENTS = {
48 URL_NAME_REQUIREMENTS = {
51 # group name can have a slash in them, but they must not end with a slash
49 # group name can have a slash in them, but they must not end with a slash
52 'group_name': r'.*?[^/]',
50 'group_name': r'.*?[^/]',
53 'repo_group_name': r'.*?[^/]',
51 'repo_group_name': r'.*?[^/]',
54 # repo names can have a slash in them, but they must not end with a slash
52 # repo names can have a slash in them, but they must not end with a slash
55 'repo_name': r'.*?[^/]',
53 'repo_name': r'.*?[^/]',
56 # file path eats up everything at the end
54 # file path eats up everything at the end
57 'f_path': r'.*',
55 'f_path': r'.*',
58 # reference types
56 # reference types
59 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
57 'source_ref_type': r'(branch|book|tag|rev|\%\(source_ref_type\)s)',
60 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
58 'target_ref_type': r'(branch|book|tag|rev|\%\(target_ref_type\)s)',
61 }
59 }
62
60
63
61
64 def add_route_with_slash(config,name, pattern, **kw):
62 def add_route_with_slash(config,name, pattern, **kw):
65 config.add_route(name, pattern, **kw)
63 config.add_route(name, pattern, **kw)
66 if not pattern.endswith('/'):
64 if not pattern.endswith('/'):
67 config.add_route(name + '_slash', pattern + '/', **kw)
65 config.add_route(name + '_slash', pattern + '/', **kw)
68
66
69
67
70 def add_route_requirements(route_path, requirements=None):
68 def add_route_requirements(route_path, requirements=None):
71 """
69 """
72 Adds regex requirements to pyramid routes using a mapping dict
70 Adds regex requirements to pyramid routes using a mapping dict
73 e.g::
71 e.g::
74 add_route_requirements('{repo_name}/settings')
72 add_route_requirements('{repo_name}/settings')
75 """
73 """
76 requirements = requirements or URL_NAME_REQUIREMENTS
74 requirements = requirements or URL_NAME_REQUIREMENTS
77 for key, regex in list(requirements.items()):
75 for key, regex in list(requirements.items()):
78 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
76 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
79 return route_path
77 return route_path
80
78
81
79
82 def get_format_ref_id(repo):
80 def get_format_ref_id(repo):
83 """Returns a `repo` specific reference formatter function"""
81 """Returns a `repo` specific reference formatter function"""
84 if h.is_svn(repo):
82 if h.is_svn(repo):
85 return _format_ref_id_svn
83 return _format_ref_id_svn
86 else:
84 else:
87 return _format_ref_id
85 return _format_ref_id
88
86
89
87
90 def _format_ref_id(name, raw_id):
88 def _format_ref_id(name, raw_id):
91 """Default formatting of a given reference `name`"""
89 """Default formatting of a given reference `name`"""
92 return name
90 return name
93
91
94
92
95 def _format_ref_id_svn(name, raw_id):
93 def _format_ref_id_svn(name, raw_id):
96 """Special way of formatting a reference for Subversion including path"""
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 class TemplateArgs(StrictAttributeDict):
98 class TemplateArgs(StrictAttributeDict):
101 pass
99 pass
102
100
103
101
104 class BaseAppView(object):
102 class BaseAppView(object):
105
103
106 def __init__(self, context, request):
104 def __init__(self, context, request):
107 self.request = request
105 self.request = request
108 self.context = context
106 self.context = context
109 self.session = request.session
107 self.session = request.session
110 if not hasattr(request, 'user'):
108 if not hasattr(request, 'user'):
111 # NOTE(marcink): edge case, we ended up in matched route
109 # NOTE(marcink): edge case, we ended up in matched route
112 # but probably of web-app context, e.g API CALL/VCS CALL
110 # but probably of web-app context, e.g API CALL/VCS CALL
113 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
111 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
114 log.warning('Unable to process request `%s` in this scope', request)
112 log.warning('Unable to process request `%s` in this scope', request)
115 raise HTTPBadRequest()
113 raise HTTPBadRequest()
116
114
117 self._rhodecode_user = request.user # auth user
115 self._rhodecode_user = request.user # auth user
118 self._rhodecode_db_user = self._rhodecode_user.get_instance()
116 self._rhodecode_db_user = self._rhodecode_user.get_instance()
119 self._maybe_needs_password_change(
117 self._maybe_needs_password_change(
120 request.matched_route.name, self._rhodecode_db_user)
118 request.matched_route.name, self._rhodecode_db_user)
121
119
122 def _maybe_needs_password_change(self, view_name, user_obj):
120 def _maybe_needs_password_change(self, view_name, user_obj):
123
121
124 dont_check_views = [
122 dont_check_views = [
125 'channelstream_connect',
123 'channelstream_connect',
126 'ops_ping'
124 'ops_ping'
127 ]
125 ]
128 if view_name in dont_check_views:
126 if view_name in dont_check_views:
129 return
127 return
130
128
131 log.debug('Checking if user %s needs password change on view %s',
129 log.debug('Checking if user %s needs password change on view %s',
132 user_obj, view_name)
130 user_obj, view_name)
133
131
134 skip_user_views = [
132 skip_user_views = [
135 'logout', 'login',
133 'logout', 'login',
136 'my_account_password', 'my_account_password_update'
134 'my_account_password', 'my_account_password_update'
137 ]
135 ]
138
136
139 if not user_obj:
137 if not user_obj:
140 return
138 return
141
139
142 if user_obj.username == User.DEFAULT_USER:
140 if user_obj.username == User.DEFAULT_USER:
143 return
141 return
144
142
145 now = time.time()
143 now = time.time()
146 should_change = user_obj.user_data.get('force_password_change')
144 should_change = user_obj.user_data.get('force_password_change')
147 change_after = safe_int(should_change) or 0
145 change_after = safe_int(should_change) or 0
148 if should_change and now > change_after:
146 if should_change and now > change_after:
149 log.debug('User %s requires password change', user_obj)
147 log.debug('User %s requires password change', user_obj)
150 h.flash('You are required to change your password', 'warning',
148 h.flash('You are required to change your password', 'warning',
151 ignore_duplicate=True)
149 ignore_duplicate=True)
152
150
153 if view_name not in skip_user_views:
151 if view_name not in skip_user_views:
154 raise HTTPFound(
152 raise HTTPFound(
155 self.request.route_path('my_account_password'))
153 self.request.route_path('my_account_password'))
156
154
157 def _log_creation_exception(self, e, repo_name):
155 def _log_creation_exception(self, e, repo_name):
158 _ = self.request.translate
156 _ = self.request.translate
159 reason = None
157 reason = None
160 if len(e.args) == 2:
158 if len(e.args) == 2:
161 reason = e.args[1]
159 reason = e.args[1]
162
160
163 if reason == 'INVALID_CERTIFICATE':
161 if reason == 'INVALID_CERTIFICATE':
164 log.exception(
162 log.exception(
165 'Exception creating a repository: invalid certificate')
163 'Exception creating a repository: invalid certificate')
166 msg = (_('Error creating repository %s: invalid certificate')
164 msg = (_('Error creating repository %s: invalid certificate')
167 % repo_name)
165 % repo_name)
168 else:
166 else:
169 log.exception("Exception creating a repository")
167 log.exception("Exception creating a repository")
170 msg = (_('Error creating repository %s')
168 msg = (_('Error creating repository %s')
171 % repo_name)
169 % repo_name)
172 return msg
170 return msg
173
171
174 def _get_local_tmpl_context(self, include_app_defaults=True):
172 def _get_local_tmpl_context(self, include_app_defaults=True):
175 c = TemplateArgs()
173 c = TemplateArgs()
176 c.auth_user = self.request.user
174 c.auth_user = self.request.user
177 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
175 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
178 c.rhodecode_user = self.request.user
176 c.rhodecode_user = self.request.user
179
177
180 if include_app_defaults:
178 if include_app_defaults:
181 from rhodecode.lib.base import attach_context_attributes
179 from rhodecode.lib.base import attach_context_attributes
182 attach_context_attributes(c, self.request, self.request.user.user_id)
180 attach_context_attributes(c, self.request, self.request.user.user_id)
183
181
184 c.is_super_admin = c.auth_user.is_admin
182 c.is_super_admin = c.auth_user.is_admin
185
183
186 c.can_create_repo = c.is_super_admin
184 c.can_create_repo = c.is_super_admin
187 c.can_create_repo_group = c.is_super_admin
185 c.can_create_repo_group = c.is_super_admin
188 c.can_create_user_group = c.is_super_admin
186 c.can_create_user_group = c.is_super_admin
189
187
190 c.is_delegated_admin = False
188 c.is_delegated_admin = False
191
189
192 if not c.auth_user.is_default and not c.is_super_admin:
190 if not c.auth_user.is_default and not c.is_super_admin:
193 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
191 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
194 user=self.request.user)
192 user=self.request.user)
195 repositories = c.auth_user.repositories_admin or c.can_create_repo
193 repositories = c.auth_user.repositories_admin or c.can_create_repo
196
194
197 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
195 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
198 user=self.request.user)
196 user=self.request.user)
199 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
197 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
200
198
201 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
199 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
202 user=self.request.user)
200 user=self.request.user)
203 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
201 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
204 # delegated admin can create, or manage some objects
202 # delegated admin can create, or manage some objects
205 c.is_delegated_admin = repositories or repository_groups or user_groups
203 c.is_delegated_admin = repositories or repository_groups or user_groups
206 return c
204 return c
207
205
208 def _get_template_context(self, tmpl_args, **kwargs):
206 def _get_template_context(self, tmpl_args, **kwargs):
209
207
210 local_tmpl_args = {
208 local_tmpl_args = {
211 'defaults': {},
209 'defaults': {},
212 'errors': {},
210 'errors': {},
213 'c': tmpl_args
211 'c': tmpl_args
214 }
212 }
215 local_tmpl_args.update(kwargs)
213 local_tmpl_args.update(kwargs)
216 return local_tmpl_args
214 return local_tmpl_args
217
215
218 def load_default_context(self):
216 def load_default_context(self):
219 """
217 """
220 example:
218 example:
221
219
222 def load_default_context(self):
220 def load_default_context(self):
223 c = self._get_local_tmpl_context()
221 c = self._get_local_tmpl_context()
224 c.custom_var = 'foobar'
222 c.custom_var = 'foobar'
225
223
226 return c
224 return c
227 """
225 """
228 raise NotImplementedError('Needs implementation in view class')
226 raise NotImplementedError('Needs implementation in view class')
229
227
230
228
231 class RepoAppView(BaseAppView):
229 class RepoAppView(BaseAppView):
232
230
233 def __init__(self, context, request):
231 def __init__(self, context, request):
234 super(RepoAppView, self).__init__(context, request)
232 super().__init__(context, request)
235 self.db_repo = request.db_repo
233 self.db_repo = request.db_repo
236 self.db_repo_name = self.db_repo.repo_name
234 self.db_repo_name = self.db_repo.repo_name
237 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
235 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
238 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
236 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
239 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
237 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
240
238
241 def _handle_missing_requirements(self, error):
239 def _handle_missing_requirements(self, error):
242 log.error(
240 log.error(
243 'Requirements are missing for repository %s: %s',
241 'Requirements are missing for repository %s: %s',
244 self.db_repo_name, safe_str(error))
242 self.db_repo_name, safe_str(error))
245
243
246 def _prepare_and_set_clone_url(self, c):
244 def _prepare_and_set_clone_url(self, c):
247 username = ''
245 username = ''
248 if self._rhodecode_user.username != User.DEFAULT_USER:
246 if self._rhodecode_user.username != User.DEFAULT_USER:
249 username = self._rhodecode_user.username
247 username = self._rhodecode_user.username
250
248
251 _def_clone_uri = c.clone_uri_tmpl
249 _def_clone_uri = c.clone_uri_tmpl
252 _def_clone_uri_id = c.clone_uri_id_tmpl
250 _def_clone_uri_id = c.clone_uri_id_tmpl
253 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
251 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
254
252
255 c.clone_repo_url = self.db_repo.clone_url(
253 c.clone_repo_url = self.db_repo.clone_url(
256 user=username, uri_tmpl=_def_clone_uri)
254 user=username, uri_tmpl=_def_clone_uri)
257 c.clone_repo_url_id = self.db_repo.clone_url(
255 c.clone_repo_url_id = self.db_repo.clone_url(
258 user=username, uri_tmpl=_def_clone_uri_id)
256 user=username, uri_tmpl=_def_clone_uri_id)
259 c.clone_repo_url_ssh = self.db_repo.clone_url(
257 c.clone_repo_url_ssh = self.db_repo.clone_url(
260 uri_tmpl=_def_clone_uri_ssh, ssh=True)
258 uri_tmpl=_def_clone_uri_ssh, ssh=True)
261
259
262 def _get_local_tmpl_context(self, include_app_defaults=True):
260 def _get_local_tmpl_context(self, include_app_defaults=True):
263 _ = self.request.translate
261 _ = self.request.translate
264 c = super(RepoAppView, self)._get_local_tmpl_context(
262 c = super()._get_local_tmpl_context(
265 include_app_defaults=include_app_defaults)
263 include_app_defaults=include_app_defaults)
266
264
267 # register common vars for this type of view
265 # register common vars for this type of view
268 c.rhodecode_db_repo = self.db_repo
266 c.rhodecode_db_repo = self.db_repo
269 c.repo_name = self.db_repo_name
267 c.repo_name = self.db_repo_name
270 c.repository_pull_requests = self.db_repo_pull_requests
268 c.repository_pull_requests = self.db_repo_pull_requests
271 c.repository_artifacts = self.db_repo_artifacts
269 c.repository_artifacts = self.db_repo_artifacts
272 c.repository_is_user_following = ScmModel().is_following_repo(
270 c.repository_is_user_following = ScmModel().is_following_repo(
273 self.db_repo_name, self._rhodecode_user.user_id)
271 self.db_repo_name, self._rhodecode_user.user_id)
274 self.path_filter = PathFilter(None)
272 self.path_filter = PathFilter(None)
275
273
276 c.repository_requirements_missing = {}
274 c.repository_requirements_missing = {}
277 try:
275 try:
278 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
276 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
279 # NOTE(marcink):
277 # NOTE(marcink):
280 # comparison to None since if it's an object __bool__ is expensive to
278 # comparison to None since if it's an object __bool__ is expensive to
281 # calculate
279 # calculate
282 if self.rhodecode_vcs_repo is not None:
280 if self.rhodecode_vcs_repo is not None:
283 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
281 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
284 c.auth_user.username)
282 c.auth_user.username)
285 self.path_filter = PathFilter(path_perms)
283 self.path_filter = PathFilter(path_perms)
286 except RepositoryRequirementError as e:
284 except RepositoryRequirementError as e:
287 c.repository_requirements_missing = {'error': str(e)}
285 c.repository_requirements_missing = {'error': str(e)}
288 self._handle_missing_requirements(e)
286 self._handle_missing_requirements(e)
289 self.rhodecode_vcs_repo = None
287 self.rhodecode_vcs_repo = None
290
288
291 c.path_filter = self.path_filter # used by atom_feed_entry.mako
289 c.path_filter = self.path_filter # used by atom_feed_entry.mako
292
290
293 if self.rhodecode_vcs_repo is None:
291 if self.rhodecode_vcs_repo is None:
294 # unable to fetch this repo as vcs instance, report back to user
292 # unable to fetch this repo as vcs instance, report back to user
295 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
293 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
296 h.flash(_(
294 h.flash(_(
297 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
295 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
298 "Please check if it exist, or is not damaged.") %
296 "Please check if it exist, or is not damaged.") %
299 {'repo_name': c.repo_name},
297 {'repo_name': c.repo_name},
300 category='error', ignore_duplicate=True)
298 category='error', ignore_duplicate=True)
301 if c.repository_requirements_missing:
299 if c.repository_requirements_missing:
302 route = self.request.matched_route.name
300 route = self.request.matched_route.name
303 if route.startswith(('edit_repo', 'repo_summary')):
301 if route.startswith(('edit_repo', 'repo_summary')):
304 # allow summary and edit repo on missing requirements
302 # allow summary and edit repo on missing requirements
305 return c
303 return c
306
304
307 raise HTTPFound(
305 raise HTTPFound(
308 h.route_path('repo_summary', repo_name=self.db_repo_name))
306 h.route_path('repo_summary', repo_name=self.db_repo_name))
309
307
310 else: # redirect if we don't show missing requirements
308 else: # redirect if we don't show missing requirements
311 raise HTTPFound(h.route_path('home'))
309 raise HTTPFound(h.route_path('home'))
312
310
313 c.has_origin_repo_read_perm = False
311 c.has_origin_repo_read_perm = False
314 if self.db_repo.fork:
312 if self.db_repo.fork:
315 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
313 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
316 'repository.write', 'repository.read', 'repository.admin')(
314 'repository.write', 'repository.read', 'repository.admin')(
317 self.db_repo.fork.repo_name, 'summary fork link')
315 self.db_repo.fork.repo_name, 'summary fork link')
318
316
319 return c
317 return c
320
318
321 def _get_f_path_unchecked(self, matchdict, default=None):
319 def _get_f_path_unchecked(self, matchdict, default=None):
322 """
320 """
323 Should only be used by redirects, everything else should call _get_f_path
321 Should only be used by redirects, everything else should call _get_f_path
324 """
322 """
325 f_path = matchdict.get('f_path')
323 f_path = matchdict.get('f_path')
326 if f_path:
324 if f_path:
327 # fix for multiple initial slashes that causes errors for GIT
325 # fix for multiple initial slashes that causes errors for GIT
328 return f_path.lstrip('/')
326 return f_path.lstrip('/')
329
327
330 return default
328 return default
331
329
332 def _get_f_path(self, matchdict, default=None):
330 def _get_f_path(self, matchdict, default=None):
333 f_path_match = self._get_f_path_unchecked(matchdict, default)
331 f_path_match = self._get_f_path_unchecked(matchdict, default)
334 return self.path_filter.assert_path_permissions(f_path_match)
332 return self.path_filter.assert_path_permissions(f_path_match)
335
333
336 def _get_general_setting(self, target_repo, settings_key, default=False):
334 def _get_general_setting(self, target_repo, settings_key, default=False):
337 settings_model = VcsSettingsModel(repo=target_repo)
335 settings_model = VcsSettingsModel(repo=target_repo)
338 settings = settings_model.get_general_settings()
336 settings = settings_model.get_general_settings()
339 return settings.get(settings_key, default)
337 return settings.get(settings_key, default)
340
338
341 def _get_repo_setting(self, target_repo, settings_key, default=False):
339 def _get_repo_setting(self, target_repo, settings_key, default=False):
342 settings_model = VcsSettingsModel(repo=target_repo)
340 settings_model = VcsSettingsModel(repo=target_repo)
343 settings = settings_model.get_repo_settings_inherited()
341 settings = settings_model.get_repo_settings_inherited()
344 return settings.get(settings_key, default)
342 return settings.get(settings_key, default)
345
343
346 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
344 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
347 log.debug('Looking for README file at path %s', path)
345 log.debug('Looking for README file at path %s', path)
348 if commit_id:
346 if commit_id:
349 landing_commit_id = commit_id
347 landing_commit_id = commit_id
350 else:
348 else:
351 landing_commit = db_repo.get_landing_commit()
349 landing_commit = db_repo.get_landing_commit()
352 if isinstance(landing_commit, EmptyCommit):
350 if isinstance(landing_commit, EmptyCommit):
353 return None, None
351 return None, None
354 landing_commit_id = landing_commit.raw_id
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 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid, use_async_runner=True)
355 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid, use_async_runner=True)
358 start = time.time()
356 start = time.time()
359
357
360 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
358 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
361 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
359 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
362 readme_data = None
360 readme_data = None
363 readme_filename = None
361 readme_filename = None
364
362
365 commit = db_repo.get_commit(_commit_id)
363 commit = db_repo.get_commit(_commit_id)
366 log.debug("Searching for a README file at commit %s.", _commit_id)
364 log.debug("Searching for a README file at commit %s.", _commit_id)
367 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
365 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
368
366
369 if readme_node:
367 if readme_node:
370 log.debug('Found README node: %s', readme_node)
368 log.debug('Found README node: %s', readme_node)
371 relative_urls = {
369 relative_urls = {
372 'raw': h.route_path(
370 'raw': h.route_path(
373 'repo_file_raw', repo_name=_repo_name,
371 'repo_file_raw', repo_name=_repo_name,
374 commit_id=commit.raw_id, f_path=readme_node.path),
372 commit_id=commit.raw_id, f_path=readme_node.path),
375 'standard': h.route_path(
373 'standard': h.route_path(
376 'repo_files', repo_name=_repo_name,
374 'repo_files', repo_name=_repo_name,
377 commit_id=commit.raw_id, f_path=readme_node.path),
375 commit_id=commit.raw_id, f_path=readme_node.path),
378 }
376 }
379
377
380 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
378 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
381 readme_filename = readme_node.str_path
379 readme_filename = readme_node.str_path
382
380
383 return readme_data, readme_filename
381 return readme_data, readme_filename
384
382
385 readme_data, readme_filename = generate_repo_readme(
383 readme_data, readme_filename = generate_repo_readme(
386 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
384 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
387
385
388 compute_time = time.time() - start
386 compute_time = time.time() - start
389 log.debug('Repo README for path %s generated and computed in %.4fs',
387 log.debug('Repo README for path %s generated and computed in %.4fs',
390 path, compute_time)
388 path, compute_time)
391 return readme_data, readme_filename
389 return readme_data, readme_filename
392
390
393 def _render_readme_or_none(self, commit, readme_node, relative_urls):
391 def _render_readme_or_none(self, commit, readme_node, relative_urls):
394 log.debug('Found README file `%s` rendering...', readme_node.path)
392 log.debug('Found README file `%s` rendering...', readme_node.path)
395 renderer = MarkupRenderer()
393 renderer = MarkupRenderer()
396 try:
394 try:
397 html_source = renderer.render(
395 html_source = renderer.render(
398 readme_node.str_content, filename=readme_node.path)
396 readme_node.str_content, filename=readme_node.path)
399 if relative_urls:
397 if relative_urls:
400 return relative_links(html_source, relative_urls)
398 return relative_links(html_source, relative_urls)
401 return html_source
399 return html_source
402 except Exception:
400 except Exception:
403 log.exception("Exception while trying to render the README")
401 log.exception("Exception while trying to render the README")
404
402
405 def get_recache_flag(self):
403 def get_recache_flag(self):
406 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
404 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
407 flag_val = self.request.GET.get(flag_name)
405 flag_val = self.request.GET.get(flag_name)
408 if str2bool(flag_val):
406 if str2bool(flag_val):
409 return True
407 return True
410 return False
408 return False
411
409
412 def get_commit_preload_attrs(cls):
410 def get_commit_preload_attrs(cls):
413 pre_load = ['author', 'branch', 'date', 'message', 'parents',
411 pre_load = ['author', 'branch', 'date', 'message', 'parents',
414 'obsolete', 'phase', 'hidden']
412 'obsolete', 'phase', 'hidden']
415 return pre_load
413 return pre_load
416
414
417
415
418 class PathFilter(object):
416 class PathFilter(object):
419
417
420 # Expects and instance of BasePathPermissionChecker or None
418 # Expects and instance of BasePathPermissionChecker or None
421 def __init__(self, permission_checker):
419 def __init__(self, permission_checker):
422 self.permission_checker = permission_checker
420 self.permission_checker = permission_checker
423
421
424 def assert_path_permissions(self, path):
422 def assert_path_permissions(self, path):
425 if self.path_access_allowed(path):
423 if self.path_access_allowed(path):
426 return path
424 return path
427 raise HTTPForbidden()
425 raise HTTPForbidden()
428
426
429 def path_access_allowed(self, path):
427 def path_access_allowed(self, path):
430 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
428 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
431 if self.permission_checker:
429 if self.permission_checker:
432 has_access = path and self.permission_checker.has_access(path)
430 has_access = path and self.permission_checker.has_access(path)
433 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
431 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
434 return has_access
432 return has_access
435
433
436 log.debug('ACL permissions checker not enabled, skipping...')
434 log.debug('ACL permissions checker not enabled, skipping...')
437 return True
435 return True
438
436
439 def filter_patchset(self, patchset):
437 def filter_patchset(self, patchset):
440 if not self.permission_checker or not patchset:
438 if not self.permission_checker or not patchset:
441 return patchset, False
439 return patchset, False
442 had_filtered = False
440 had_filtered = False
443 filtered_patchset = []
441 filtered_patchset = []
444 for patch in patchset:
442 for patch in patchset:
445 filename = patch.get('filename', None)
443 filename = patch.get('filename', None)
446 if not filename or self.permission_checker.has_access(filename):
444 if not filename or self.permission_checker.has_access(filename):
447 filtered_patchset.append(patch)
445 filtered_patchset.append(patch)
448 else:
446 else:
449 had_filtered = True
447 had_filtered = True
450 if had_filtered:
448 if had_filtered:
451 if isinstance(patchset, diffs.LimitedDiffContainer):
449 if isinstance(patchset, diffs.LimitedDiffContainer):
452 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
450 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
453 return filtered_patchset, True
451 return filtered_patchset, True
454 else:
452 else:
455 return patchset, False
453 return patchset, False
456
454
457 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
455 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
458
456
459 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
457 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
460 result = diffset.render_patchset(
458 result = diffset.render_patchset(
461 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
459 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
462 result.has_hidden_changes = has_hidden_changes
460 result.has_hidden_changes = has_hidden_changes
463 return result
461 return result
464
462
465 def get_raw_patch(self, diff_processor):
463 def get_raw_patch(self, diff_processor):
466 if self.permission_checker is None:
464 if self.permission_checker is None:
467 return diff_processor.as_raw()
465 return diff_processor.as_raw()
468 elif self.permission_checker.has_full_access:
466 elif self.permission_checker.has_full_access:
469 return diff_processor.as_raw()
467 return diff_processor.as_raw()
470 else:
468 else:
471 return '# Repository has user-specific filters, raw patch generation is disabled.'
469 return '# Repository has user-specific filters, raw patch generation is disabled.'
472
470
473 @property
471 @property
474 def is_enabled(self):
472 def is_enabled(self):
475 return self.permission_checker is not None
473 return self.permission_checker is not None
476
474
477
475
478 class RepoGroupAppView(BaseAppView):
476 class RepoGroupAppView(BaseAppView):
479 def __init__(self, context, request):
477 def __init__(self, context, request):
480 super(RepoGroupAppView, self).__init__(context, request)
478 super().__init__(context, request)
481 self.db_repo_group = request.db_repo_group
479 self.db_repo_group = request.db_repo_group
482 self.db_repo_group_name = self.db_repo_group.group_name
480 self.db_repo_group_name = self.db_repo_group.group_name
483
481
484 def _get_local_tmpl_context(self, include_app_defaults=True):
482 def _get_local_tmpl_context(self, include_app_defaults=True):
485 _ = self.request.translate
483 _ = self.request.translate
486 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
484 c = super()._get_local_tmpl_context(
487 include_app_defaults=include_app_defaults)
485 include_app_defaults=include_app_defaults)
488 c.repo_group = self.db_repo_group
486 c.repo_group = self.db_repo_group
489 return c
487 return c
490
488
491 def _revoke_perms_on_yourself(self, form_result):
489 def _revoke_perms_on_yourself(self, form_result):
492 _updates = [u for u in form_result['perm_updates'] if self._rhodecode_user.user_id == int(u[0])]
490 _updates = [u for u in form_result['perm_updates'] if self._rhodecode_user.user_id == int(u[0])]
493 _additions = [u for u in form_result['perm_additions'] if self._rhodecode_user.user_id == int(u[0])]
491 _additions = [u for u in form_result['perm_additions'] if self._rhodecode_user.user_id == int(u[0])]
494 _deletions = [u for u in form_result['perm_deletions'] if self._rhodecode_user.user_id == int(u[0])]
492 _deletions = [u for u in form_result['perm_deletions'] if self._rhodecode_user.user_id == int(u[0])]
495 admin_perm = 'group.admin'
493 admin_perm = 'group.admin'
496 if _updates and _updates[0][1] != admin_perm or \
494 if _updates and _updates[0][1] != admin_perm or \
497 _additions and _additions[0][1] != admin_perm or \
495 _additions and _additions[0][1] != admin_perm or \
498 _deletions and _deletions[0][1] != admin_perm:
496 _deletions and _deletions[0][1] != admin_perm:
499 return True
497 return True
500 return False
498 return False
501
499
502
500
503 class UserGroupAppView(BaseAppView):
501 class UserGroupAppView(BaseAppView):
504 def __init__(self, context, request):
502 def __init__(self, context, request):
505 super(UserGroupAppView, self).__init__(context, request)
503 super().__init__(context, request)
506 self.db_user_group = request.db_user_group
504 self.db_user_group = request.db_user_group
507 self.db_user_group_name = self.db_user_group.users_group_name
505 self.db_user_group_name = self.db_user_group.users_group_name
508
506
509
507
510 class UserAppView(BaseAppView):
508 class UserAppView(BaseAppView):
511 def __init__(self, context, request):
509 def __init__(self, context, request):
512 super(UserAppView, self).__init__(context, request)
510 super().__init__(context, request)
513 self.db_user = request.db_user
511 self.db_user = request.db_user
514 self.db_user_id = self.db_user.user_id
512 self.db_user_id = self.db_user.user_id
515
513
516 _ = self.request.translate
514 _ = self.request.translate
517 if not request.db_user_supports_default:
515 if not request.db_user_supports_default:
518 if self.db_user.username == User.DEFAULT_USER:
516 if self.db_user.username == User.DEFAULT_USER:
519 h.flash(_("Editing user `{}` is disabled.".format(
517 h.flash(_("Editing user `{}` is disabled.".format(
520 User.DEFAULT_USER)), category='warning')
518 User.DEFAULT_USER)), category='warning')
521 raise HTTPFound(h.route_path('users'))
519 raise HTTPFound(h.route_path('users'))
522
520
523
521
524 class DataGridAppView(object):
522 class DataGridAppView(object):
525 """
523 """
526 Common class to have re-usable grid rendering components
524 Common class to have re-usable grid rendering components
527 """
525 """
528
526
529 def _extract_ordering(self, request, column_map=None):
527 def _extract_ordering(self, request, column_map=None):
530 column_map = column_map or {}
528 column_map = column_map or {}
531 column_index = safe_int(request.GET.get('order[0][column]'))
529 column_index = safe_int(request.GET.get('order[0][column]'))
532 order_dir = request.GET.get(
530 order_dir = request.GET.get(
533 'order[0][dir]', 'desc')
531 'order[0][dir]', 'desc')
534 order_by = request.GET.get(
532 order_by = request.GET.get(
535 'columns[%s][data][sort]' % column_index, 'name_raw')
533 'columns[%s][data][sort]' % column_index, 'name_raw')
536
534
537 # translate datatable to DB columns
535 # translate datatable to DB columns
538 order_by = column_map.get(order_by) or order_by
536 order_by = column_map.get(order_by) or order_by
539
537
540 search_q = request.GET.get('search[value]')
538 search_q = request.GET.get('search[value]')
541 return search_q, order_by, order_dir
539 return search_q, order_by, order_dir
542
540
543 def _extract_chunk(self, request):
541 def _extract_chunk(self, request):
544 start = safe_int(request.GET.get('start'), 0)
542 start = safe_int(request.GET.get('start'), 0)
545 length = safe_int(request.GET.get('length'), 25)
543 length = safe_int(request.GET.get('length'), 25)
546 draw = safe_int(request.GET.get('draw'))
544 draw = safe_int(request.GET.get('draw'))
547 return draw, start, length
545 return draw, start, length
548
546
549 def _get_order_col(self, order_by, model):
547 def _get_order_col(self, order_by, model):
550 if isinstance(order_by, str):
548 if isinstance(order_by, str):
551 try:
549 try:
552 return operator.attrgetter(order_by)(model)
550 return operator.attrgetter(order_by)(model)
553 except AttributeError:
551 except AttributeError:
554 return None
552 return None
555 else:
553 else:
556 return order_by
554 return order_by
557
555
558
556
559 class BaseReferencesView(RepoAppView):
557 class BaseReferencesView(RepoAppView):
560 """
558 """
561 Base for reference view for branches, tags and bookmarks.
559 Base for reference view for branches, tags and bookmarks.
562 """
560 """
563 def load_default_context(self):
561 def load_default_context(self):
564 c = self._get_local_tmpl_context()
562 c = self._get_local_tmpl_context()
565 return c
563 return c
566
564
567 def load_refs_context(self, ref_items, partials_template):
565 def load_refs_context(self, ref_items, partials_template):
568 _render = self.request.get_partial_renderer(partials_template)
566 _render = self.request.get_partial_renderer(partials_template)
569 pre_load = ["author", "date", "message", "parents"]
567 pre_load = ["author", "date", "message", "parents"]
570
568
571 is_svn = h.is_svn(self.rhodecode_vcs_repo)
569 is_svn = h.is_svn(self.rhodecode_vcs_repo)
572 is_hg = h.is_hg(self.rhodecode_vcs_repo)
570 is_hg = h.is_hg(self.rhodecode_vcs_repo)
573
571
574 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
572 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
575
573
576 closed_refs = {}
574 closed_refs = {}
577 if is_hg:
575 if is_hg:
578 closed_refs = self.rhodecode_vcs_repo.branches_closed
576 closed_refs = self.rhodecode_vcs_repo.branches_closed
579
577
580 data = []
578 data = []
581 for ref_name, commit_id in ref_items:
579 for ref_name, commit_id in ref_items:
582 commit = self.rhodecode_vcs_repo.get_commit(
580 commit = self.rhodecode_vcs_repo.get_commit(
583 commit_id=commit_id, pre_load=pre_load)
581 commit_id=commit_id, pre_load=pre_load)
584 closed = ref_name in closed_refs
582 closed = ref_name in closed_refs
585
583
586 # TODO: johbo: Unify generation of reference links
584 # TODO: johbo: Unify generation of reference links
587 use_commit_id = '/' in ref_name or is_svn
585 use_commit_id = '/' in ref_name or is_svn
588
586
589 if use_commit_id:
587 if use_commit_id:
590 files_url = h.route_path(
588 files_url = h.route_path(
591 'repo_files',
589 'repo_files',
592 repo_name=self.db_repo_name,
590 repo_name=self.db_repo_name,
593 f_path=ref_name if is_svn else '',
591 f_path=ref_name if is_svn else '',
594 commit_id=commit_id,
592 commit_id=commit_id,
595 _query=dict(at=ref_name)
593 _query=dict(at=ref_name)
596 )
594 )
597
595
598 else:
596 else:
599 files_url = h.route_path(
597 files_url = h.route_path(
600 'repo_files',
598 'repo_files',
601 repo_name=self.db_repo_name,
599 repo_name=self.db_repo_name,
602 f_path=ref_name if is_svn else '',
600 f_path=ref_name if is_svn else '',
603 commit_id=ref_name,
601 commit_id=ref_name,
604 _query=dict(at=ref_name)
602 _query=dict(at=ref_name)
605 )
603 )
606
604
607 data.append({
605 data.append({
608 "name": _render('name', ref_name, files_url, closed),
606 "name": _render('name', ref_name, files_url, closed),
609 "name_raw": ref_name,
607 "name_raw": ref_name,
610 "date": _render('date', commit.date),
608 "date": _render('date', commit.date),
611 "date_raw": datetime_to_time(commit.date),
609 "date_raw": datetime_to_time(commit.date),
612 "author": _render('author', commit.author),
610 "author": _render('author', commit.author),
613 "commit": _render(
611 "commit": _render(
614 'commit', commit.message, commit.raw_id, commit.idx),
612 'commit', commit.message, commit.raw_id, commit.idx),
615 "commit_raw": commit.idx,
613 "commit_raw": commit.idx,
616 "compare": _render(
614 "compare": _render(
617 'compare', format_ref_id(ref_name, commit.raw_id)),
615 'compare', format_ref_id(ref_name, commit.raw_id)),
618 })
616 })
619
617
620 return data
618 return data
621
619
622
620
623 class RepoRoutePredicate(object):
621 class RepoRoutePredicate(object):
624 def __init__(self, val, config):
622 def __init__(self, val, config):
625 self.val = val
623 self.val = val
626
624
627 def text(self):
625 def text(self):
628 return f'repo_route = {self.val}'
626 return f'repo_route = {self.val}'
629
627
630 phash = text
628 phash = text
631
629
632 def __call__(self, info, request):
630 def __call__(self, info, request):
633 if hasattr(request, 'vcs_call'):
631 if hasattr(request, 'vcs_call'):
634 # skip vcs calls
632 # skip vcs calls
635 return
633 return
636
634
637 repo_name = info['match']['repo_name']
635 repo_name = info['match']['repo_name']
638
636
639 repo_name_parts = repo_name.split('/')
637 repo_name_parts = repo_name.split('/')
640 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
638 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
641
639
642 if repo_name_parts != repo_slugs:
640 if repo_name_parts != repo_slugs:
643 # short-skip if the repo-name doesn't follow slug rule
641 # short-skip if the repo-name doesn't follow slug rule
644 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
642 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
645 return False
643 return False
646
644
647 repo_model = repo.RepoModel()
645 repo_model = repo.RepoModel()
648
646
649 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
647 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
650
648
651 def redirect_if_creating(route_info, db_repo):
649 def redirect_if_creating(route_info, db_repo):
652 skip_views = ['edit_repo_advanced_delete']
650 skip_views = ['edit_repo_advanced_delete']
653 route = route_info['route']
651 route = route_info['route']
654 # we should skip delete view so we can actually "remove" repositories
652 # we should skip delete view so we can actually "remove" repositories
655 # if they get stuck in creating state.
653 # if they get stuck in creating state.
656 if route.name in skip_views:
654 if route.name in skip_views:
657 return
655 return
658
656
659 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
657 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
660 repo_creating_url = request.route_path(
658 repo_creating_url = request.route_path(
661 'repo_creating', repo_name=db_repo.repo_name)
659 'repo_creating', repo_name=db_repo.repo_name)
662 raise HTTPFound(repo_creating_url)
660 raise HTTPFound(repo_creating_url)
663
661
664 if by_name_match:
662 if by_name_match:
665 # register this as request object we can re-use later
663 # register this as request object we can re-use later
666 request.db_repo = by_name_match
664 request.db_repo = by_name_match
667 request.db_repo_name = request.db_repo.repo_name
665 request.db_repo_name = request.db_repo.repo_name
668
666
669 redirect_if_creating(info, by_name_match)
667 redirect_if_creating(info, by_name_match)
670 return True
668 return True
671
669
672 by_id_match = repo_model.get_repo_by_id(repo_name)
670 by_id_match = repo_model.get_repo_by_id(repo_name)
673 if by_id_match:
671 if by_id_match:
674 request.db_repo = by_id_match
672 request.db_repo = by_id_match
675 request.db_repo_name = request.db_repo.repo_name
673 request.db_repo_name = request.db_repo.repo_name
676 redirect_if_creating(info, by_id_match)
674 redirect_if_creating(info, by_id_match)
677 return True
675 return True
678
676
679 return False
677 return False
680
678
681
679
682 class RepoForbidArchivedRoutePredicate(object):
680 class RepoForbidArchivedRoutePredicate(object):
683 def __init__(self, val, config):
681 def __init__(self, val, config):
684 self.val = val
682 self.val = val
685
683
686 def text(self):
684 def text(self):
687 return f'repo_forbid_archived = {self.val}'
685 return f'repo_forbid_archived = {self.val}'
688
686
689 phash = text
687 phash = text
690
688
691 def __call__(self, info, request):
689 def __call__(self, info, request):
692 _ = request.translate
690 _ = request.translate
693 rhodecode_db_repo = request.db_repo
691 rhodecode_db_repo = request.db_repo
694
692
695 log.debug(
693 log.debug(
696 '%s checking if archived flag for repo for %s',
694 '%s checking if archived flag for repo for %s',
697 self.__class__.__name__, rhodecode_db_repo.repo_name)
695 self.__class__.__name__, rhodecode_db_repo.repo_name)
698
696
699 if rhodecode_db_repo.archived:
697 if rhodecode_db_repo.archived:
700 log.warning('Current view is not supported for archived repo:%s',
698 log.warning('Current view is not supported for archived repo:%s',
701 rhodecode_db_repo.repo_name)
699 rhodecode_db_repo.repo_name)
702
700
703 h.flash(
701 h.flash(
704 h.literal(_('Action not supported for archived repository.')),
702 h.literal(_('Action not supported for archived repository.')),
705 category='warning')
703 category='warning')
706 summary_url = request.route_path(
704 summary_url = request.route_path(
707 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
705 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
708 raise HTTPFound(summary_url)
706 raise HTTPFound(summary_url)
709 return True
707 return True
710
708
711
709
712 class RepoTypeRoutePredicate(object):
710 class RepoTypeRoutePredicate(object):
713 def __init__(self, val, config):
711 def __init__(self, val, config):
714 self.val = val or ['hg', 'git', 'svn']
712 self.val = val or ['hg', 'git', 'svn']
715
713
716 def text(self):
714 def text(self):
717 return f'repo_accepted_type = {self.val}'
715 return f'repo_accepted_type = {self.val}'
718
716
719 phash = text
717 phash = text
720
718
721 def __call__(self, info, request):
719 def __call__(self, info, request):
722 if hasattr(request, 'vcs_call'):
720 if hasattr(request, 'vcs_call'):
723 # skip vcs calls
721 # skip vcs calls
724 return
722 return
725
723
726 rhodecode_db_repo = request.db_repo
724 rhodecode_db_repo = request.db_repo
727
725
728 log.debug(
726 log.debug(
729 '%s checking repo type for %s in %s',
727 '%s checking repo type for %s in %s',
730 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
728 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
731
729
732 if rhodecode_db_repo.repo_type in self.val:
730 if rhodecode_db_repo.repo_type in self.val:
733 return True
731 return True
734 else:
732 else:
735 log.warning('Current view is not supported for repo type:%s',
733 log.warning('Current view is not supported for repo type:%s',
736 rhodecode_db_repo.repo_type)
734 rhodecode_db_repo.repo_type)
737 return False
735 return False
738
736
739
737
740 class RepoGroupRoutePredicate(object):
738 class RepoGroupRoutePredicate(object):
741 def __init__(self, val, config):
739 def __init__(self, val, config):
742 self.val = val
740 self.val = val
743
741
744 def text(self):
742 def text(self):
745 return f'repo_group_route = {self.val}'
743 return f'repo_group_route = {self.val}'
746
744
747 phash = text
745 phash = text
748
746
749 def __call__(self, info, request):
747 def __call__(self, info, request):
750 if hasattr(request, 'vcs_call'):
748 if hasattr(request, 'vcs_call'):
751 # skip vcs calls
749 # skip vcs calls
752 return
750 return
753
751
754 repo_group_name = info['match']['repo_group_name']
752 repo_group_name = info['match']['repo_group_name']
755
753
756 repo_group_name_parts = repo_group_name.split('/')
754 repo_group_name_parts = repo_group_name.split('/')
757 repo_group_slugs = [x for x in [repo_name_slug(x) for x in repo_group_name_parts]]
755 repo_group_slugs = [x for x in [repo_name_slug(x) for x in repo_group_name_parts]]
758 if repo_group_name_parts != repo_group_slugs:
756 if repo_group_name_parts != repo_group_slugs:
759 # short-skip if the repo-name doesn't follow slug rule
757 # short-skip if the repo-name doesn't follow slug rule
760 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
758 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
761 return False
759 return False
762
760
763 repo_group_model = repo_group.RepoGroupModel()
761 repo_group_model = repo_group.RepoGroupModel()
764 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
762 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
765
763
766 if by_name_match:
764 if by_name_match:
767 # register this as request object we can re-use later
765 # register this as request object we can re-use later
768 request.db_repo_group = by_name_match
766 request.db_repo_group = by_name_match
769 request.db_repo_group_name = request.db_repo_group.group_name
767 request.db_repo_group_name = request.db_repo_group.group_name
770 return True
768 return True
771
769
772 return False
770 return False
773
771
774
772
775 class UserGroupRoutePredicate(object):
773 class UserGroupRoutePredicate(object):
776 def __init__(self, val, config):
774 def __init__(self, val, config):
777 self.val = val
775 self.val = val
778
776
779 def text(self):
777 def text(self):
780 return f'user_group_route = {self.val}'
778 return f'user_group_route = {self.val}'
781
779
782 phash = text
780 phash = text
783
781
784 def __call__(self, info, request):
782 def __call__(self, info, request):
785 if hasattr(request, 'vcs_call'):
783 if hasattr(request, 'vcs_call'):
786 # skip vcs calls
784 # skip vcs calls
787 return
785 return
788
786
789 user_group_id = info['match']['user_group_id']
787 user_group_id = info['match']['user_group_id']
790 user_group_model = user_group.UserGroup()
788 user_group_model = user_group.UserGroup()
791 by_id_match = user_group_model.get(user_group_id, cache=False)
789 by_id_match = user_group_model.get(user_group_id, cache=False)
792
790
793 if by_id_match:
791 if by_id_match:
794 # register this as request object we can re-use later
792 # register this as request object we can re-use later
795 request.db_user_group = by_id_match
793 request.db_user_group = by_id_match
796 return True
794 return True
797
795
798 return False
796 return False
799
797
800
798
801 class UserRoutePredicateBase(object):
799 class UserRoutePredicateBase(object):
802 supports_default = None
800 supports_default = None
803
801
804 def __init__(self, val, config):
802 def __init__(self, val, config):
805 self.val = val
803 self.val = val
806
804
807 def text(self):
805 def text(self):
808 raise NotImplementedError()
806 raise NotImplementedError()
809
807
810 def __call__(self, info, request):
808 def __call__(self, info, request):
811 if hasattr(request, 'vcs_call'):
809 if hasattr(request, 'vcs_call'):
812 # skip vcs calls
810 # skip vcs calls
813 return
811 return
814
812
815 user_id = info['match']['user_id']
813 user_id = info['match']['user_id']
816 user_model = user.User()
814 user_model = user.User()
817 by_id_match = user_model.get(user_id, cache=False)
815 by_id_match = user_model.get(user_id, cache=False)
818
816
819 if by_id_match:
817 if by_id_match:
820 # register this as request object we can re-use later
818 # register this as request object we can re-use later
821 request.db_user = by_id_match
819 request.db_user = by_id_match
822 request.db_user_supports_default = self.supports_default
820 request.db_user_supports_default = self.supports_default
823 return True
821 return True
824
822
825 return False
823 return False
826
824
827
825
828 class UserRoutePredicate(UserRoutePredicateBase):
826 class UserRoutePredicate(UserRoutePredicateBase):
829 supports_default = False
827 supports_default = False
830
828
831 def text(self):
829 def text(self):
832 return f'user_route = {self.val}'
830 return f'user_route = {self.val}'
833
831
834 phash = text
832 phash = text
835
833
836
834
837 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
835 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
838 supports_default = True
836 supports_default = True
839
837
840 def text(self):
838 def text(self):
841 return f'user_with_default_route = {self.val}'
839 return f'user_with_default_route = {self.val}'
842
840
843 phash = text
841 phash = text
844
842
845
843
846 def includeme(config):
844 def includeme(config):
847 config.add_route_predicate(
845 config.add_route_predicate(
848 'repo_route', RepoRoutePredicate)
846 'repo_route', RepoRoutePredicate)
849 config.add_route_predicate(
847 config.add_route_predicate(
850 'repo_accepted_types', RepoTypeRoutePredicate)
848 'repo_accepted_types', RepoTypeRoutePredicate)
851 config.add_route_predicate(
849 config.add_route_predicate(
852 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
850 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
853 config.add_route_predicate(
851 config.add_route_predicate(
854 'repo_group_route', RepoGroupRoutePredicate)
852 'repo_group_route', RepoGroupRoutePredicate)
855 config.add_route_predicate(
853 config.add_route_predicate(
856 'user_group_route', UserGroupRoutePredicate)
854 'user_group_route', UserGroupRoutePredicate)
857 config.add_route_predicate(
855 config.add_route_predicate(
858 'user_route_with_default', UserRouteWithDefaultPredicate)
856 'user_route_with_default', UserRouteWithDefaultPredicate)
859 config.add_route_predicate(
857 config.add_route_predicate(
860 'user_route', UserRoutePredicate)
858 'user_route', UserRoutePredicate)
@@ -1,29 +1,27 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 from zope.interface import Interface
19 from zope.interface import Interface
22
20
23
21
24 class IAdminNavigationRegistry(Interface):
22 class IAdminNavigationRegistry(Interface):
25 """
23 """
26 Interface for the admin navigation registry. Currently this is only
24 Interface for the admin navigation registry. Currently this is only
27 used to register and retrieve it via pyramids registry.
25 used to register and retrieve it via pyramids registry.
28 """
26 """
29 pass
27 pass
@@ -1,55 +1,53 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from rhodecode import events
21 from rhodecode import events
24 from rhodecode.lib import rc_cache
22 from rhodecode.lib import rc_cache
25
23
26 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
27
25
28 # names of namespaces used for different permission related cached
26 # names of namespaces used for different permission related cached
29 # during flush operation we need to take care of all those
27 # during flush operation we need to take care of all those
30 cache_namespaces = [
28 cache_namespaces = [
31 'cache_user_auth.{}',
29 'cache_user_auth.{}',
32 'cache_user_repo_acl_ids.{}',
30 'cache_user_repo_acl_ids.{}',
33 'cache_user_user_group_acl_ids.{}',
31 'cache_user_user_group_acl_ids.{}',
34 'cache_user_repo_group_acl_ids.{}'
32 'cache_user_repo_group_acl_ids.{}'
35 ]
33 ]
36
34
37
35
38 def trigger_user_permission_flush(event):
36 def trigger_user_permission_flush(event):
39 """
37 """
40 Subscriber to the `UserPermissionsChange`. This triggers the
38 Subscriber to the `UserPermissionsChange`. This triggers the
41 automatic flush of permission caches, so the users affected receive new permissions
39 automatic flush of permission caches, so the users affected receive new permissions
42 Right Away
40 Right Away
43 """
41 """
44
42
45 affected_user_ids = set(event.user_ids)
43 affected_user_ids = set(event.user_ids)
46 for user_id in affected_user_ids:
44 for user_id in affected_user_ids:
47 for cache_namespace_uid_tmpl in cache_namespaces:
45 for cache_namespace_uid_tmpl in cache_namespaces:
48 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
46 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
49 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
47 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
50 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
48 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
51 del_keys, user_id, cache_namespace_uid)
49 del_keys, user_id, cache_namespace_uid)
52
50
53
51
54 def includeme(config):
52 def includeme(config):
55 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
53 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,40 +1,38 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
21 from rhodecode.apps._base import BaseAppView, DataGridAppView
24 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
22 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
25
23
26 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
27
25
28
26
29 class AdminArtifactsView(BaseAppView, DataGridAppView):
27 class AdminArtifactsView(BaseAppView, DataGridAppView):
30
28
31 def load_default_context(self):
29 def load_default_context(self):
32 c = self._get_local_tmpl_context()
30 c = self._get_local_tmpl_context()
33 return c
31 return c
34
32
35 @LoginRequired()
33 @LoginRequired()
36 @HasPermissionAllDecorator('hg.admin')
34 @HasPermissionAllDecorator('hg.admin')
37 def artifacts(self):
35 def artifacts(self):
38 c = self.load_default_context()
36 c = self.load_default_context()
39 c.active = 'artifacts'
37 c.active = 'artifacts'
40 return self._get_template_context(c)
38 return self._get_template_context(c)
@@ -1,87 +1,85 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPNotFound
21 from pyramid.httpexceptions import HTTPNotFound
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.model.db import joinedload, UserLog
24 from rhodecode.model.db import joinedload, UserLog
27 from rhodecode.lib.user_log_filter import user_log_filter
25 from rhodecode.lib.user_log_filter import user_log_filter
28 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
26 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
29 from rhodecode.lib.utils2 import safe_int
27 from rhodecode.lib.utils2 import safe_int
30 from rhodecode.lib.helpers import SqlPage
28 from rhodecode.lib.helpers import SqlPage
31
29
32 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
33
31
34
32
35 class AdminAuditLogsView(BaseAppView):
33 class AdminAuditLogsView(BaseAppView):
36
34
37 def load_default_context(self):
35 def load_default_context(self):
38 c = self._get_local_tmpl_context()
36 c = self._get_local_tmpl_context()
39 return c
37 return c
40
38
41 @LoginRequired()
39 @LoginRequired()
42 @HasPermissionAllDecorator('hg.admin')
40 @HasPermissionAllDecorator('hg.admin')
43 def admin_audit_logs(self):
41 def admin_audit_logs(self):
44 c = self.load_default_context()
42 c = self.load_default_context()
45
43
46 users_log = UserLog.query()\
44 users_log = UserLog.query()\
47 .options(joinedload(UserLog.user))\
45 .options(joinedload(UserLog.user))\
48 .options(joinedload(UserLog.repository))
46 .options(joinedload(UserLog.repository))
49
47
50 # FILTERING
48 # FILTERING
51 c.search_term = self.request.GET.get('filter')
49 c.search_term = self.request.GET.get('filter')
52 try:
50 try:
53 users_log = user_log_filter(users_log, c.search_term)
51 users_log = user_log_filter(users_log, c.search_term)
54 except Exception:
52 except Exception:
55 # we want this to crash for now
53 # we want this to crash for now
56 raise
54 raise
57
55
58 users_log = users_log.order_by(UserLog.action_date.desc())
56 users_log = users_log.order_by(UserLog.action_date.desc())
59
57
60 p = safe_int(self.request.GET.get('page', 1), 1)
58 p = safe_int(self.request.GET.get('page', 1), 1)
61
59
62 def url_generator(page_num):
60 def url_generator(page_num):
63 query_params = {
61 query_params = {
64 'page': page_num
62 'page': page_num
65 }
63 }
66 if c.search_term:
64 if c.search_term:
67 query_params['filter'] = c.search_term
65 query_params['filter'] = c.search_term
68 return self.request.current_route_path(_query=query_params)
66 return self.request.current_route_path(_query=query_params)
69
67
70 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
68 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
71 url_maker=url_generator)
69 url_maker=url_generator)
72 return self._get_template_context(c)
70 return self._get_template_context(c)
73
71
74 @LoginRequired()
72 @LoginRequired()
75 @HasPermissionAllDecorator('hg.admin')
73 @HasPermissionAllDecorator('hg.admin')
76 def admin_audit_log_entry(self):
74 def admin_audit_log_entry(self):
77 c = self.load_default_context()
75 c = self.load_default_context()
78 audit_log_id = self.request.matchdict['audit_log_id']
76 audit_log_id = self.request.matchdict['audit_log_id']
79
77
80 c.audit_log_entry = UserLog.query()\
78 c.audit_log_entry = UserLog.query()\
81 .options(joinedload(UserLog.user))\
79 .options(joinedload(UserLog.user))\
82 .options(joinedload(UserLog.repository))\
80 .options(joinedload(UserLog.repository))\
83 .filter(UserLog.user_log_id == audit_log_id).scalar()
81 .filter(UserLog.user_log_id == audit_log_id).scalar()
84 if not c.audit_log_entry:
82 if not c.audit_log_entry:
85 raise HTTPNotFound()
83 raise HTTPNotFound()
86
84
87 return self._get_template_context(c)
85 return self._get_template_context(c)
@@ -1,103 +1,101 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 import formencode
21 import formencode
24 import formencode.htmlfill
22 import formencode.htmlfill
25
23
26 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
27 from pyramid.renderers import render
25 from pyramid.renderers import render
28 from pyramid.response import Response
26 from pyramid.response import Response
29
27
30 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps._base import BaseAppView
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
34 from rhodecode.model.forms import DefaultsForm
32 from rhodecode.model.forms import DefaultsForm
35 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
36 from rhodecode import BACKENDS
34 from rhodecode import BACKENDS
37 from rhodecode.model.settings import SettingsModel
35 from rhodecode.model.settings import SettingsModel
38
36
39 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
40
38
41
39
42 class AdminDefaultSettingsView(BaseAppView):
40 class AdminDefaultSettingsView(BaseAppView):
43
41
44 def load_default_context(self):
42 def load_default_context(self):
45 c = self._get_local_tmpl_context()
43 c = self._get_local_tmpl_context()
46 return c
44 return c
47
45
48 @LoginRequired()
46 @LoginRequired()
49 @HasPermissionAllDecorator('hg.admin')
47 @HasPermissionAllDecorator('hg.admin')
50 def defaults_repository_show(self):
48 def defaults_repository_show(self):
51 c = self.load_default_context()
49 c = self.load_default_context()
52 c.backends = BACKENDS.keys()
50 c.backends = BACKENDS.keys()
53 c.active = 'repositories'
51 c.active = 'repositories'
54 defaults = SettingsModel().get_default_repo_settings()
52 defaults = SettingsModel().get_default_repo_settings()
55
53
56 data = render(
54 data = render(
57 'rhodecode:templates/admin/defaults/defaults.mako',
55 'rhodecode:templates/admin/defaults/defaults.mako',
58 self._get_template_context(c), self.request)
56 self._get_template_context(c), self.request)
59 html = formencode.htmlfill.render(
57 html = formencode.htmlfill.render(
60 data,
58 data,
61 defaults=defaults,
59 defaults=defaults,
62 encoding="UTF-8",
60 encoding="UTF-8",
63 force_defaults=False
61 force_defaults=False
64 )
62 )
65 return Response(html)
63 return Response(html)
66
64
67 @LoginRequired()
65 @LoginRequired()
68 @HasPermissionAllDecorator('hg.admin')
66 @HasPermissionAllDecorator('hg.admin')
69 @CSRFRequired()
67 @CSRFRequired()
70 def defaults_repository_update(self):
68 def defaults_repository_update(self):
71 _ = self.request.translate
69 _ = self.request.translate
72 c = self.load_default_context()
70 c = self.load_default_context()
73 c.active = 'repositories'
71 c.active = 'repositories'
74 form = DefaultsForm(self.request.translate)()
72 form = DefaultsForm(self.request.translate)()
75
73
76 try:
74 try:
77 form_result = form.to_python(dict(self.request.POST))
75 form_result = form.to_python(dict(self.request.POST))
78 for k, v in form_result.items():
76 for k, v in form_result.items():
79 setting = SettingsModel().create_or_update_setting(k, v)
77 setting = SettingsModel().create_or_update_setting(k, v)
80 Session().add(setting)
78 Session().add(setting)
81 Session().commit()
79 Session().commit()
82 h.flash(_('Default settings updated successfully'),
80 h.flash(_('Default settings updated successfully'),
83 category='success')
81 category='success')
84
82
85 except formencode.Invalid as errors:
83 except formencode.Invalid as errors:
86 data = render(
84 data = render(
87 'rhodecode:templates/admin/defaults/defaults.mako',
85 'rhodecode:templates/admin/defaults/defaults.mako',
88 self._get_template_context(c), self.request)
86 self._get_template_context(c), self.request)
89 html = formencode.htmlfill.render(
87 html = formencode.htmlfill.render(
90 data,
88 data,
91 defaults=errors.value,
89 defaults=errors.value,
92 errors=errors.unpack_errors() or {},
90 errors=errors.unpack_errors() or {},
93 prefix_error=False,
91 prefix_error=False,
94 encoding="UTF-8",
92 encoding="UTF-8",
95 force_defaults=False
93 force_defaults=False
96 )
94 )
97 return Response(html)
95 return Response(html)
98 except Exception:
96 except Exception:
99 log.exception('Exception in update action')
97 log.exception('Exception in update action')
100 h.flash(_('Error occurred during update of default values'),
98 h.flash(_('Error occurred during update of default values'),
101 category='error')
99 category='error')
102
100
103 raise HTTPFound(h.route_path('admin_defaults_repositories'))
101 raise HTTPFound(h.route_path('admin_defaults_repositories'))
@@ -1,161 +1,159 b''
1
2
3 # Copyright (C) 2018-2023 RhodeCode GmbH
1 # Copyright (C) 2018-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
18 import os
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound
21 from pyramid.httpexceptions import HTTPFound
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base.navigation import navigation_list
24 from rhodecode.apps._base.navigation import navigation_list
27 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import (
26 from rhodecode.lib.auth import (
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
27 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
28 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
31 from rhodecode.lib import exc_tracking
29 from rhodecode.lib import exc_tracking
32
30
33 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
34
32
35
33
36 class ExceptionsTrackerView(BaseAppView):
34 class ExceptionsTrackerView(BaseAppView):
37 def load_default_context(self):
35 def load_default_context(self):
38 c = self._get_local_tmpl_context()
36 c = self._get_local_tmpl_context()
39 c.navlist = navigation_list(self.request)
37 c.navlist = navigation_list(self.request)
40 return c
38 return c
41
39
42 def count_all_exceptions(self):
40 def count_all_exceptions(self):
43 exc_store_path = exc_tracking.get_exc_store()
41 exc_store_path = exc_tracking.get_exc_store()
44 count = 0
42 count = 0
45 for fname in os.listdir(exc_store_path):
43 for fname in os.listdir(exc_store_path):
46 parts = fname.split('_', 2)
44 parts = fname.split('_', 2)
47 if not len(parts) == 3:
45 if not len(parts) == 3:
48 continue
46 continue
49 count +=1
47 count +=1
50 return count
48 return count
51
49
52 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
50 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
53 exc_store_path = exc_tracking.get_exc_store()
51 exc_store_path = exc_tracking.get_exc_store()
54 exception_list = []
52 exception_list = []
55
53
56 def key_sorter(val):
54 def key_sorter(val):
57 try:
55 try:
58 return val.split('_')[-1]
56 return val.split('_')[-1]
59 except Exception:
57 except Exception:
60 return 0
58 return 0
61
59
62 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
60 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
63
61
64 parts = fname.split('_', 2)
62 parts = fname.split('_', 2)
65 if not len(parts) == 3:
63 if not len(parts) == 3:
66 continue
64 continue
67
65
68 exc_id, app_type, exc_timestamp = parts
66 exc_id, app_type, exc_timestamp = parts
69
67
70 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
68 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
71 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
69 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
72
70
73 if read_metadata:
71 if read_metadata:
74 full_path = os.path.join(exc_store_path, fname)
72 full_path = os.path.join(exc_store_path, fname)
75 if not os.path.isfile(full_path):
73 if not os.path.isfile(full_path):
76 continue
74 continue
77 try:
75 try:
78 # we can read our metadata
76 # we can read our metadata
79 with open(full_path, 'rb') as f:
77 with open(full_path, 'rb') as f:
80 exc_metadata = exc_tracking.exc_unserialize(f.read())
78 exc_metadata = exc_tracking.exc_unserialize(f.read())
81 exc.update(exc_metadata)
79 exc.update(exc_metadata)
82 except Exception:
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 pass
82 pass
85 # convert our timestamp to a date obj, for nicer representation
83 # convert our timestamp to a date obj, for nicer representation
86 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
84 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
87
85
88 type_present = exc.get('exc_type')
86 type_present = exc.get('exc_type')
89 if type_filter:
87 if type_filter:
90 if type_present and type_present == type_filter:
88 if type_present and type_present == type_filter:
91 exception_list.append(exc)
89 exception_list.append(exc)
92 else:
90 else:
93 exception_list.append(exc)
91 exception_list.append(exc)
94
92
95 if limit and len(exception_list) >= limit:
93 if limit and len(exception_list) >= limit:
96 break
94 break
97 return exception_list
95 return exception_list
98
96
99 @LoginRequired()
97 @LoginRequired()
100 @HasPermissionAllDecorator('hg.admin')
98 @HasPermissionAllDecorator('hg.admin')
101 def browse_exceptions(self):
99 def browse_exceptions(self):
102 _ = self.request.translate
100 _ = self.request.translate
103 c = self.load_default_context()
101 c = self.load_default_context()
104 c.active = 'exceptions_browse'
102 c.active = 'exceptions_browse'
105 c.limit = safe_int(self.request.GET.get('limit')) or 50
103 c.limit = safe_int(self.request.GET.get('limit')) or 50
106 c.type_filter = self.request.GET.get('type_filter')
104 c.type_filter = self.request.GET.get('type_filter')
107 c.next_limit = c.limit + 50
105 c.next_limit = c.limit + 50
108 c.exception_list = self.get_all_exceptions(
106 c.exception_list = self.get_all_exceptions(
109 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
107 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
110 c.exception_list_count = self.count_all_exceptions()
108 c.exception_list_count = self.count_all_exceptions()
111 c.exception_store_dir = exc_tracking.get_exc_store()
109 c.exception_store_dir = exc_tracking.get_exc_store()
112 return self._get_template_context(c)
110 return self._get_template_context(c)
113
111
114 @LoginRequired()
112 @LoginRequired()
115 @HasPermissionAllDecorator('hg.admin')
113 @HasPermissionAllDecorator('hg.admin')
116 def exception_show(self):
114 def exception_show(self):
117 _ = self.request.translate
115 _ = self.request.translate
118 c = self.load_default_context()
116 c = self.load_default_context()
119
117
120 c.active = 'exceptions'
118 c.active = 'exceptions'
121 c.exception_id = self.request.matchdict['exception_id']
119 c.exception_id = self.request.matchdict['exception_id']
122 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
120 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
123 return self._get_template_context(c)
121 return self._get_template_context(c)
124
122
125 @LoginRequired()
123 @LoginRequired()
126 @HasPermissionAllDecorator('hg.admin')
124 @HasPermissionAllDecorator('hg.admin')
127 @CSRFRequired()
125 @CSRFRequired()
128 def exception_delete_all(self):
126 def exception_delete_all(self):
129 _ = self.request.translate
127 _ = self.request.translate
130 c = self.load_default_context()
128 c = self.load_default_context()
131 type_filter = self.request.POST.get('type_filter')
129 type_filter = self.request.POST.get('type_filter')
132
130
133 c.active = 'exceptions'
131 c.active = 'exceptions'
134 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
132 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
135 exc_count = 0
133 exc_count = 0
136
134
137 for exc in all_exc:
135 for exc in all_exc:
138 if type_filter:
136 if type_filter:
139 if exc.get('exc_type') == type_filter:
137 if exc.get('exc_type') == type_filter:
140 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
138 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
141 exc_count += 1
139 exc_count += 1
142 else:
140 else:
143 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
141 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
144 exc_count += 1
142 exc_count += 1
145
143
146 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
144 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
147 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
145 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
148
146
149 @LoginRequired()
147 @LoginRequired()
150 @HasPermissionAllDecorator('hg.admin')
148 @HasPermissionAllDecorator('hg.admin')
151 @CSRFRequired()
149 @CSRFRequired()
152 def exception_delete(self):
150 def exception_delete(self):
153 _ = self.request.translate
151 _ = self.request.translate
154 c = self.load_default_context()
152 c = self.load_default_context()
155
153
156 c.active = 'exceptions'
154 c.active = 'exceptions'
157 c.exception_id = self.request.matchdict['exception_id']
155 c.exception_id = self.request.matchdict['exception_id']
158 exc_tracking.delete_exception(c.exception_id, prefix=None)
156 exc_tracking.delete_exception(c.exception_id, prefix=None)
159
157
160 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
158 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
161 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
159 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
@@ -1,72 +1,70 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
25 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
28 from rhodecode.model.db import PullRequest
26 from rhodecode.model.db import PullRequest
29
27
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class AdminMainView(BaseAppView):
32 class AdminMainView(BaseAppView):
35 def load_default_context(self):
33 def load_default_context(self):
36 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
37 return c
35 return c
38
36
39 @LoginRequired()
37 @LoginRequired()
40 @NotAnonymous()
38 @NotAnonymous()
41 def admin_main(self):
39 def admin_main(self):
42 c = self.load_default_context()
40 c = self.load_default_context()
43 c.active = 'admin'
41 c.active = 'admin'
44
42
45 if not (c.is_super_admin or c.is_delegated_admin):
43 if not (c.is_super_admin or c.is_delegated_admin):
46 raise HTTPNotFound()
44 raise HTTPNotFound()
47
45
48 return self._get_template_context(c)
46 return self._get_template_context(c)
49
47
50 @LoginRequired()
48 @LoginRequired()
51 def pull_requests(self):
49 def pull_requests(self):
52 """
50 """
53 Global redirect for Pull Requests
51 Global redirect for Pull Requests
54 pull_request_id: id of pull requests in the system
52 pull_request_id: id of pull requests in the system
55 """
53 """
56
54
57 pull_request = PullRequest.get_or_404(
55 pull_request = PullRequest.get_or_404(
58 self.request.matchdict['pull_request_id'])
56 self.request.matchdict['pull_request_id'])
59 pull_request_id = pull_request.pull_request_id
57 pull_request_id = pull_request.pull_request_id
60
58
61 repo_name = pull_request.target_repo.repo_name
59 repo_name = pull_request.target_repo.repo_name
62 # NOTE(marcink):
60 # NOTE(marcink):
63 # check permissions so we don't redirect to repo that we don't have access to
61 # check permissions so we don't redirect to repo that we don't have access to
64 # exposing it's name
62 # exposing it's name
65 target_repo_perm = HasRepoPermissionAny(
63 target_repo_perm = HasRepoPermissionAny(
66 'repository.read', 'repository.write', 'repository.admin')(repo_name)
64 'repository.read', 'repository.write', 'repository.admin')(repo_name)
67 if not target_repo_perm:
65 if not target_repo_perm:
68 raise HTTPNotFound()
66 raise HTTPNotFound()
69
67
70 raise HTTPFound(
68 raise HTTPFound(
71 h.route_path('pullrequest_show', repo_name=repo_name,
69 h.route_path('pullrequest_show', repo_name=repo_name,
72 pull_request_id=pull_request_id))
70 pull_request_id=pull_request_id))
@@ -1,46 +1,44 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import collections
19 import collections
22 import logging
20 import logging
23
21
24 from rhodecode.apps._base import BaseAppView
22 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base.navigation import navigation_list
23 from rhodecode.apps._base.navigation import navigation_list
26 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
24 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
27 from rhodecode.lib.utils import read_opensource_licenses
25 from rhodecode.lib.utils import read_opensource_licenses
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 class OpenSourceLicensesAdminSettingsView(BaseAppView):
30 class OpenSourceLicensesAdminSettingsView(BaseAppView):
33
31
34 def load_default_context(self):
32 def load_default_context(self):
35 c = self._get_local_tmpl_context()
33 c = self._get_local_tmpl_context()
36 return c
34 return c
37
35
38 @LoginRequired()
36 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
37 @HasPermissionAllDecorator('hg.admin')
40 def open_source_licenses(self):
38 def open_source_licenses(self):
41 c = self.load_default_context()
39 c = self.load_default_context()
42 c.active = 'open_source'
40 c.active = 'open_source'
43 c.navlist = navigation_list(self.request)
41 c.navlist = navigation_list(self.request)
44 c.opensource_licenses = sorted(
42 c.opensource_licenses = sorted(
45 read_opensource_licenses(), key=lambda d: d["name"])
43 read_opensource_licenses(), key=lambda d: d["name"])
46 return self._get_template_context(c)
44 return self._get_template_context(c)
@@ -1,479 +1,477 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import re
19 import re
22 import logging
20 import logging
23 import formencode
21 import formencode
24 import formencode.htmlfill
22 import formencode.htmlfill
25 import datetime
23 import datetime
26 from pyramid.interfaces import IRoutesMapper
24 from pyramid.interfaces import IRoutesMapper
27
25
28 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
29 from pyramid.renderers import render
27 from pyramid.renderers import render
30 from pyramid.response import Response
28 from pyramid.response import Response
31
29
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
31 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode import events
32 from rhodecode import events
35
33
36 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
37 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
36 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib.utils2 import aslist, safe_str
37 from rhodecode.lib.utils2 import aslist, safe_str
40 from rhodecode.model.db import (
38 from rhodecode.model.db import (
41 or_, coalesce, User, UserIpMap, UserSshKeys)
39 or_, coalesce, User, UserIpMap, UserSshKeys)
42 from rhodecode.model.forms import (
40 from rhodecode.model.forms import (
43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
41 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
45 from rhodecode.model.permission import PermissionModel
43 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
47
45
48
46
49 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
50
48
51
49
52 class AdminPermissionsView(BaseAppView, DataGridAppView):
50 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 def load_default_context(self):
51 def load_default_context(self):
54 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
55 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
56 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
57 return c
55 return c
58
56
59 @LoginRequired()
57 @LoginRequired()
60 @HasPermissionAllDecorator('hg.admin')
58 @HasPermissionAllDecorator('hg.admin')
61 def permissions_application(self):
59 def permissions_application(self):
62 c = self.load_default_context()
60 c = self.load_default_context()
63 c.active = 'application'
61 c.active = 'application'
64
62
65 c.user = User.get_default_user(refresh=True)
63 c.user = User.get_default_user(refresh=True)
66
64
67 app_settings = c.rc_config
65 app_settings = c.rc_config
68
66
69 defaults = {
67 defaults = {
70 'anonymous': c.user.active,
68 'anonymous': c.user.active,
71 'default_register_message': app_settings.get(
69 'default_register_message': app_settings.get(
72 'rhodecode_register_message')
70 'rhodecode_register_message')
73 }
71 }
74 defaults.update(c.user.get_default_perms())
72 defaults.update(c.user.get_default_perms())
75
73
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
74 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 self._get_template_context(c), self.request)
75 self._get_template_context(c), self.request)
78 html = formencode.htmlfill.render(
76 html = formencode.htmlfill.render(
79 data,
77 data,
80 defaults=defaults,
78 defaults=defaults,
81 encoding="UTF-8",
79 encoding="UTF-8",
82 force_defaults=False
80 force_defaults=False
83 )
81 )
84 return Response(html)
82 return Response(html)
85
83
86 @LoginRequired()
84 @LoginRequired()
87 @HasPermissionAllDecorator('hg.admin')
85 @HasPermissionAllDecorator('hg.admin')
88 @CSRFRequired()
86 @CSRFRequired()
89 def permissions_application_update(self):
87 def permissions_application_update(self):
90 _ = self.request.translate
88 _ = self.request.translate
91 c = self.load_default_context()
89 c = self.load_default_context()
92 c.active = 'application'
90 c.active = 'application'
93
91
94 _form = ApplicationPermissionsForm(
92 _form = ApplicationPermissionsForm(
95 self.request.translate,
93 self.request.translate,
96 [x[0] for x in c.register_choices],
94 [x[0] for x in c.register_choices],
97 [x[0] for x in c.password_reset_choices],
95 [x[0] for x in c.password_reset_choices],
98 [x[0] for x in c.extern_activate_choices])()
96 [x[0] for x in c.extern_activate_choices])()
99
97
100 try:
98 try:
101 form_result = _form.to_python(dict(self.request.POST))
99 form_result = _form.to_python(dict(self.request.POST))
102 form_result.update({'perm_user_name': User.DEFAULT_USER})
100 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 PermissionModel().update_application_permissions(form_result)
101 PermissionModel().update_application_permissions(form_result)
104
102
105 settings = [
103 settings = [
106 ('register_message', 'default_register_message'),
104 ('register_message', 'default_register_message'),
107 ]
105 ]
108 for setting, form_key in settings:
106 for setting, form_key in settings:
109 sett = SettingsModel().create_or_update_setting(
107 sett = SettingsModel().create_or_update_setting(
110 setting, form_result[form_key])
108 setting, form_result[form_key])
111 Session().add(sett)
109 Session().add(sett)
112
110
113 Session().commit()
111 Session().commit()
114 h.flash(_('Application permissions updated successfully'),
112 h.flash(_('Application permissions updated successfully'),
115 category='success')
113 category='success')
116
114
117 except formencode.Invalid as errors:
115 except formencode.Invalid as errors:
118 defaults = errors.value
116 defaults = errors.value
119
117
120 data = render(
118 data = render(
121 'rhodecode:templates/admin/permissions/permissions.mako',
119 'rhodecode:templates/admin/permissions/permissions.mako',
122 self._get_template_context(c), self.request)
120 self._get_template_context(c), self.request)
123 html = formencode.htmlfill.render(
121 html = formencode.htmlfill.render(
124 data,
122 data,
125 defaults=defaults,
123 defaults=defaults,
126 errors=errors.unpack_errors() or {},
124 errors=errors.unpack_errors() or {},
127 prefix_error=False,
125 prefix_error=False,
128 encoding="UTF-8",
126 encoding="UTF-8",
129 force_defaults=False
127 force_defaults=False
130 )
128 )
131 return Response(html)
129 return Response(html)
132
130
133 except Exception:
131 except Exception:
134 log.exception("Exception during update of permissions")
132 log.exception("Exception during update of permissions")
135 h.flash(_('Error occurred during update of permissions'),
133 h.flash(_('Error occurred during update of permissions'),
136 category='error')
134 category='error')
137
135
138 affected_user_ids = [User.get_default_user_id()]
136 affected_user_ids = [User.get_default_user_id()]
139 PermissionModel().trigger_permission_flush(affected_user_ids)
137 PermissionModel().trigger_permission_flush(affected_user_ids)
140
138
141 raise HTTPFound(h.route_path('admin_permissions_application'))
139 raise HTTPFound(h.route_path('admin_permissions_application'))
142
140
143 @LoginRequired()
141 @LoginRequired()
144 @HasPermissionAllDecorator('hg.admin')
142 @HasPermissionAllDecorator('hg.admin')
145 def permissions_objects(self):
143 def permissions_objects(self):
146 c = self.load_default_context()
144 c = self.load_default_context()
147 c.active = 'objects'
145 c.active = 'objects'
148
146
149 c.user = User.get_default_user(refresh=True)
147 c.user = User.get_default_user(refresh=True)
150 defaults = {}
148 defaults = {}
151 defaults.update(c.user.get_default_perms())
149 defaults.update(c.user.get_default_perms())
152
150
153 data = render(
151 data = render(
154 'rhodecode:templates/admin/permissions/permissions.mako',
152 'rhodecode:templates/admin/permissions/permissions.mako',
155 self._get_template_context(c), self.request)
153 self._get_template_context(c), self.request)
156 html = formencode.htmlfill.render(
154 html = formencode.htmlfill.render(
157 data,
155 data,
158 defaults=defaults,
156 defaults=defaults,
159 encoding="UTF-8",
157 encoding="UTF-8",
160 force_defaults=False
158 force_defaults=False
161 )
159 )
162 return Response(html)
160 return Response(html)
163
161
164 @LoginRequired()
162 @LoginRequired()
165 @HasPermissionAllDecorator('hg.admin')
163 @HasPermissionAllDecorator('hg.admin')
166 @CSRFRequired()
164 @CSRFRequired()
167 def permissions_objects_update(self):
165 def permissions_objects_update(self):
168 _ = self.request.translate
166 _ = self.request.translate
169 c = self.load_default_context()
167 c = self.load_default_context()
170 c.active = 'objects'
168 c.active = 'objects'
171
169
172 _form = ObjectPermissionsForm(
170 _form = ObjectPermissionsForm(
173 self.request.translate,
171 self.request.translate,
174 [x[0] for x in c.repo_perms_choices],
172 [x[0] for x in c.repo_perms_choices],
175 [x[0] for x in c.group_perms_choices],
173 [x[0] for x in c.group_perms_choices],
176 [x[0] for x in c.user_group_perms_choices],
174 [x[0] for x in c.user_group_perms_choices],
177 )()
175 )()
178
176
179 try:
177 try:
180 form_result = _form.to_python(dict(self.request.POST))
178 form_result = _form.to_python(dict(self.request.POST))
181 form_result.update({'perm_user_name': User.DEFAULT_USER})
179 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 PermissionModel().update_object_permissions(form_result)
180 PermissionModel().update_object_permissions(form_result)
183
181
184 Session().commit()
182 Session().commit()
185 h.flash(_('Object permissions updated successfully'),
183 h.flash(_('Object permissions updated successfully'),
186 category='success')
184 category='success')
187
185
188 except formencode.Invalid as errors:
186 except formencode.Invalid as errors:
189 defaults = errors.value
187 defaults = errors.value
190
188
191 data = render(
189 data = render(
192 'rhodecode:templates/admin/permissions/permissions.mako',
190 'rhodecode:templates/admin/permissions/permissions.mako',
193 self._get_template_context(c), self.request)
191 self._get_template_context(c), self.request)
194 html = formencode.htmlfill.render(
192 html = formencode.htmlfill.render(
195 data,
193 data,
196 defaults=defaults,
194 defaults=defaults,
197 errors=errors.unpack_errors() or {},
195 errors=errors.unpack_errors() or {},
198 prefix_error=False,
196 prefix_error=False,
199 encoding="UTF-8",
197 encoding="UTF-8",
200 force_defaults=False
198 force_defaults=False
201 )
199 )
202 return Response(html)
200 return Response(html)
203 except Exception:
201 except Exception:
204 log.exception("Exception during update of permissions")
202 log.exception("Exception during update of permissions")
205 h.flash(_('Error occurred during update of permissions'),
203 h.flash(_('Error occurred during update of permissions'),
206 category='error')
204 category='error')
207
205
208 affected_user_ids = [User.get_default_user_id()]
206 affected_user_ids = [User.get_default_user_id()]
209 PermissionModel().trigger_permission_flush(affected_user_ids)
207 PermissionModel().trigger_permission_flush(affected_user_ids)
210
208
211 raise HTTPFound(h.route_path('admin_permissions_object'))
209 raise HTTPFound(h.route_path('admin_permissions_object'))
212
210
213 @LoginRequired()
211 @LoginRequired()
214 @HasPermissionAllDecorator('hg.admin')
212 @HasPermissionAllDecorator('hg.admin')
215 def permissions_branch(self):
213 def permissions_branch(self):
216 c = self.load_default_context()
214 c = self.load_default_context()
217 c.active = 'branch'
215 c.active = 'branch'
218
216
219 c.user = User.get_default_user(refresh=True)
217 c.user = User.get_default_user(refresh=True)
220 defaults = {}
218 defaults = {}
221 defaults.update(c.user.get_default_perms())
219 defaults.update(c.user.get_default_perms())
222
220
223 data = render(
221 data = render(
224 'rhodecode:templates/admin/permissions/permissions.mako',
222 'rhodecode:templates/admin/permissions/permissions.mako',
225 self._get_template_context(c), self.request)
223 self._get_template_context(c), self.request)
226 html = formencode.htmlfill.render(
224 html = formencode.htmlfill.render(
227 data,
225 data,
228 defaults=defaults,
226 defaults=defaults,
229 encoding="UTF-8",
227 encoding="UTF-8",
230 force_defaults=False
228 force_defaults=False
231 )
229 )
232 return Response(html)
230 return Response(html)
233
231
234 @LoginRequired()
232 @LoginRequired()
235 @HasPermissionAllDecorator('hg.admin')
233 @HasPermissionAllDecorator('hg.admin')
236 def permissions_global(self):
234 def permissions_global(self):
237 c = self.load_default_context()
235 c = self.load_default_context()
238 c.active = 'global'
236 c.active = 'global'
239
237
240 c.user = User.get_default_user(refresh=True)
238 c.user = User.get_default_user(refresh=True)
241 defaults = {}
239 defaults = {}
242 defaults.update(c.user.get_default_perms())
240 defaults.update(c.user.get_default_perms())
243
241
244 data = render(
242 data = render(
245 'rhodecode:templates/admin/permissions/permissions.mako',
243 'rhodecode:templates/admin/permissions/permissions.mako',
246 self._get_template_context(c), self.request)
244 self._get_template_context(c), self.request)
247 html = formencode.htmlfill.render(
245 html = formencode.htmlfill.render(
248 data,
246 data,
249 defaults=defaults,
247 defaults=defaults,
250 encoding="UTF-8",
248 encoding="UTF-8",
251 force_defaults=False
249 force_defaults=False
252 )
250 )
253 return Response(html)
251 return Response(html)
254
252
255 @LoginRequired()
253 @LoginRequired()
256 @HasPermissionAllDecorator('hg.admin')
254 @HasPermissionAllDecorator('hg.admin')
257 @CSRFRequired()
255 @CSRFRequired()
258 def permissions_global_update(self):
256 def permissions_global_update(self):
259 _ = self.request.translate
257 _ = self.request.translate
260 c = self.load_default_context()
258 c = self.load_default_context()
261 c.active = 'global'
259 c.active = 'global'
262
260
263 _form = UserPermissionsForm(
261 _form = UserPermissionsForm(
264 self.request.translate,
262 self.request.translate,
265 [x[0] for x in c.repo_create_choices],
263 [x[0] for x in c.repo_create_choices],
266 [x[0] for x in c.repo_create_on_write_choices],
264 [x[0] for x in c.repo_create_on_write_choices],
267 [x[0] for x in c.repo_group_create_choices],
265 [x[0] for x in c.repo_group_create_choices],
268 [x[0] for x in c.user_group_create_choices],
266 [x[0] for x in c.user_group_create_choices],
269 [x[0] for x in c.fork_choices],
267 [x[0] for x in c.fork_choices],
270 [x[0] for x in c.inherit_default_permission_choices])()
268 [x[0] for x in c.inherit_default_permission_choices])()
271
269
272 try:
270 try:
273 form_result = _form.to_python(dict(self.request.POST))
271 form_result = _form.to_python(dict(self.request.POST))
274 form_result.update({'perm_user_name': User.DEFAULT_USER})
272 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 PermissionModel().update_user_permissions(form_result)
273 PermissionModel().update_user_permissions(form_result)
276
274
277 Session().commit()
275 Session().commit()
278 h.flash(_('Global permissions updated successfully'),
276 h.flash(_('Global permissions updated successfully'),
279 category='success')
277 category='success')
280
278
281 except formencode.Invalid as errors:
279 except formencode.Invalid as errors:
282 defaults = errors.value
280 defaults = errors.value
283
281
284 data = render(
282 data = render(
285 'rhodecode:templates/admin/permissions/permissions.mako',
283 'rhodecode:templates/admin/permissions/permissions.mako',
286 self._get_template_context(c), self.request)
284 self._get_template_context(c), self.request)
287 html = formencode.htmlfill.render(
285 html = formencode.htmlfill.render(
288 data,
286 data,
289 defaults=defaults,
287 defaults=defaults,
290 errors=errors.unpack_errors() or {},
288 errors=errors.unpack_errors() or {},
291 prefix_error=False,
289 prefix_error=False,
292 encoding="UTF-8",
290 encoding="UTF-8",
293 force_defaults=False
291 force_defaults=False
294 )
292 )
295 return Response(html)
293 return Response(html)
296 except Exception:
294 except Exception:
297 log.exception("Exception during update of permissions")
295 log.exception("Exception during update of permissions")
298 h.flash(_('Error occurred during update of permissions'),
296 h.flash(_('Error occurred during update of permissions'),
299 category='error')
297 category='error')
300
298
301 affected_user_ids = [User.get_default_user_id()]
299 affected_user_ids = [User.get_default_user_id()]
302 PermissionModel().trigger_permission_flush(affected_user_ids)
300 PermissionModel().trigger_permission_flush(affected_user_ids)
303
301
304 raise HTTPFound(h.route_path('admin_permissions_global'))
302 raise HTTPFound(h.route_path('admin_permissions_global'))
305
303
306 @LoginRequired()
304 @LoginRequired()
307 @HasPermissionAllDecorator('hg.admin')
305 @HasPermissionAllDecorator('hg.admin')
308 def permissions_ips(self):
306 def permissions_ips(self):
309 c = self.load_default_context()
307 c = self.load_default_context()
310 c.active = 'ips'
308 c.active = 'ips'
311
309
312 c.user = User.get_default_user(refresh=True)
310 c.user = User.get_default_user(refresh=True)
313 c.user_ip_map = (
311 c.user_ip_map = (
314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
312 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315
313
316 return self._get_template_context(c)
314 return self._get_template_context(c)
317
315
318 @LoginRequired()
316 @LoginRequired()
319 @HasPermissionAllDecorator('hg.admin')
317 @HasPermissionAllDecorator('hg.admin')
320 def permissions_overview(self):
318 def permissions_overview(self):
321 c = self.load_default_context()
319 c = self.load_default_context()
322 c.active = 'perms'
320 c.active = 'perms'
323
321
324 c.user = User.get_default_user(refresh=True)
322 c.user = User.get_default_user(refresh=True)
325 c.perm_user = c.user.AuthUser()
323 c.perm_user = c.user.AuthUser()
326 return self._get_template_context(c)
324 return self._get_template_context(c)
327
325
328 @LoginRequired()
326 @LoginRequired()
329 @HasPermissionAllDecorator('hg.admin')
327 @HasPermissionAllDecorator('hg.admin')
330 def auth_token_access(self):
328 def auth_token_access(self):
331 from rhodecode import CONFIG
329 from rhodecode import CONFIG
332
330
333 c = self.load_default_context()
331 c = self.load_default_context()
334 c.active = 'auth_token_access'
332 c.active = 'auth_token_access'
335
333
336 c.user = User.get_default_user(refresh=True)
334 c.user = User.get_default_user(refresh=True)
337 c.perm_user = c.user.AuthUser()
335 c.perm_user = c.user.AuthUser()
338
336
339 mapper = self.request.registry.queryUtility(IRoutesMapper)
337 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 c.view_data = []
338 c.view_data = []
341
339
342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
340 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 introspector = self.request.registry.introspector
341 introspector = self.request.registry.introspector
344
342
345 view_intr = {}
343 view_intr = {}
346 for view_data in introspector.get_category('views'):
344 for view_data in introspector.get_category('views'):
347 intr = view_data['introspectable']
345 intr = view_data['introspectable']
348
346
349 if 'route_name' in intr and intr['attr']:
347 if 'route_name' in intr and intr['attr']:
350 view_intr[intr['route_name']] = '{}:{}'.format(
348 view_intr[intr['route_name']] = '{}:{}'.format(
351 str(intr['derived_callable'].__name__), intr['attr']
349 str(intr['derived_callable'].__name__), intr['attr']
352 )
350 )
353
351
354 c.whitelist_key = 'api_access_controllers_whitelist'
352 c.whitelist_key = 'api_access_controllers_whitelist'
355 c.whitelist_file = CONFIG.get('__file__')
353 c.whitelist_file = CONFIG.get('__file__')
356 whitelist_views = aslist(
354 whitelist_views = aslist(
357 CONFIG.get(c.whitelist_key), sep=',')
355 CONFIG.get(c.whitelist_key), sep=',')
358
356
359 for route_info in mapper.get_routes():
357 for route_info in mapper.get_routes():
360 if not route_info.name.startswith('__'):
358 if not route_info.name.startswith('__'):
361 routepath = route_info.pattern
359 routepath = route_info.pattern
362
360
363 def replace(matchobj):
361 def replace(matchobj):
364 if matchobj.group(1):
362 if matchobj.group(1):
365 return "{%s}" % matchobj.group(1).split(':')[0]
363 return "{%s}" % matchobj.group(1).split(':')[0]
366 else:
364 else:
367 return "{%s}" % matchobj.group(2)
365 return "{%s}" % matchobj.group(2)
368
366
369 routepath = _argument_prog.sub(replace, routepath)
367 routepath = _argument_prog.sub(replace, routepath)
370
368
371 if not routepath.startswith('/'):
369 if not routepath.startswith('/'):
372 routepath = '/' + routepath
370 routepath = '/' + routepath
373
371
374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
372 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 active = view_fqn in whitelist_views
373 active = view_fqn in whitelist_views
376 c.view_data.append((route_info.name, view_fqn, routepath, active))
374 c.view_data.append((route_info.name, view_fqn, routepath, active))
377
375
378 c.whitelist_views = whitelist_views
376 c.whitelist_views = whitelist_views
379 return self._get_template_context(c)
377 return self._get_template_context(c)
380
378
381 def ssh_enabled(self):
379 def ssh_enabled(self):
382 return self.request.registry.settings.get(
380 return self.request.registry.settings.get(
383 'ssh.generate_authorized_keyfile')
381 'ssh.generate_authorized_keyfile')
384
382
385 @LoginRequired()
383 @LoginRequired()
386 @HasPermissionAllDecorator('hg.admin')
384 @HasPermissionAllDecorator('hg.admin')
387 def ssh_keys(self):
385 def ssh_keys(self):
388 c = self.load_default_context()
386 c = self.load_default_context()
389 c.active = 'ssh_keys'
387 c.active = 'ssh_keys'
390 c.ssh_enabled = self.ssh_enabled()
388 c.ssh_enabled = self.ssh_enabled()
391 return self._get_template_context(c)
389 return self._get_template_context(c)
392
390
393 @LoginRequired()
391 @LoginRequired()
394 @HasPermissionAllDecorator('hg.admin')
392 @HasPermissionAllDecorator('hg.admin')
395 def ssh_keys_data(self):
393 def ssh_keys_data(self):
396 _ = self.request.translate
394 _ = self.request.translate
397 self.load_default_context()
395 self.load_default_context()
398 column_map = {
396 column_map = {
399 'fingerprint': 'ssh_key_fingerprint',
397 'fingerprint': 'ssh_key_fingerprint',
400 'username': User.username
398 'username': User.username
401 }
399 }
402 draw, start, limit = self._extract_chunk(self.request)
400 draw, start, limit = self._extract_chunk(self.request)
403 search_q, order_by, order_dir = self._extract_ordering(
401 search_q, order_by, order_dir = self._extract_ordering(
404 self.request, column_map=column_map)
402 self.request, column_map=column_map)
405
403
406 ssh_keys_data_total_count = UserSshKeys.query()\
404 ssh_keys_data_total_count = UserSshKeys.query()\
407 .count()
405 .count()
408
406
409 # json generate
407 # json generate
410 base_q = UserSshKeys.query().join(UserSshKeys.user)
408 base_q = UserSshKeys.query().join(UserSshKeys.user)
411
409
412 if search_q:
410 if search_q:
413 like_expression = u'%{}%'.format(safe_str(search_q))
411 like_expression = f'%{safe_str(search_q)}%'
414 base_q = base_q.filter(or_(
412 base_q = base_q.filter(or_(
415 User.username.ilike(like_expression),
413 User.username.ilike(like_expression),
416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
414 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 ))
415 ))
418
416
419 users_data_total_filtered_count = base_q.count()
417 users_data_total_filtered_count = base_q.count()
420
418
421 sort_col = self._get_order_col(order_by, UserSshKeys)
419 sort_col = self._get_order_col(order_by, UserSshKeys)
422 if sort_col:
420 if sort_col:
423 if order_dir == 'asc':
421 if order_dir == 'asc':
424 # handle null values properly to order by NULL last
422 # handle null values properly to order by NULL last
425 if order_by in ['created_on']:
423 if order_by in ['created_on']:
426 sort_col = coalesce(sort_col, datetime.date.max)
424 sort_col = coalesce(sort_col, datetime.date.max)
427 sort_col = sort_col.asc()
425 sort_col = sort_col.asc()
428 else:
426 else:
429 # handle null values properly to order by NULL last
427 # handle null values properly to order by NULL last
430 if order_by in ['created_on']:
428 if order_by in ['created_on']:
431 sort_col = coalesce(sort_col, datetime.date.min)
429 sort_col = coalesce(sort_col, datetime.date.min)
432 sort_col = sort_col.desc()
430 sort_col = sort_col.desc()
433
431
434 base_q = base_q.order_by(sort_col)
432 base_q = base_q.order_by(sort_col)
435 base_q = base_q.offset(start).limit(limit)
433 base_q = base_q.offset(start).limit(limit)
436
434
437 ssh_keys = base_q.all()
435 ssh_keys = base_q.all()
438
436
439 ssh_keys_data = []
437 ssh_keys_data = []
440 for ssh_key in ssh_keys:
438 for ssh_key in ssh_keys:
441 ssh_keys_data.append({
439 ssh_keys_data.append({
442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
440 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 "fingerprint": ssh_key.ssh_key_fingerprint,
441 "fingerprint": ssh_key.ssh_key_fingerprint,
444 "description": ssh_key.description,
442 "description": ssh_key.description,
445 "created_on": h.format_date(ssh_key.created_on),
443 "created_on": h.format_date(ssh_key.created_on),
446 "accessed_on": h.format_date(ssh_key.accessed_on),
444 "accessed_on": h.format_date(ssh_key.accessed_on),
447 "action": h.link_to(
445 "action": h.link_to(
448 _('Edit'), h.route_path('edit_user_ssh_keys',
446 _('Edit'), h.route_path('edit_user_ssh_keys',
449 user_id=ssh_key.user.user_id))
447 user_id=ssh_key.user.user_id))
450 })
448 })
451
449
452 data = ({
450 data = ({
453 'draw': draw,
451 'draw': draw,
454 'data': ssh_keys_data,
452 'data': ssh_keys_data,
455 'recordsTotal': ssh_keys_data_total_count,
453 'recordsTotal': ssh_keys_data_total_count,
456 'recordsFiltered': users_data_total_filtered_count,
454 'recordsFiltered': users_data_total_filtered_count,
457 })
455 })
458
456
459 return data
457 return data
460
458
461 @LoginRequired()
459 @LoginRequired()
462 @HasPermissionAllDecorator('hg.admin')
460 @HasPermissionAllDecorator('hg.admin')
463 @CSRFRequired()
461 @CSRFRequired()
464 def ssh_keys_update(self):
462 def ssh_keys_update(self):
465 _ = self.request.translate
463 _ = self.request.translate
466 self.load_default_context()
464 self.load_default_context()
467
465
468 ssh_enabled = self.ssh_enabled()
466 ssh_enabled = self.ssh_enabled()
469 key_file = self.request.registry.settings.get(
467 key_file = self.request.registry.settings.get(
470 'ssh.authorized_keys_file_path')
468 'ssh.authorized_keys_file_path')
471 if ssh_enabled:
469 if ssh_enabled:
472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
470 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 h.flash(_('Updated SSH keys file: {}').format(key_file),
471 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 category='success')
472 category='success')
475 else:
473 else:
476 h.flash(_('SSH key support is disabled in .ini file'),
474 h.flash(_('SSH key support is disabled in .ini file'),
477 category='warning')
475 category='warning')
478
476
479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
477 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,170 +1,168 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 import psutil
21 import psutil
24 import signal
22 import signal
25
23
26
24
27 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps._base.navigation import navigation_list
26 from rhodecode.apps._base.navigation import navigation_list
29 from rhodecode.lib import system_info
27 from rhodecode.lib import system_info
30 from rhodecode.lib.auth import (
28 from rhodecode.lib.auth import (
31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
30 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
33
31
34 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
35
33
36
34
37 class AdminProcessManagementView(BaseAppView):
35 class AdminProcessManagementView(BaseAppView):
38 def load_default_context(self):
36 def load_default_context(self):
39 c = self._get_local_tmpl_context()
37 c = self._get_local_tmpl_context()
40 return c
38 return c
41
39
42 def _format_proc(self, proc, with_children=False):
40 def _format_proc(self, proc, with_children=False):
43 try:
41 try:
44 mem = proc.memory_info()
42 mem = proc.memory_info()
45 proc_formatted = StrictAttributeDict({
43 proc_formatted = StrictAttributeDict({
46 'pid': proc.pid,
44 'pid': proc.pid,
47 'name': proc.name(),
45 'name': proc.name(),
48 'mem_rss': mem.rss,
46 'mem_rss': mem.rss,
49 'mem_vms': mem.vms,
47 'mem_vms': mem.vms,
50 'cpu_percent': proc.cpu_percent(interval=0.1),
48 'cpu_percent': proc.cpu_percent(interval=0.1),
51 'create_time': proc.create_time(),
49 'create_time': proc.create_time(),
52 'cmd': ' '.join(proc.cmdline()),
50 'cmd': ' '.join(proc.cmdline()),
53 })
51 })
54
52
55 if with_children:
53 if with_children:
56 proc_formatted.update({
54 proc_formatted.update({
57 'children': [self._format_proc(x)
55 'children': [self._format_proc(x)
58 for x in proc.children(recursive=True)]
56 for x in proc.children(recursive=True)]
59 })
57 })
60 except Exception:
58 except Exception:
61 log.exception('Failed to load proc')
59 log.exception('Failed to load proc')
62 proc_formatted = None
60 proc_formatted = None
63 return proc_formatted
61 return proc_formatted
64
62
65 def get_processes(self):
63 def get_processes(self):
66 proc_list = []
64 proc_list = []
67 for p in psutil.process_iter():
65 for p in psutil.process_iter():
68 if 'gunicorn' in p.name():
66 if 'gunicorn' in p.name():
69 proc = self._format_proc(p, with_children=True)
67 proc = self._format_proc(p, with_children=True)
70 if proc:
68 if proc:
71 proc_list.append(proc)
69 proc_list.append(proc)
72
70
73 return proc_list
71 return proc_list
74
72
75 def get_workers(self):
73 def get_workers(self):
76 workers = None
74 workers = None
77 try:
75 try:
78 rc_config = system_info.rhodecode_config().value['config']
76 rc_config = system_info.rhodecode_config().value['config']
79 workers = rc_config['server:main'].get('workers')
77 workers = rc_config['server:main'].get('workers')
80 except Exception:
78 except Exception:
81 pass
79 pass
82
80
83 return workers or '?'
81 return workers or '?'
84
82
85 @LoginRequired()
83 @LoginRequired()
86 @HasPermissionAllDecorator('hg.admin')
84 @HasPermissionAllDecorator('hg.admin')
87 def process_management(self):
85 def process_management(self):
88 _ = self.request.translate
86 _ = self.request.translate
89 c = self.load_default_context()
87 c = self.load_default_context()
90
88
91 c.active = 'process_management'
89 c.active = 'process_management'
92 c.navlist = navigation_list(self.request)
90 c.navlist = navigation_list(self.request)
93 c.gunicorn_processes = self.get_processes()
91 c.gunicorn_processes = self.get_processes()
94 c.gunicorn_workers = self.get_workers()
92 c.gunicorn_workers = self.get_workers()
95 return self._get_template_context(c)
93 return self._get_template_context(c)
96
94
97 @LoginRequired()
95 @LoginRequired()
98 @HasPermissionAllDecorator('hg.admin')
96 @HasPermissionAllDecorator('hg.admin')
99 def process_management_data(self):
97 def process_management_data(self):
100 _ = self.request.translate
98 _ = self.request.translate
101 c = self.load_default_context()
99 c = self.load_default_context()
102 c.gunicorn_processes = self.get_processes()
100 c.gunicorn_processes = self.get_processes()
103 return self._get_template_context(c)
101 return self._get_template_context(c)
104
102
105 @LoginRequired()
103 @LoginRequired()
106 @HasPermissionAllDecorator('hg.admin')
104 @HasPermissionAllDecorator('hg.admin')
107 @CSRFRequired()
105 @CSRFRequired()
108 def process_management_signal(self):
106 def process_management_signal(self):
109 pids = self.request.json.get('pids', [])
107 pids = self.request.json.get('pids', [])
110 result = []
108 result = []
111
109
112 def on_terminate(proc):
110 def on_terminate(proc):
113 msg = "terminated"
111 msg = "terminated"
114 result.append(msg)
112 result.append(msg)
115
113
116 procs = []
114 procs = []
117 for pid in pids:
115 for pid in pids:
118 pid = safe_int(pid)
116 pid = safe_int(pid)
119 if pid:
117 if pid:
120 try:
118 try:
121 proc = psutil.Process(pid)
119 proc = psutil.Process(pid)
122 except psutil.NoSuchProcess:
120 except psutil.NoSuchProcess:
123 continue
121 continue
124
122
125 children = proc.children(recursive=True)
123 children = proc.children(recursive=True)
126 if children:
124 if children:
127 log.warning('Wont kill Master Process')
125 log.warning('Wont kill Master Process')
128 else:
126 else:
129 procs.append(proc)
127 procs.append(proc)
130
128
131 for p in procs:
129 for p in procs:
132 try:
130 try:
133 p.terminate()
131 p.terminate()
134 except psutil.AccessDenied as e:
132 except psutil.AccessDenied as e:
135 log.warning('Access denied: {}'.format(e))
133 log.warning(f'Access denied: {e}')
136
134
137 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
135 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
138 for p in alive:
136 for p in alive:
139 try:
137 try:
140 p.kill()
138 p.kill()
141 except psutil.AccessDenied as e:
139 except psutil.AccessDenied as e:
142 log.warning('Access denied: {}'.format(e))
140 log.warning(f'Access denied: {e}')
143
141
144 return {'result': result}
142 return {'result': result}
145
143
146 @LoginRequired()
144 @LoginRequired()
147 @HasPermissionAllDecorator('hg.admin')
145 @HasPermissionAllDecorator('hg.admin')
148 @CSRFRequired()
146 @CSRFRequired()
149 def process_management_master_signal(self):
147 def process_management_master_signal(self):
150 pid_data = self.request.json.get('pid_data', {})
148 pid_data = self.request.json.get('pid_data', {})
151 pid = safe_int(pid_data['pid'])
149 pid = safe_int(pid_data['pid'])
152 action = pid_data['action']
150 action = pid_data['action']
153 if pid:
151 if pid:
154 try:
152 try:
155 proc = psutil.Process(pid)
153 proc = psutil.Process(pid)
156 except psutil.NoSuchProcess:
154 except psutil.NoSuchProcess:
157 return {'result': 'failure_no_such_process'}
155 return {'result': 'failure_no_such_process'}
158
156
159 children = proc.children(recursive=True)
157 children = proc.children(recursive=True)
160 if children:
158 if children:
161 # master process
159 # master process
162 if action == '+' and len(children) <= 20:
160 if action == '+' and len(children) <= 20:
163 proc.send_signal(signal.SIGTTIN)
161 proc.send_signal(signal.SIGTTIN)
164 elif action == '-' and len(children) >= 2:
162 elif action == '-' and len(children) >= 2:
165 proc.send_signal(signal.SIGTTOU)
163 proc.send_signal(signal.SIGTTOU)
166 else:
164 else:
167 return {'result': 'failure_wrong_action'}
165 return {'result': 'failure_wrong_action'}
168 return {'result': 'success'}
166 return {'result': 'success'}
169
167
170 return {'result': 'failure_not_master'}
168 return {'result': 'failure_not_master'}
@@ -1,358 +1,356 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import datetime
18 import datetime
21 import logging
19 import logging
22 import time
20 import time
23
21
24 import formencode
22 import formencode
25 import formencode.htmlfill
23 import formencode.htmlfill
26
24
27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28
26
29 from pyramid.renderers import render
27 from pyramid.renderers import render
30 from pyramid.response import Response
28 from pyramid.response import Response
31 from sqlalchemy.orm import aliased
29 from sqlalchemy.orm import aliased
32
30
33 from rhodecode import events
31 from rhodecode import events
34 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
35
33
36 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
37 LoginRequired, CSRFRequired, NotAnonymous,
35 LoginRequired, CSRFRequired, NotAnonymous,
38 HasPermissionAny, HasRepoGroupPermissionAny)
36 HasPermissionAny, HasRepoGroupPermissionAny)
39 from rhodecode.lib import helpers as h, audit_logger
37 from rhodecode.lib import helpers as h, audit_logger
40 from rhodecode.lib.str_utils import safe_int, safe_str
38 from rhodecode.lib.str_utils import safe_int, safe_str
41 from rhodecode.model.forms import RepoGroupForm
39 from rhodecode.model.forms import RepoGroupForm
42 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.permission import PermissionModel
43 from rhodecode.model.repo_group import RepoGroupModel
41 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.scm import RepoGroupList
42 from rhodecode.model.scm import RepoGroupList
45 from rhodecode.model.db import (
43 from rhodecode.model.db import (
46 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
44 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
47
45
48 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
49
47
50
48
51 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
49 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
52
50
53 def load_default_context(self):
51 def load_default_context(self):
54 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
55
53
56 return c
54 return c
57
55
58 def _load_form_data(self, c):
56 def _load_form_data(self, c):
59 allow_empty_group = False
57 allow_empty_group = False
60
58
61 if self._can_create_repo_group():
59 if self._can_create_repo_group():
62 # we're global admin, we're ok and we can create TOP level groups
60 # we're global admin, we're ok and we can create TOP level groups
63 allow_empty_group = True
61 allow_empty_group = True
64
62
65 # override the choices for this form, we need to filter choices
63 # override the choices for this form, we need to filter choices
66 # and display only those we have ADMIN right
64 # and display only those we have ADMIN right
67 groups_with_admin_rights = RepoGroupList(
65 groups_with_admin_rights = RepoGroupList(
68 RepoGroup.query().all(),
66 RepoGroup.query().all(),
69 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
67 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
70 c.repo_groups = RepoGroup.groups_choices(
68 c.repo_groups = RepoGroup.groups_choices(
71 groups=groups_with_admin_rights,
69 groups=groups_with_admin_rights,
72 show_empty_group=allow_empty_group)
70 show_empty_group=allow_empty_group)
73 c.personal_repo_group = self._rhodecode_user.personal_repo_group
71 c.personal_repo_group = self._rhodecode_user.personal_repo_group
74
72
75 def _can_create_repo_group(self, parent_group_id=None):
73 def _can_create_repo_group(self, parent_group_id=None):
76 is_admin = HasPermissionAny('hg.admin')('group create controller')
74 is_admin = HasPermissionAny('hg.admin')('group create controller')
77 create_repo_group = HasPermissionAny(
75 create_repo_group = HasPermissionAny(
78 'hg.repogroup.create.true')('group create controller')
76 'hg.repogroup.create.true')('group create controller')
79 if is_admin or (create_repo_group and not parent_group_id):
77 if is_admin or (create_repo_group and not parent_group_id):
80 # we're global admin, or we have global repo group create
78 # we're global admin, or we have global repo group create
81 # permission
79 # permission
82 # we're ok and we can create TOP level groups
80 # we're ok and we can create TOP level groups
83 return True
81 return True
84 elif parent_group_id:
82 elif parent_group_id:
85 # we check the permission if we can write to parent group
83 # we check the permission if we can write to parent group
86 group = RepoGroup.get(parent_group_id)
84 group = RepoGroup.get(parent_group_id)
87 group_name = group.group_name if group else None
85 group_name = group.group_name if group else None
88 if HasRepoGroupPermissionAny('group.admin')(
86 if HasRepoGroupPermissionAny('group.admin')(
89 group_name, 'check if user is an admin of group'):
87 group_name, 'check if user is an admin of group'):
90 # we're an admin of passed in group, we're ok.
88 # we're an admin of passed in group, we're ok.
91 return True
89 return True
92 else:
90 else:
93 return False
91 return False
94 return False
92 return False
95
93
96 # permission check in data loading of
94 # permission check in data loading of
97 # `repo_group_list_data` via RepoGroupList
95 # `repo_group_list_data` via RepoGroupList
98 @LoginRequired()
96 @LoginRequired()
99 @NotAnonymous()
97 @NotAnonymous()
100 def repo_group_list(self):
98 def repo_group_list(self):
101 c = self.load_default_context()
99 c = self.load_default_context()
102 return self._get_template_context(c)
100 return self._get_template_context(c)
103
101
104 # permission check inside
102 # permission check inside
105 @LoginRequired()
103 @LoginRequired()
106 @NotAnonymous()
104 @NotAnonymous()
107 def repo_group_list_data(self):
105 def repo_group_list_data(self):
108 self.load_default_context()
106 self.load_default_context()
109 column_map = {
107 column_map = {
110 'name': 'group_name_hash',
108 'name': 'group_name_hash',
111 'desc': 'group_description',
109 'desc': 'group_description',
112 'last_change': 'updated_on',
110 'last_change': 'updated_on',
113 'top_level_repos': 'repos_total',
111 'top_level_repos': 'repos_total',
114 'owner': 'user_username',
112 'owner': 'user_username',
115 }
113 }
116 draw, start, limit = self._extract_chunk(self.request)
114 draw, start, limit = self._extract_chunk(self.request)
117 search_q, order_by, order_dir = self._extract_ordering(
115 search_q, order_by, order_dir = self._extract_ordering(
118 self.request, column_map=column_map)
116 self.request, column_map=column_map)
119
117
120 _render = self.request.get_partial_renderer(
118 _render = self.request.get_partial_renderer(
121 'rhodecode:templates/data_table/_dt_elements.mako')
119 'rhodecode:templates/data_table/_dt_elements.mako')
122 c = _render.get_call_context()
120 c = _render.get_call_context()
123
121
124 def quick_menu(repo_group_name):
122 def quick_menu(repo_group_name):
125 return _render('quick_repo_group_menu', repo_group_name)
123 return _render('quick_repo_group_menu', repo_group_name)
126
124
127 def repo_group_lnk(repo_group_name):
125 def repo_group_lnk(repo_group_name):
128 return _render('repo_group_name', repo_group_name)
126 return _render('repo_group_name', repo_group_name)
129
127
130 def last_change(last_change):
128 def last_change(last_change):
131 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
129 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
132 ts = time.time()
130 ts = time.time()
133 utc_offset = (datetime.datetime.fromtimestamp(ts)
131 utc_offset = (datetime.datetime.fromtimestamp(ts)
134 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
132 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
135 last_change = last_change + datetime.timedelta(seconds=utc_offset)
133 last_change = last_change + datetime.timedelta(seconds=utc_offset)
136 return _render("last_change", last_change)
134 return _render("last_change", last_change)
137
135
138 def desc(desc, personal):
136 def desc(desc, personal):
139 return _render(
137 return _render(
140 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
138 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
141
139
142 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
140 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
143 return _render(
141 return _render(
144 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
142 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
145
143
146 def user_profile(username):
144 def user_profile(username):
147 return _render('user_profile', username)
145 return _render('user_profile', username)
148
146
149 _perms = ['group.admin']
147 _perms = ['group.admin']
150 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
148 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
151
149
152 repo_groups_data_total_count = RepoGroup.query()\
150 repo_groups_data_total_count = RepoGroup.query()\
153 .filter(or_(
151 .filter(or_(
154 # generate multiple IN to fix limitation problems
152 # generate multiple IN to fix limitation problems
155 *in_filter_generator(RepoGroup.group_id, allowed_ids)
153 *in_filter_generator(RepoGroup.group_id, allowed_ids)
156 )) \
154 )) \
157 .count()
155 .count()
158
156
159 repo_groups_data_total_inactive_count = RepoGroup.query()\
157 repo_groups_data_total_inactive_count = RepoGroup.query()\
160 .filter(RepoGroup.group_id.in_(allowed_ids))\
158 .filter(RepoGroup.group_id.in_(allowed_ids))\
161 .count()
159 .count()
162
160
163 repo_count = count(Repository.repo_id)
161 repo_count = count(Repository.repo_id)
164 OwnerUser = aliased(User)
162 OwnerUser = aliased(User)
165 base_q = Session.query(
163 base_q = Session.query(
166 RepoGroup.group_name,
164 RepoGroup.group_name,
167 RepoGroup.group_name_hash,
165 RepoGroup.group_name_hash,
168 RepoGroup.group_description,
166 RepoGroup.group_description,
169 RepoGroup.group_id,
167 RepoGroup.group_id,
170 RepoGroup.personal,
168 RepoGroup.personal,
171 RepoGroup.updated_on,
169 RepoGroup.updated_on,
172 OwnerUser.username.label('owner_username'),
170 OwnerUser.username.label('owner_username'),
173 repo_count.label('repos_count')
171 repo_count.label('repos_count')
174 ) \
172 ) \
175 .filter(or_(
173 .filter(or_(
176 # generate multiple IN to fix limitation problems
174 # generate multiple IN to fix limitation problems
177 *in_filter_generator(RepoGroup.group_id, allowed_ids)
175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
178 )) \
176 )) \
179 .outerjoin(Repository, RepoGroup.group_id == Repository.group_id) \
177 .outerjoin(Repository, RepoGroup.group_id == Repository.group_id) \
180 .join(OwnerUser, RepoGroup.user_id == OwnerUser.user_id)
178 .join(OwnerUser, RepoGroup.user_id == OwnerUser.user_id)
181
179
182 base_q = base_q.group_by(RepoGroup, OwnerUser)
180 base_q = base_q.group_by(RepoGroup, OwnerUser)
183
181
184 if search_q:
182 if search_q:
185 like_expression = u'%{}%'.format(safe_str(search_q))
183 like_expression = f'%{safe_str(search_q)}%'
186 base_q = base_q.filter(or_(
184 base_q = base_q.filter(or_(
187 RepoGroup.group_name.ilike(like_expression),
185 RepoGroup.group_name.ilike(like_expression),
188 ))
186 ))
189
187
190 repo_groups_data_total_filtered_count = base_q.count()
188 repo_groups_data_total_filtered_count = base_q.count()
191 # the inactive isn't really used, but we still make it same as other data grids
189 # the inactive isn't really used, but we still make it same as other data grids
192 # which use inactive (users,user groups)
190 # which use inactive (users,user groups)
193 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
194
192
195 sort_defined = False
193 sort_defined = False
196 if order_by == 'group_name':
194 if order_by == 'group_name':
197 sort_col = func.lower(RepoGroup.group_name)
195 sort_col = func.lower(RepoGroup.group_name)
198 sort_defined = True
196 sort_defined = True
199 elif order_by == 'repos_total':
197 elif order_by == 'repos_total':
200 sort_col = repo_count
198 sort_col = repo_count
201 sort_defined = True
199 sort_defined = True
202 elif order_by == 'user_username':
200 elif order_by == 'user_username':
203 sort_col = OwnerUser.username
201 sort_col = OwnerUser.username
204 else:
202 else:
205 sort_col = getattr(RepoGroup, order_by, None)
203 sort_col = getattr(RepoGroup, order_by, None)
206
204
207 if sort_defined or sort_col:
205 if sort_defined or sort_col:
208 if order_dir == 'asc':
206 if order_dir == 'asc':
209 sort_col = sort_col.asc()
207 sort_col = sort_col.asc()
210 else:
208 else:
211 sort_col = sort_col.desc()
209 sort_col = sort_col.desc()
212
210
213 base_q = base_q.order_by(sort_col)
211 base_q = base_q.order_by(sort_col)
214 base_q = base_q.offset(start).limit(limit)
212 base_q = base_q.offset(start).limit(limit)
215
213
216 # authenticated access to user groups
214 # authenticated access to user groups
217 auth_repo_group_list = base_q.all()
215 auth_repo_group_list = base_q.all()
218
216
219 repo_groups_data = []
217 repo_groups_data = []
220 for repo_gr in auth_repo_group_list:
218 for repo_gr in auth_repo_group_list:
221 row = {
219 row = {
222 "menu": quick_menu(repo_gr.group_name),
220 "menu": quick_menu(repo_gr.group_name),
223 "name": repo_group_lnk(repo_gr.group_name),
221 "name": repo_group_lnk(repo_gr.group_name),
224
222
225 "last_change": last_change(repo_gr.updated_on),
223 "last_change": last_change(repo_gr.updated_on),
226
224
227 "last_changeset": "",
225 "last_changeset": "",
228 "last_changeset_raw": "",
226 "last_changeset_raw": "",
229
227
230 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 "desc": desc(repo_gr.group_description, repo_gr.personal),
231 "owner": user_profile(repo_gr.owner_username),
229 "owner": user_profile(repo_gr.owner_username),
232 "top_level_repos": repo_gr.repos_count,
230 "top_level_repos": repo_gr.repos_count,
233 "action": repo_group_actions(
231 "action": repo_group_actions(
234 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
235 }
233 }
236
234
237 repo_groups_data.append(row)
235 repo_groups_data.append(row)
238
236
239 data = ({
237 data = ({
240 'draw': draw,
238 'draw': draw,
241 'data': repo_groups_data,
239 'data': repo_groups_data,
242 'recordsTotal': repo_groups_data_total_count,
240 'recordsTotal': repo_groups_data_total_count,
243 'recordsTotalInactive': repo_groups_data_total_inactive_count,
241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
244 'recordsFiltered': repo_groups_data_total_filtered_count,
242 'recordsFiltered': repo_groups_data_total_filtered_count,
245 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
246 })
244 })
247
245
248 return data
246 return data
249
247
250 @LoginRequired()
248 @LoginRequired()
251 @NotAnonymous()
249 @NotAnonymous()
252 # perm checks inside
250 # perm checks inside
253 def repo_group_new(self):
251 def repo_group_new(self):
254 c = self.load_default_context()
252 c = self.load_default_context()
255
253
256 # perm check for admin, create_group perm or admin of parent_group
254 # perm check for admin, create_group perm or admin of parent_group
257 parent_group_id = safe_int(self.request.GET.get('parent_group'))
255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
258 _gr = RepoGroup.get(parent_group_id)
256 _gr = RepoGroup.get(parent_group_id)
259 if not self._can_create_repo_group(parent_group_id):
257 if not self._can_create_repo_group(parent_group_id):
260 raise HTTPForbidden()
258 raise HTTPForbidden()
261
259
262 self._load_form_data(c)
260 self._load_form_data(c)
263
261
264 defaults = {} # Future proof for default of repo group
262 defaults = {} # Future proof for default of repo group
265
263
266 parent_group_choice = '-1'
264 parent_group_choice = '-1'
267 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
268 parent_group_choice = self._rhodecode_user.personal_repo_group
266 parent_group_choice = self._rhodecode_user.personal_repo_group
269
267
270 if parent_group_id and _gr:
268 if parent_group_id and _gr:
271 if parent_group_id in [x[0] for x in c.repo_groups]:
269 if parent_group_id in [x[0] for x in c.repo_groups]:
272 parent_group_choice = safe_str(parent_group_id)
270 parent_group_choice = safe_str(parent_group_id)
273
271
274 defaults.update({'group_parent_id': parent_group_choice})
272 defaults.update({'group_parent_id': parent_group_choice})
275
273
276 data = render(
274 data = render(
277 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
278 self._get_template_context(c), self.request)
276 self._get_template_context(c), self.request)
279
277
280 html = formencode.htmlfill.render(
278 html = formencode.htmlfill.render(
281 data,
279 data,
282 defaults=defaults,
280 defaults=defaults,
283 encoding="UTF-8",
281 encoding="UTF-8",
284 force_defaults=False
282 force_defaults=False
285 )
283 )
286 return Response(html)
284 return Response(html)
287
285
288 @LoginRequired()
286 @LoginRequired()
289 @NotAnonymous()
287 @NotAnonymous()
290 @CSRFRequired()
288 @CSRFRequired()
291 # perm checks inside
289 # perm checks inside
292 def repo_group_create(self):
290 def repo_group_create(self):
293 c = self.load_default_context()
291 c = self.load_default_context()
294 _ = self.request.translate
292 _ = self.request.translate
295
293
296 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
297 can_create = self._can_create_repo_group(parent_group_id)
295 can_create = self._can_create_repo_group(parent_group_id)
298
296
299 self._load_form_data(c)
297 self._load_form_data(c)
300 # permissions for can create group based on parent_id are checked
298 # permissions for can create group based on parent_id are checked
301 # here in the Form
299 # here in the Form
302 available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
300 available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
303 repo_group_form = RepoGroupForm(
301 repo_group_form = RepoGroupForm(
304 self.request.translate, available_groups=available_groups,
302 self.request.translate, available_groups=available_groups,
305 can_create_in_root=can_create)()
303 can_create_in_root=can_create)()
306
304
307 repo_group_name = self.request.POST.get('group_name')
305 repo_group_name = self.request.POST.get('group_name')
308 try:
306 try:
309 owner = self._rhodecode_user
307 owner = self._rhodecode_user
310 form_result = repo_group_form.to_python(dict(self.request.POST))
308 form_result = repo_group_form.to_python(dict(self.request.POST))
311 copy_permissions = form_result.get('group_copy_permissions')
309 copy_permissions = form_result.get('group_copy_permissions')
312 repo_group = RepoGroupModel().create(
310 repo_group = RepoGroupModel().create(
313 group_name=form_result['group_name_full'],
311 group_name=form_result['group_name_full'],
314 group_description=form_result['group_description'],
312 group_description=form_result['group_description'],
315 owner=owner.user_id,
313 owner=owner.user_id,
316 copy_permissions=form_result['group_copy_permissions']
314 copy_permissions=form_result['group_copy_permissions']
317 )
315 )
318 Session().flush()
316 Session().flush()
319
317
320 repo_group_data = repo_group.get_api_data()
318 repo_group_data = repo_group.get_api_data()
321 audit_logger.store_web(
319 audit_logger.store_web(
322 'repo_group.create', action_data={'data': repo_group_data},
320 'repo_group.create', action_data={'data': repo_group_data},
323 user=self._rhodecode_user)
321 user=self._rhodecode_user)
324
322
325 Session().commit()
323 Session().commit()
326
324
327 _new_group_name = form_result['group_name_full']
325 _new_group_name = form_result['group_name_full']
328
326
329 repo_group_url = h.link_to(
327 repo_group_url = h.link_to(
330 _new_group_name,
328 _new_group_name,
331 h.route_path('repo_group_home', repo_group_name=_new_group_name))
329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
332 h.flash(h.literal(_('Created repository group %s')
330 h.flash(h.literal(_('Created repository group %s')
333 % repo_group_url), category='success')
331 % repo_group_url), category='success')
334
332
335 except formencode.Invalid as errors:
333 except formencode.Invalid as errors:
336 data = render(
334 data = render(
337 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
338 self._get_template_context(c), self.request)
336 self._get_template_context(c), self.request)
339 html = formencode.htmlfill.render(
337 html = formencode.htmlfill.render(
340 data,
338 data,
341 defaults=errors.value,
339 defaults=errors.value,
342 errors=errors.unpack_errors() or {},
340 errors=errors.unpack_errors() or {},
343 prefix_error=False,
341 prefix_error=False,
344 encoding="UTF-8",
342 encoding="UTF-8",
345 force_defaults=False
343 force_defaults=False
346 )
344 )
347 return Response(html)
345 return Response(html)
348 except Exception:
346 except Exception:
349 log.exception("Exception during creation of repository group")
347 log.exception("Exception during creation of repository group")
350 h.flash(_('Error occurred during creation of repository group %s')
348 h.flash(_('Error occurred during creation of repository group %s')
351 % repo_group_name, category='error')
349 % repo_group_name, category='error')
352 raise HTTPFound(h.route_path('home'))
350 raise HTTPFound(h.route_path('home'))
353
351
354 PermissionModel().trigger_permission_flush()
352 PermissionModel().trigger_permission_flush()
355
353
356 raise HTTPFound(
354 raise HTTPFound(
357 h.route_path('repo_group_home',
355 h.route_path('repo_group_home',
358 repo_group_name=form_result['group_name_full']))
356 repo_group_name=form_result['group_name_full']))
@@ -1,256 +1,254 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import formencode
20 import formencode
23 import formencode.htmlfill
21 import formencode.htmlfill
24
22
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
23 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26
24
27 from pyramid.renderers import render
25 from pyramid.renderers import render
28 from pyramid.response import Response
26 from pyramid.response import Response
29 from sqlalchemy.orm import aliased
27 from sqlalchemy.orm import aliased
30
28
31 from rhodecode import events
29 from rhodecode import events
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.lib.celerylib.utils import get_task_id
31 from rhodecode.lib.celerylib.utils import get_task_id
34
32
35 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
36 LoginRequired, CSRFRequired, NotAnonymous,
34 LoginRequired, CSRFRequired, NotAnonymous,
37 HasPermissionAny, HasRepoGroupPermissionAny)
35 HasPermissionAny, HasRepoGroupPermissionAny)
38 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
39 from rhodecode.lib.utils import repo_name_slug
37 from rhodecode.lib.utils import repo_name_slug
40 from rhodecode.lib.utils2 import safe_int, safe_str
38 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.model.forms import RepoForm
39 from rhodecode.model.forms import RepoForm
42 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.permission import PermissionModel
43 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
42 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
45 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.db import (
44 from rhodecode.model.db import (
47 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
45 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
48
46
49 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
50
48
51
49
52 class AdminReposView(BaseAppView, DataGridAppView):
50 class AdminReposView(BaseAppView, DataGridAppView):
53
51
54 def load_default_context(self):
52 def load_default_context(self):
55 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
56 return c
54 return c
57
55
58 def _load_form_data(self, c):
56 def _load_form_data(self, c):
59 acl_groups = RepoGroupList(RepoGroup.query().all(),
57 acl_groups = RepoGroupList(RepoGroup.query().all(),
60 perm_set=['group.write', 'group.admin'])
58 perm_set=['group.write', 'group.admin'])
61 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
62 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
60 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
63 c.personal_repo_group = self._rhodecode_user.personal_repo_group
61 c.personal_repo_group = self._rhodecode_user.personal_repo_group
64
62
65 @LoginRequired()
63 @LoginRequired()
66 @NotAnonymous()
64 @NotAnonymous()
67 # perms check inside
65 # perms check inside
68 def repository_list(self):
66 def repository_list(self):
69 c = self.load_default_context()
67 c = self.load_default_context()
70 return self._get_template_context(c)
68 return self._get_template_context(c)
71
69
72 @LoginRequired()
70 @LoginRequired()
73 @NotAnonymous()
71 @NotAnonymous()
74 # perms check inside
72 # perms check inside
75 def repository_list_data(self):
73 def repository_list_data(self):
76 self.load_default_context()
74 self.load_default_context()
77 column_map = {
75 column_map = {
78 'name': 'repo_name',
76 'name': 'repo_name',
79 'desc': 'description',
77 'desc': 'description',
80 'last_change': 'updated_on',
78 'last_change': 'updated_on',
81 'owner': 'user_username',
79 'owner': 'user_username',
82 }
80 }
83 draw, start, limit = self._extract_chunk(self.request)
81 draw, start, limit = self._extract_chunk(self.request)
84 search_q, order_by, order_dir = self._extract_ordering(
82 search_q, order_by, order_dir = self._extract_ordering(
85 self.request, column_map=column_map)
83 self.request, column_map=column_map)
86
84
87 _perms = ['repository.admin']
85 _perms = ['repository.admin']
88 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
86 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
89
87
90 repos_data_total_count = Repository.query() \
88 repos_data_total_count = Repository.query() \
91 .filter(or_(
89 .filter(or_(
92 # generate multiple IN to fix limitation problems
90 # generate multiple IN to fix limitation problems
93 *in_filter_generator(Repository.repo_id, allowed_ids))
91 *in_filter_generator(Repository.repo_id, allowed_ids))
94 ) \
92 ) \
95 .count()
93 .count()
96
94
97 RepoFork = aliased(Repository)
95 RepoFork = aliased(Repository)
98 OwnerUser = aliased(User)
96 OwnerUser = aliased(User)
99 base_q = Session.query(
97 base_q = Session.query(
100 Repository.repo_id,
98 Repository.repo_id,
101 Repository.repo_name,
99 Repository.repo_name,
102 Repository.description,
100 Repository.description,
103 Repository.repo_type,
101 Repository.repo_type,
104 Repository.repo_state,
102 Repository.repo_state,
105 Repository.private,
103 Repository.private,
106 Repository.archived,
104 Repository.archived,
107 Repository.updated_on,
105 Repository.updated_on,
108 Repository._changeset_cache,
106 Repository._changeset_cache,
109 RepoFork.repo_name.label('fork_repo_name'),
107 RepoFork.repo_name.label('fork_repo_name'),
110 OwnerUser.username.label('owner_username'),
108 OwnerUser.username.label('owner_username'),
111 ) \
109 ) \
112 .filter(or_(
110 .filter(or_(
113 # generate multiple IN to fix limitation problems
111 # generate multiple IN to fix limitation problems
114 *in_filter_generator(Repository.repo_id, allowed_ids))
112 *in_filter_generator(Repository.repo_id, allowed_ids))
115 ) \
113 ) \
116 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
114 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
117 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
115 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
118
116
119 if search_q:
117 if search_q:
120 like_expression = u'%{}%'.format(safe_str(search_q))
118 like_expression = f'%{safe_str(search_q)}%'
121 base_q = base_q.filter(or_(
119 base_q = base_q.filter(or_(
122 Repository.repo_name.ilike(like_expression),
120 Repository.repo_name.ilike(like_expression),
123 ))
121 ))
124
122
125 #TODO: check if we need group_by here ?
123 #TODO: check if we need group_by here ?
126 #base_q = base_q.group_by(Repository, User)
124 #base_q = base_q.group_by(Repository, User)
127
125
128 repos_data_total_filtered_count = base_q.count()
126 repos_data_total_filtered_count = base_q.count()
129
127
130 sort_defined = False
128 sort_defined = False
131 if order_by == 'repo_name':
129 if order_by == 'repo_name':
132 sort_col = func.lower(Repository.repo_name)
130 sort_col = func.lower(Repository.repo_name)
133 sort_defined = True
131 sort_defined = True
134 elif order_by == 'user_username':
132 elif order_by == 'user_username':
135 sort_col = OwnerUser.username
133 sort_col = OwnerUser.username
136 else:
134 else:
137 sort_col = getattr(Repository, order_by, None)
135 sort_col = getattr(Repository, order_by, None)
138
136
139 if sort_defined or sort_col:
137 if sort_defined or sort_col:
140 if order_dir == 'asc':
138 if order_dir == 'asc':
141 sort_col = sort_col.asc()
139 sort_col = sort_col.asc()
142 else:
140 else:
143 sort_col = sort_col.desc()
141 sort_col = sort_col.desc()
144
142
145 base_q = base_q.order_by(sort_col)
143 base_q = base_q.order_by(sort_col)
146 base_q = base_q.offset(start).limit(limit)
144 base_q = base_q.offset(start).limit(limit)
147
145
148 repos_list = base_q.all()
146 repos_list = base_q.all()
149
147
150 repos_data = RepoModel().get_repos_as_dict(
148 repos_data = RepoModel().get_repos_as_dict(
151 repo_list=repos_list, admin=True, super_user_actions=True)
149 repo_list=repos_list, admin=True, super_user_actions=True)
152
150
153 data = ({
151 data = ({
154 'draw': draw,
152 'draw': draw,
155 'data': repos_data,
153 'data': repos_data,
156 'recordsTotal': repos_data_total_count,
154 'recordsTotal': repos_data_total_count,
157 'recordsFiltered': repos_data_total_filtered_count,
155 'recordsFiltered': repos_data_total_filtered_count,
158 })
156 })
159 return data
157 return data
160
158
161 @LoginRequired()
159 @LoginRequired()
162 @NotAnonymous()
160 @NotAnonymous()
163 # perms check inside
161 # perms check inside
164 def repository_new(self):
162 def repository_new(self):
165 c = self.load_default_context()
163 c = self.load_default_context()
166
164
167 new_repo = self.request.GET.get('repo', '')
165 new_repo = self.request.GET.get('repo', '')
168 parent_group_id = safe_int(self.request.GET.get('parent_group'))
166 parent_group_id = safe_int(self.request.GET.get('parent_group'))
169 _gr = RepoGroup.get(parent_group_id)
167 _gr = RepoGroup.get(parent_group_id)
170
168
171 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
169 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
172 # you're not super admin nor have global create permissions,
170 # you're not super admin nor have global create permissions,
173 # but maybe you have at least write permission to a parent group ?
171 # but maybe you have at least write permission to a parent group ?
174
172
175 gr_name = _gr.group_name if _gr else None
173 gr_name = _gr.group_name if _gr else None
176 # create repositories with write permission on group is set to true
174 # create repositories with write permission on group is set to true
177 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
175 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
178 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
176 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
179 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
177 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
180 if not (group_admin or (group_write and create_on_write)):
178 if not (group_admin or (group_write and create_on_write)):
181 raise HTTPForbidden()
179 raise HTTPForbidden()
182
180
183 self._load_form_data(c)
181 self._load_form_data(c)
184 c.new_repo = repo_name_slug(new_repo)
182 c.new_repo = repo_name_slug(new_repo)
185
183
186 # apply the defaults from defaults page
184 # apply the defaults from defaults page
187 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
185 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
188 # set checkbox to autochecked
186 # set checkbox to autochecked
189 defaults['repo_copy_permissions'] = True
187 defaults['repo_copy_permissions'] = True
190
188
191 parent_group_choice = '-1'
189 parent_group_choice = '-1'
192 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
190 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
193 parent_group_choice = self._rhodecode_user.personal_repo_group
191 parent_group_choice = self._rhodecode_user.personal_repo_group
194
192
195 if parent_group_id and _gr:
193 if parent_group_id and _gr:
196 if parent_group_id in [x[0] for x in c.repo_groups]:
194 if parent_group_id in [x[0] for x in c.repo_groups]:
197 parent_group_choice = safe_str(parent_group_id)
195 parent_group_choice = safe_str(parent_group_id)
198
196
199 defaults.update({'repo_group': parent_group_choice})
197 defaults.update({'repo_group': parent_group_choice})
200
198
201 data = render('rhodecode:templates/admin/repos/repo_add.mako',
199 data = render('rhodecode:templates/admin/repos/repo_add.mako',
202 self._get_template_context(c), self.request)
200 self._get_template_context(c), self.request)
203 html = formencode.htmlfill.render(
201 html = formencode.htmlfill.render(
204 data,
202 data,
205 defaults=defaults,
203 defaults=defaults,
206 encoding="UTF-8",
204 encoding="UTF-8",
207 force_defaults=False
205 force_defaults=False
208 )
206 )
209 return Response(html)
207 return Response(html)
210
208
211 @LoginRequired()
209 @LoginRequired()
212 @NotAnonymous()
210 @NotAnonymous()
213 @CSRFRequired()
211 @CSRFRequired()
214 # perms check inside
212 # perms check inside
215 def repository_create(self):
213 def repository_create(self):
216 c = self.load_default_context()
214 c = self.load_default_context()
217
215
218 form_result = {}
216 form_result = {}
219 self._load_form_data(c)
217 self._load_form_data(c)
220
218
221 try:
219 try:
222 # CanWriteToGroup validators checks permissions of this POST
220 # CanWriteToGroup validators checks permissions of this POST
223 form = RepoForm(
221 form = RepoForm(
224 self.request.translate, repo_groups=c.repo_groups_choices)()
222 self.request.translate, repo_groups=c.repo_groups_choices)()
225 form_result = form.to_python(dict(self.request.POST))
223 form_result = form.to_python(dict(self.request.POST))
226 copy_permissions = form_result.get('repo_copy_permissions')
224 copy_permissions = form_result.get('repo_copy_permissions')
227 # create is done sometimes async on celery, db transaction
225 # create is done sometimes async on celery, db transaction
228 # management is handled there.
226 # management is handled there.
229 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
227 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
230 task_id = get_task_id(task)
228 task_id = get_task_id(task)
231 except formencode.Invalid as errors:
229 except formencode.Invalid as errors:
232 data = render('rhodecode:templates/admin/repos/repo_add.mako',
230 data = render('rhodecode:templates/admin/repos/repo_add.mako',
233 self._get_template_context(c), self.request)
231 self._get_template_context(c), self.request)
234 html = formencode.htmlfill.render(
232 html = formencode.htmlfill.render(
235 data,
233 data,
236 defaults=errors.value,
234 defaults=errors.value,
237 errors=errors.unpack_errors() or {},
235 errors=errors.unpack_errors() or {},
238 prefix_error=False,
236 prefix_error=False,
239 encoding="UTF-8",
237 encoding="UTF-8",
240 force_defaults=False
238 force_defaults=False
241 )
239 )
242 return Response(html)
240 return Response(html)
243
241
244 except Exception as e:
242 except Exception as e:
245 msg = self._log_creation_exception(e, form_result.get('repo_name'))
243 msg = self._log_creation_exception(e, form_result.get('repo_name'))
246 h.flash(msg, category='error')
244 h.flash(msg, category='error')
247 raise HTTPFound(h.route_path('home'))
245 raise HTTPFound(h.route_path('home'))
248
246
249 repo_name = form_result.get('repo_name_full')
247 repo_name = form_result.get('repo_name_full')
250
248
251 affected_user_ids = [self._rhodecode_user.user_id]
249 affected_user_ids = [self._rhodecode_user.user_id]
252 PermissionModel().trigger_permission_flush(affected_user_ids)
250 PermissionModel().trigger_permission_flush(affected_user_ids)
253
251
254 raise HTTPFound(
252 raise HTTPFound(
255 h.route_path('repo_creating', repo_name=repo_name,
253 h.route_path('repo_creating', repo_name=repo_name,
256 _query=dict(task_id=task_id)))
254 _query=dict(task_id=task_id)))
@@ -1,95 +1,93 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25
23
26 from rhodecode.apps._base import BaseAppView
24 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base.navigation import navigation_list
25 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.lib.auth import (
26 from rhodecode.lib.auth import (
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
27 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 from rhodecode.lib.utils2 import safe_int
28 from rhodecode.lib.utils2 import safe_int
31 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
32 from rhodecode.lib import user_sessions
30 from rhodecode.lib import user_sessions
33 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
34
32
35
33
36 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
37
35
38
36
39 class AdminSessionSettingsView(BaseAppView):
37 class AdminSessionSettingsView(BaseAppView):
40
38
41 def load_default_context(self):
39 def load_default_context(self):
42 c = self._get_local_tmpl_context()
40 c = self._get_local_tmpl_context()
43 return c
41 return c
44
42
45 @LoginRequired()
43 @LoginRequired()
46 @HasPermissionAllDecorator('hg.admin')
44 @HasPermissionAllDecorator('hg.admin')
47 def settings_sessions(self):
45 def settings_sessions(self):
48 c = self.load_default_context()
46 c = self.load_default_context()
49
47
50 c.active = 'sessions'
48 c.active = 'sessions'
51 c.navlist = navigation_list(self.request)
49 c.navlist = navigation_list(self.request)
52
50
53 c.cleanup_older_days = 60
51 c.cleanup_older_days = 60
54 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
52 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
55
53
56 config = system_info.rhodecode_config().get_value()['value']['config']
54 config = system_info.rhodecode_config().get_value()['value']['config']
57 c.session_model = user_sessions.get_session_handler(
55 c.session_model = user_sessions.get_session_handler(
58 config.get('beaker.session.type', 'memory'))(config)
56 config.get('beaker.session.type', 'memory'))(config)
59
57
60 c.session_conf = c.session_model.config
58 c.session_conf = c.session_model.config
61 c.session_count = c.session_model.get_count()
59 c.session_count = c.session_model.get_count()
62 c.session_expired_count = c.session_model.get_expired_count(
60 c.session_expired_count = c.session_model.get_expired_count(
63 older_than_seconds)
61 older_than_seconds)
64
62
65 return self._get_template_context(c)
63 return self._get_template_context(c)
66
64
67 @LoginRequired()
65 @LoginRequired()
68 @HasPermissionAllDecorator('hg.admin')
66 @HasPermissionAllDecorator('hg.admin')
69 @CSRFRequired()
67 @CSRFRequired()
70 def settings_sessions_cleanup(self):
68 def settings_sessions_cleanup(self):
71 _ = self.request.translate
69 _ = self.request.translate
72 expire_days = safe_int(self.request.params.get('expire_days'))
70 expire_days = safe_int(self.request.params.get('expire_days'))
73
71
74 if expire_days is None:
72 if expire_days is None:
75 expire_days = 60
73 expire_days = 60
76
74
77 older_than_seconds = 60 * 60 * 24 * expire_days
75 older_than_seconds = 60 * 60 * 24 * expire_days
78
76
79 config = system_info.rhodecode_config().get_value()['value']['config']
77 config = system_info.rhodecode_config().get_value()['value']['config']
80 session_model = user_sessions.get_session_handler(
78 session_model = user_sessions.get_session_handler(
81 config.get('beaker.session.type', 'memory'))(config)
79 config.get('beaker.session.type', 'memory'))(config)
82
80
83 try:
81 try:
84 session_model.clean_sessions(
82 session_model.clean_sessions(
85 older_than_seconds=older_than_seconds)
83 older_than_seconds=older_than_seconds)
86 h.flash(_('Cleaned up old sessions'), category='success')
84 h.flash(_('Cleaned up old sessions'), category='success')
87 except user_sessions.CleanupCommand as msg:
85 except user_sessions.CleanupCommand as msg:
88 h.flash(msg.message, category='warning')
86 h.flash(msg.message, category='warning')
89 except Exception as e:
87 except Exception as e:
90 log.exception('Failed session cleanup')
88 log.exception('Failed session cleanup')
91 h.flash(_('Failed to cleanup up old sessions'), category='error')
89 h.flash(_('Failed to cleanup up old sessions'), category='error')
92
90
93 redirect_to = self.request.resource_path(
91 redirect_to = self.request.resource_path(
94 self.context, route_name='admin_settings_sessions')
92 self.context, route_name='admin_settings_sessions')
95 return HTTPFound(redirect_to)
93 return HTTPFound(redirect_to)
@@ -1,722 +1,721 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20
19
21 import logging
20 import logging
22 import collections
21 import collections
23
22
24 import datetime
23 import datetime
25 import formencode
24 import formencode
26 import formencode.htmlfill
25 import formencode.htmlfill
27
26
28 import rhodecode
27 import rhodecode
29
28
30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.renderers import render
30 from pyramid.renderers import render
32 from pyramid.response import Response
31 from pyramid.response import Response
33
32
34 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base.navigation import navigation_list
34 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps.svn_support.config_keys import generate_config
35 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 from rhodecode.lib.celerylib import tasks, run_task
39 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.str_utils import safe_str
40 from rhodecode.lib.str_utils import safe_str
42 from rhodecode.lib.utils import repo2db_mapper
41 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, AttributeDict
42 from rhodecode.lib.utils2 import str2bool, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
43 from rhodecode.lib.index import searcher_from_config
45
44
46 from rhodecode.model.db import RhodeCodeUi, Repository
45 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
46 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
48 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
49 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
52
51
53 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.notification import EmailNotificationModel
53 from rhodecode.model.notification import EmailNotificationModel
55 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
56 from rhodecode.model.settings import (
55 from rhodecode.model.settings import (
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 SettingsModel)
57 SettingsModel)
59
58
60
59
61 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
62
61
63
62
64 class AdminSettingsView(BaseAppView):
63 class AdminSettingsView(BaseAppView):
65
64
66 def load_default_context(self):
65 def load_default_context(self):
67 c = self._get_local_tmpl_context()
66 c = self._get_local_tmpl_context()
68 c.labs_active = str2bool(
67 c.labs_active = str2bool(
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 c.navlist = navigation_list(self.request)
69 c.navlist = navigation_list(self.request)
71 return c
70 return c
72
71
73 @classmethod
72 @classmethod
74 def _get_ui_settings(cls):
73 def _get_ui_settings(cls):
75 ret = RhodeCodeUi.query().all()
74 ret = RhodeCodeUi.query().all()
76
75
77 if not ret:
76 if not ret:
78 raise Exception('Could not get application ui settings !')
77 raise Exception('Could not get application ui settings !')
79 settings = {}
78 settings = {}
80 for each in ret:
79 for each in ret:
81 k = each.ui_key
80 k = each.ui_key
82 v = each.ui_value
81 v = each.ui_value
83 if k == '/':
82 if k == '/':
84 k = 'root_path'
83 k = 'root_path'
85
84
86 if k in ['push_ssl', 'publish', 'enabled']:
85 if k in ['push_ssl', 'publish', 'enabled']:
87 v = str2bool(v)
86 v = str2bool(v)
88
87
89 if k.find('.') != -1:
88 if k.find('.') != -1:
90 k = k.replace('.', '_')
89 k = k.replace('.', '_')
91
90
92 if each.ui_section in ['hooks', 'extensions']:
91 if each.ui_section in ['hooks', 'extensions']:
93 v = each.ui_active
92 v = each.ui_active
94
93
95 settings[each.ui_section + '_' + k] = v
94 settings[each.ui_section + '_' + k] = v
96 return settings
95 return settings
97
96
98 @classmethod
97 @classmethod
99 def _form_defaults(cls):
98 def _form_defaults(cls):
100 defaults = SettingsModel().get_all_settings()
99 defaults = SettingsModel().get_all_settings()
101 defaults.update(cls._get_ui_settings())
100 defaults.update(cls._get_ui_settings())
102
101
103 defaults.update({
102 defaults.update({
104 'new_svn_branch': '',
103 'new_svn_branch': '',
105 'new_svn_tag': '',
104 'new_svn_tag': '',
106 })
105 })
107 return defaults
106 return defaults
108
107
109 @LoginRequired()
108 @LoginRequired()
110 @HasPermissionAllDecorator('hg.admin')
109 @HasPermissionAllDecorator('hg.admin')
111 def settings_vcs(self):
110 def settings_vcs(self):
112 c = self.load_default_context()
111 c = self.load_default_context()
113 c.active = 'vcs'
112 c.active = 'vcs'
114 model = VcsSettingsModel()
113 model = VcsSettingsModel()
115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
117
116
118 settings = self.request.registry.settings
117 settings = self.request.registry.settings
119 c.svn_proxy_generate_config = settings[generate_config]
118 c.svn_proxy_generate_config = settings[generate_config]
120
119
121 defaults = self._form_defaults()
120 defaults = self._form_defaults()
122
121
123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
124
123
125 data = render('rhodecode:templates/admin/settings/settings.mako',
124 data = render('rhodecode:templates/admin/settings/settings.mako',
126 self._get_template_context(c), self.request)
125 self._get_template_context(c), self.request)
127 html = formencode.htmlfill.render(
126 html = formencode.htmlfill.render(
128 data,
127 data,
129 defaults=defaults,
128 defaults=defaults,
130 encoding="UTF-8",
129 encoding="UTF-8",
131 force_defaults=False
130 force_defaults=False
132 )
131 )
133 return Response(html)
132 return Response(html)
134
133
135 @LoginRequired()
134 @LoginRequired()
136 @HasPermissionAllDecorator('hg.admin')
135 @HasPermissionAllDecorator('hg.admin')
137 @CSRFRequired()
136 @CSRFRequired()
138 def settings_vcs_update(self):
137 def settings_vcs_update(self):
139 _ = self.request.translate
138 _ = self.request.translate
140 c = self.load_default_context()
139 c = self.load_default_context()
141 c.active = 'vcs'
140 c.active = 'vcs'
142
141
143 model = VcsSettingsModel()
142 model = VcsSettingsModel()
144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
146
145
147 settings = self.request.registry.settings
146 settings = self.request.registry.settings
148 c.svn_proxy_generate_config = settings[generate_config]
147 c.svn_proxy_generate_config = settings[generate_config]
149
148
150 application_form = ApplicationUiSettingsForm(self.request.translate)()
149 application_form = ApplicationUiSettingsForm(self.request.translate)()
151
150
152 try:
151 try:
153 form_result = application_form.to_python(dict(self.request.POST))
152 form_result = application_form.to_python(dict(self.request.POST))
154 except formencode.Invalid as errors:
153 except formencode.Invalid as errors:
155 h.flash(
154 h.flash(
156 _("Some form inputs contain invalid data."),
155 _("Some form inputs contain invalid data."),
157 category='error')
156 category='error')
158 data = render('rhodecode:templates/admin/settings/settings.mako',
157 data = render('rhodecode:templates/admin/settings/settings.mako',
159 self._get_template_context(c), self.request)
158 self._get_template_context(c), self.request)
160 html = formencode.htmlfill.render(
159 html = formencode.htmlfill.render(
161 data,
160 data,
162 defaults=errors.value,
161 defaults=errors.value,
163 errors=errors.unpack_errors() or {},
162 errors=errors.unpack_errors() or {},
164 prefix_error=False,
163 prefix_error=False,
165 encoding="UTF-8",
164 encoding="UTF-8",
166 force_defaults=False
165 force_defaults=False
167 )
166 )
168 return Response(html)
167 return Response(html)
169
168
170 try:
169 try:
171 if c.visual.allow_repo_location_change:
170 if c.visual.allow_repo_location_change:
172 model.update_global_path_setting(form_result['paths_root_path'])
171 model.update_global_path_setting(form_result['paths_root_path'])
173
172
174 model.update_global_ssl_setting(form_result['web_push_ssl'])
173 model.update_global_ssl_setting(form_result['web_push_ssl'])
175 model.update_global_hook_settings(form_result)
174 model.update_global_hook_settings(form_result)
176
175
177 model.create_or_update_global_svn_settings(form_result)
176 model.create_or_update_global_svn_settings(form_result)
178 model.create_or_update_global_hg_settings(form_result)
177 model.create_or_update_global_hg_settings(form_result)
179 model.create_or_update_global_git_settings(form_result)
178 model.create_or_update_global_git_settings(form_result)
180 model.create_or_update_global_pr_settings(form_result)
179 model.create_or_update_global_pr_settings(form_result)
181 except Exception:
180 except Exception:
182 log.exception("Exception while updating settings")
181 log.exception("Exception while updating settings")
183 h.flash(_('Error occurred during updating '
182 h.flash(_('Error occurred during updating '
184 'application settings'), category='error')
183 'application settings'), category='error')
185 else:
184 else:
186 Session().commit()
185 Session().commit()
187 h.flash(_('Updated VCS settings'), category='success')
186 h.flash(_('Updated VCS settings'), category='success')
188 raise HTTPFound(h.route_path('admin_settings_vcs'))
187 raise HTTPFound(h.route_path('admin_settings_vcs'))
189
188
190 data = render('rhodecode:templates/admin/settings/settings.mako',
189 data = render('rhodecode:templates/admin/settings/settings.mako',
191 self._get_template_context(c), self.request)
190 self._get_template_context(c), self.request)
192 html = formencode.htmlfill.render(
191 html = formencode.htmlfill.render(
193 data,
192 data,
194 defaults=self._form_defaults(),
193 defaults=self._form_defaults(),
195 encoding="UTF-8",
194 encoding="UTF-8",
196 force_defaults=False
195 force_defaults=False
197 )
196 )
198 return Response(html)
197 return Response(html)
199
198
200 @LoginRequired()
199 @LoginRequired()
201 @HasPermissionAllDecorator('hg.admin')
200 @HasPermissionAllDecorator('hg.admin')
202 @CSRFRequired()
201 @CSRFRequired()
203 def settings_vcs_delete_svn_pattern(self):
202 def settings_vcs_delete_svn_pattern(self):
204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
203 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
205 model = VcsSettingsModel()
204 model = VcsSettingsModel()
206 try:
205 try:
207 model.delete_global_svn_pattern(delete_pattern_id)
206 model.delete_global_svn_pattern(delete_pattern_id)
208 except SettingNotFound:
207 except SettingNotFound:
209 log.exception(
208 log.exception(
210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
209 'Failed to delete svn_pattern with id %s', delete_pattern_id)
211 raise HTTPNotFound()
210 raise HTTPNotFound()
212
211
213 Session().commit()
212 Session().commit()
214 return True
213 return True
215
214
216 @LoginRequired()
215 @LoginRequired()
217 @HasPermissionAllDecorator('hg.admin')
216 @HasPermissionAllDecorator('hg.admin')
218 def settings_mapping(self):
217 def settings_mapping(self):
219 c = self.load_default_context()
218 c = self.load_default_context()
220 c.active = 'mapping'
219 c.active = 'mapping'
221
220
222 data = render('rhodecode:templates/admin/settings/settings.mako',
221 data = render('rhodecode:templates/admin/settings/settings.mako',
223 self._get_template_context(c), self.request)
222 self._get_template_context(c), self.request)
224 html = formencode.htmlfill.render(
223 html = formencode.htmlfill.render(
225 data,
224 data,
226 defaults=self._form_defaults(),
225 defaults=self._form_defaults(),
227 encoding="UTF-8",
226 encoding="UTF-8",
228 force_defaults=False
227 force_defaults=False
229 )
228 )
230 return Response(html)
229 return Response(html)
231
230
232 @LoginRequired()
231 @LoginRequired()
233 @HasPermissionAllDecorator('hg.admin')
232 @HasPermissionAllDecorator('hg.admin')
234 @CSRFRequired()
233 @CSRFRequired()
235 def settings_mapping_update(self):
234 def settings_mapping_update(self):
236 _ = self.request.translate
235 _ = self.request.translate
237 c = self.load_default_context()
236 c = self.load_default_context()
238 c.active = 'mapping'
237 c.active = 'mapping'
239 rm_obsolete = self.request.POST.get('destroy', False)
238 rm_obsolete = self.request.POST.get('destroy', False)
240 invalidate_cache = self.request.POST.get('invalidate', False)
239 invalidate_cache = self.request.POST.get('invalidate', False)
241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
240 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
242
241
243 if invalidate_cache:
242 if invalidate_cache:
244 log.debug('invalidating all repositories cache')
243 log.debug('invalidating all repositories cache')
245 for repo in Repository.get_all():
244 for repo in Repository.get_all():
246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
245 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
247
246
248 filesystem_repos = ScmModel().repo_scan()
247 filesystem_repos = ScmModel().repo_scan()
249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
248 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
250 PermissionModel().trigger_permission_flush()
249 PermissionModel().trigger_permission_flush()
251
250
252 def _repr(l):
251 def _repr(l):
253 return ', '.join(map(safe_str, l)) or '-'
252 return ', '.join(map(safe_str, l)) or '-'
254 h.flash(_('Repositories successfully '
253 h.flash(_('Repositories successfully '
255 'rescanned added: %s ; removed: %s') %
254 'rescanned added: %s ; removed: %s') %
256 (_repr(added), _repr(removed)),
255 (_repr(added), _repr(removed)),
257 category='success')
256 category='success')
258 raise HTTPFound(h.route_path('admin_settings_mapping'))
257 raise HTTPFound(h.route_path('admin_settings_mapping'))
259
258
260 @LoginRequired()
259 @LoginRequired()
261 @HasPermissionAllDecorator('hg.admin')
260 @HasPermissionAllDecorator('hg.admin')
262 def settings_global(self):
261 def settings_global(self):
263 c = self.load_default_context()
262 c = self.load_default_context()
264 c.active = 'global'
263 c.active = 'global'
265 c.personal_repo_group_default_pattern = RepoGroupModel()\
264 c.personal_repo_group_default_pattern = RepoGroupModel()\
266 .get_personal_group_name_pattern()
265 .get_personal_group_name_pattern()
267
266
268 data = render('rhodecode:templates/admin/settings/settings.mako',
267 data = render('rhodecode:templates/admin/settings/settings.mako',
269 self._get_template_context(c), self.request)
268 self._get_template_context(c), self.request)
270 html = formencode.htmlfill.render(
269 html = formencode.htmlfill.render(
271 data,
270 data,
272 defaults=self._form_defaults(),
271 defaults=self._form_defaults(),
273 encoding="UTF-8",
272 encoding="UTF-8",
274 force_defaults=False
273 force_defaults=False
275 )
274 )
276 return Response(html)
275 return Response(html)
277
276
278 @LoginRequired()
277 @LoginRequired()
279 @HasPermissionAllDecorator('hg.admin')
278 @HasPermissionAllDecorator('hg.admin')
280 @CSRFRequired()
279 @CSRFRequired()
281 def settings_global_update(self):
280 def settings_global_update(self):
282 _ = self.request.translate
281 _ = self.request.translate
283 c = self.load_default_context()
282 c = self.load_default_context()
284 c.active = 'global'
283 c.active = 'global'
285 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 .get_personal_group_name_pattern()
285 .get_personal_group_name_pattern()
287 application_form = ApplicationSettingsForm(self.request.translate)()
286 application_form = ApplicationSettingsForm(self.request.translate)()
288 try:
287 try:
289 form_result = application_form.to_python(dict(self.request.POST))
288 form_result = application_form.to_python(dict(self.request.POST))
290 except formencode.Invalid as errors:
289 except formencode.Invalid as errors:
291 h.flash(
290 h.flash(
292 _("Some form inputs contain invalid data."),
291 _("Some form inputs contain invalid data."),
293 category='error')
292 category='error')
294 data = render('rhodecode:templates/admin/settings/settings.mako',
293 data = render('rhodecode:templates/admin/settings/settings.mako',
295 self._get_template_context(c), self.request)
294 self._get_template_context(c), self.request)
296 html = formencode.htmlfill.render(
295 html = formencode.htmlfill.render(
297 data,
296 data,
298 defaults=errors.value,
297 defaults=errors.value,
299 errors=errors.unpack_errors() or {},
298 errors=errors.unpack_errors() or {},
300 prefix_error=False,
299 prefix_error=False,
301 encoding="UTF-8",
300 encoding="UTF-8",
302 force_defaults=False
301 force_defaults=False
303 )
302 )
304 return Response(html)
303 return Response(html)
305
304
306 settings = [
305 settings = [
307 ('title', 'rhodecode_title', 'unicode'),
306 ('title', 'rhodecode_title', 'unicode'),
308 ('realm', 'rhodecode_realm', 'unicode'),
307 ('realm', 'rhodecode_realm', 'unicode'),
309 ('pre_code', 'rhodecode_pre_code', 'unicode'),
308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
310 ('post_code', 'rhodecode_post_code', 'unicode'),
309 ('post_code', 'rhodecode_post_code', 'unicode'),
311 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
312 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
313 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
314 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
315 ]
314 ]
316
315
317 try:
316 try:
318 for setting, form_key, type_ in settings:
317 for setting, form_key, type_ in settings:
319 sett = SettingsModel().create_or_update_setting(
318 sett = SettingsModel().create_or_update_setting(
320 setting, form_result[form_key], type_)
319 setting, form_result[form_key], type_)
321 Session().add(sett)
320 Session().add(sett)
322
321
323 Session().commit()
322 Session().commit()
324 SettingsModel().invalidate_settings_cache()
323 SettingsModel().invalidate_settings_cache()
325 h.flash(_('Updated application settings'), category='success')
324 h.flash(_('Updated application settings'), category='success')
326 except Exception:
325 except Exception:
327 log.exception("Exception while updating application settings")
326 log.exception("Exception while updating application settings")
328 h.flash(
327 h.flash(
329 _('Error occurred during updating application settings'),
328 _('Error occurred during updating application settings'),
330 category='error')
329 category='error')
331
330
332 raise HTTPFound(h.route_path('admin_settings_global'))
331 raise HTTPFound(h.route_path('admin_settings_global'))
333
332
334 @LoginRequired()
333 @LoginRequired()
335 @HasPermissionAllDecorator('hg.admin')
334 @HasPermissionAllDecorator('hg.admin')
336 def settings_visual(self):
335 def settings_visual(self):
337 c = self.load_default_context()
336 c = self.load_default_context()
338 c.active = 'visual'
337 c.active = 'visual'
339
338
340 data = render('rhodecode:templates/admin/settings/settings.mako',
339 data = render('rhodecode:templates/admin/settings/settings.mako',
341 self._get_template_context(c), self.request)
340 self._get_template_context(c), self.request)
342 html = formencode.htmlfill.render(
341 html = formencode.htmlfill.render(
343 data,
342 data,
344 defaults=self._form_defaults(),
343 defaults=self._form_defaults(),
345 encoding="UTF-8",
344 encoding="UTF-8",
346 force_defaults=False
345 force_defaults=False
347 )
346 )
348 return Response(html)
347 return Response(html)
349
348
350 @LoginRequired()
349 @LoginRequired()
351 @HasPermissionAllDecorator('hg.admin')
350 @HasPermissionAllDecorator('hg.admin')
352 @CSRFRequired()
351 @CSRFRequired()
353 def settings_visual_update(self):
352 def settings_visual_update(self):
354 _ = self.request.translate
353 _ = self.request.translate
355 c = self.load_default_context()
354 c = self.load_default_context()
356 c.active = 'visual'
355 c.active = 'visual'
357 application_form = ApplicationVisualisationForm(self.request.translate)()
356 application_form = ApplicationVisualisationForm(self.request.translate)()
358 try:
357 try:
359 form_result = application_form.to_python(dict(self.request.POST))
358 form_result = application_form.to_python(dict(self.request.POST))
360 except formencode.Invalid as errors:
359 except formencode.Invalid as errors:
361 h.flash(
360 h.flash(
362 _("Some form inputs contain invalid data."),
361 _("Some form inputs contain invalid data."),
363 category='error')
362 category='error')
364 data = render('rhodecode:templates/admin/settings/settings.mako',
363 data = render('rhodecode:templates/admin/settings/settings.mako',
365 self._get_template_context(c), self.request)
364 self._get_template_context(c), self.request)
366 html = formencode.htmlfill.render(
365 html = formencode.htmlfill.render(
367 data,
366 data,
368 defaults=errors.value,
367 defaults=errors.value,
369 errors=errors.unpack_errors() or {},
368 errors=errors.unpack_errors() or {},
370 prefix_error=False,
369 prefix_error=False,
371 encoding="UTF-8",
370 encoding="UTF-8",
372 force_defaults=False
371 force_defaults=False
373 )
372 )
374 return Response(html)
373 return Response(html)
375
374
376 try:
375 try:
377 settings = [
376 settings = [
378 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
377 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
379 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
378 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
380 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
379 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
381 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
380 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
382 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
381 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
383 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
382 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
384 ('show_version', 'rhodecode_show_version', 'bool'),
383 ('show_version', 'rhodecode_show_version', 'bool'),
385 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
384 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
386 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
385 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
387 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
386 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
388 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
387 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
389 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
388 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
390 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
389 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
391 ('support_url', 'rhodecode_support_url', 'unicode'),
390 ('support_url', 'rhodecode_support_url', 'unicode'),
392 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
391 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
393 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
392 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
394 ]
393 ]
395 for setting, form_key, type_ in settings:
394 for setting, form_key, type_ in settings:
396 sett = SettingsModel().create_or_update_setting(
395 sett = SettingsModel().create_or_update_setting(
397 setting, form_result[form_key], type_)
396 setting, form_result[form_key], type_)
398 Session().add(sett)
397 Session().add(sett)
399
398
400 Session().commit()
399 Session().commit()
401 SettingsModel().invalidate_settings_cache()
400 SettingsModel().invalidate_settings_cache()
402 h.flash(_('Updated visualisation settings'), category='success')
401 h.flash(_('Updated visualisation settings'), category='success')
403 except Exception:
402 except Exception:
404 log.exception("Exception updating visualization settings")
403 log.exception("Exception updating visualization settings")
405 h.flash(_('Error occurred during updating '
404 h.flash(_('Error occurred during updating '
406 'visualisation settings'),
405 'visualisation settings'),
407 category='error')
406 category='error')
408
407
409 raise HTTPFound(h.route_path('admin_settings_visual'))
408 raise HTTPFound(h.route_path('admin_settings_visual'))
410
409
411 @LoginRequired()
410 @LoginRequired()
412 @HasPermissionAllDecorator('hg.admin')
411 @HasPermissionAllDecorator('hg.admin')
413 def settings_issuetracker(self):
412 def settings_issuetracker(self):
414 c = self.load_default_context()
413 c = self.load_default_context()
415 c.active = 'issuetracker'
414 c.active = 'issuetracker'
416 defaults = c.rc_config
415 defaults = c.rc_config
417
416
418 entry_key = 'rhodecode_issuetracker_pat_'
417 entry_key = 'rhodecode_issuetracker_pat_'
419
418
420 c.issuetracker_entries = {}
419 c.issuetracker_entries = {}
421 for k, v in defaults.items():
420 for k, v in defaults.items():
422 if k.startswith(entry_key):
421 if k.startswith(entry_key):
423 uid = k[len(entry_key):]
422 uid = k[len(entry_key):]
424 c.issuetracker_entries[uid] = None
423 c.issuetracker_entries[uid] = None
425
424
426 for uid in c.issuetracker_entries:
425 for uid in c.issuetracker_entries:
427 c.issuetracker_entries[uid] = AttributeDict({
426 c.issuetracker_entries[uid] = AttributeDict({
428 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
427 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
429 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
428 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
430 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
429 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
431 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
430 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
432 })
431 })
433
432
434 return self._get_template_context(c)
433 return self._get_template_context(c)
435
434
436 @LoginRequired()
435 @LoginRequired()
437 @HasPermissionAllDecorator('hg.admin')
436 @HasPermissionAllDecorator('hg.admin')
438 @CSRFRequired()
437 @CSRFRequired()
439 def settings_issuetracker_test(self):
438 def settings_issuetracker_test(self):
440 error_container = []
439 error_container = []
441
440
442 urlified_commit = h.urlify_commit_message(
441 urlified_commit = h.urlify_commit_message(
443 self.request.POST.get('test_text', ''),
442 self.request.POST.get('test_text', ''),
444 'repo_group/test_repo1', error_container=error_container)
443 'repo_group/test_repo1', error_container=error_container)
445 if error_container:
444 if error_container:
446 def converter(inp):
445 def converter(inp):
447 return h.html_escape(inp)
446 return h.html_escape(inp)
448
447
449 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
448 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
450
449
451 return urlified_commit
450 return urlified_commit
452
451
453 @LoginRequired()
452 @LoginRequired()
454 @HasPermissionAllDecorator('hg.admin')
453 @HasPermissionAllDecorator('hg.admin')
455 @CSRFRequired()
454 @CSRFRequired()
456 def settings_issuetracker_update(self):
455 def settings_issuetracker_update(self):
457 _ = self.request.translate
456 _ = self.request.translate
458 self.load_default_context()
457 self.load_default_context()
459 settings_model = IssueTrackerSettingsModel()
458 settings_model = IssueTrackerSettingsModel()
460
459
461 try:
460 try:
462 form = IssueTrackerPatternsForm(self.request.translate)()
461 form = IssueTrackerPatternsForm(self.request.translate)()
463 data = form.to_python(self.request.POST)
462 data = form.to_python(self.request.POST)
464 except formencode.Invalid as errors:
463 except formencode.Invalid as errors:
465 log.exception('Failed to add new pattern')
464 log.exception('Failed to add new pattern')
466 error = errors
465 error = errors
467 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
466 h.flash(_(f'Invalid issue tracker pattern: {error}'),
468 category='error')
467 category='error')
469 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
468 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
470
469
471 if data:
470 if data:
472 for uid in data.get('delete_patterns', []):
471 for uid in data.get('delete_patterns', []):
473 settings_model.delete_entries(uid)
472 settings_model.delete_entries(uid)
474
473
475 for pattern in data.get('patterns', []):
474 for pattern in data.get('patterns', []):
476 for setting, value, type_ in pattern:
475 for setting, value, type_ in pattern:
477 sett = settings_model.create_or_update_setting(
476 sett = settings_model.create_or_update_setting(
478 setting, value, type_)
477 setting, value, type_)
479 Session().add(sett)
478 Session().add(sett)
480
479
481 Session().commit()
480 Session().commit()
482
481
483 SettingsModel().invalidate_settings_cache()
482 SettingsModel().invalidate_settings_cache()
484 h.flash(_('Updated issue tracker entries'), category='success')
483 h.flash(_('Updated issue tracker entries'), category='success')
485 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
484 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
486
485
487 @LoginRequired()
486 @LoginRequired()
488 @HasPermissionAllDecorator('hg.admin')
487 @HasPermissionAllDecorator('hg.admin')
489 @CSRFRequired()
488 @CSRFRequired()
490 def settings_issuetracker_delete(self):
489 def settings_issuetracker_delete(self):
491 _ = self.request.translate
490 _ = self.request.translate
492 self.load_default_context()
491 self.load_default_context()
493 uid = self.request.POST.get('uid')
492 uid = self.request.POST.get('uid')
494 try:
493 try:
495 IssueTrackerSettingsModel().delete_entries(uid)
494 IssueTrackerSettingsModel().delete_entries(uid)
496 except Exception:
495 except Exception:
497 log.exception('Failed to delete issue tracker setting %s', uid)
496 log.exception('Failed to delete issue tracker setting %s', uid)
498 raise HTTPNotFound()
497 raise HTTPNotFound()
499
498
500 SettingsModel().invalidate_settings_cache()
499 SettingsModel().invalidate_settings_cache()
501 h.flash(_('Removed issue tracker entry.'), category='success')
500 h.flash(_('Removed issue tracker entry.'), category='success')
502
501
503 return {'deleted': uid}
502 return {'deleted': uid}
504
503
505 @LoginRequired()
504 @LoginRequired()
506 @HasPermissionAllDecorator('hg.admin')
505 @HasPermissionAllDecorator('hg.admin')
507 def settings_email(self):
506 def settings_email(self):
508 c = self.load_default_context()
507 c = self.load_default_context()
509 c.active = 'email'
508 c.active = 'email'
510 c.rhodecode_ini = rhodecode.CONFIG
509 c.rhodecode_ini = rhodecode.CONFIG
511
510
512 data = render('rhodecode:templates/admin/settings/settings.mako',
511 data = render('rhodecode:templates/admin/settings/settings.mako',
513 self._get_template_context(c), self.request)
512 self._get_template_context(c), self.request)
514 html = formencode.htmlfill.render(
513 html = formencode.htmlfill.render(
515 data,
514 data,
516 defaults=self._form_defaults(),
515 defaults=self._form_defaults(),
517 encoding="UTF-8",
516 encoding="UTF-8",
518 force_defaults=False
517 force_defaults=False
519 )
518 )
520 return Response(html)
519 return Response(html)
521
520
522 @LoginRequired()
521 @LoginRequired()
523 @HasPermissionAllDecorator('hg.admin')
522 @HasPermissionAllDecorator('hg.admin')
524 @CSRFRequired()
523 @CSRFRequired()
525 def settings_email_update(self):
524 def settings_email_update(self):
526 _ = self.request.translate
525 _ = self.request.translate
527 c = self.load_default_context()
526 c = self.load_default_context()
528 c.active = 'email'
527 c.active = 'email'
529
528
530 test_email = self.request.POST.get('test_email')
529 test_email = self.request.POST.get('test_email')
531
530
532 if not test_email:
531 if not test_email:
533 h.flash(_('Please enter email address'), category='error')
532 h.flash(_('Please enter email address'), category='error')
534 raise HTTPFound(h.route_path('admin_settings_email'))
533 raise HTTPFound(h.route_path('admin_settings_email'))
535
534
536 email_kwargs = {
535 email_kwargs = {
537 'date': datetime.datetime.now(),
536 'date': datetime.datetime.now(),
538 'user': self._rhodecode_db_user
537 'user': self._rhodecode_db_user
539 }
538 }
540
539
541 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
540 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
542 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
541 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
543
542
544 recipients = [test_email] if test_email else None
543 recipients = [test_email] if test_email else None
545
544
546 run_task(tasks.send_email, recipients, subject,
545 run_task(tasks.send_email, recipients, subject,
547 email_body_plaintext, email_body)
546 email_body_plaintext, email_body)
548
547
549 h.flash(_('Send email task created'), category='success')
548 h.flash(_('Send email task created'), category='success')
550 raise HTTPFound(h.route_path('admin_settings_email'))
549 raise HTTPFound(h.route_path('admin_settings_email'))
551
550
552 @LoginRequired()
551 @LoginRequired()
553 @HasPermissionAllDecorator('hg.admin')
552 @HasPermissionAllDecorator('hg.admin')
554 def settings_hooks(self):
553 def settings_hooks(self):
555 c = self.load_default_context()
554 c = self.load_default_context()
556 c.active = 'hooks'
555 c.active = 'hooks'
557
556
558 model = SettingsModel()
557 model = SettingsModel()
559 c.hooks = model.get_builtin_hooks()
558 c.hooks = model.get_builtin_hooks()
560 c.custom_hooks = model.get_custom_hooks()
559 c.custom_hooks = model.get_custom_hooks()
561
560
562 data = render('rhodecode:templates/admin/settings/settings.mako',
561 data = render('rhodecode:templates/admin/settings/settings.mako',
563 self._get_template_context(c), self.request)
562 self._get_template_context(c), self.request)
564 html = formencode.htmlfill.render(
563 html = formencode.htmlfill.render(
565 data,
564 data,
566 defaults=self._form_defaults(),
565 defaults=self._form_defaults(),
567 encoding="UTF-8",
566 encoding="UTF-8",
568 force_defaults=False
567 force_defaults=False
569 )
568 )
570 return Response(html)
569 return Response(html)
571
570
572 @LoginRequired()
571 @LoginRequired()
573 @HasPermissionAllDecorator('hg.admin')
572 @HasPermissionAllDecorator('hg.admin')
574 @CSRFRequired()
573 @CSRFRequired()
575 def settings_hooks_update(self):
574 def settings_hooks_update(self):
576 _ = self.request.translate
575 _ = self.request.translate
577 c = self.load_default_context()
576 c = self.load_default_context()
578 c.active = 'hooks'
577 c.active = 'hooks'
579 if c.visual.allow_custom_hooks_settings:
578 if c.visual.allow_custom_hooks_settings:
580 ui_key = self.request.POST.get('new_hook_ui_key')
579 ui_key = self.request.POST.get('new_hook_ui_key')
581 ui_value = self.request.POST.get('new_hook_ui_value')
580 ui_value = self.request.POST.get('new_hook_ui_value')
582
581
583 hook_id = self.request.POST.get('hook_id')
582 hook_id = self.request.POST.get('hook_id')
584 new_hook = False
583 new_hook = False
585
584
586 model = SettingsModel()
585 model = SettingsModel()
587 try:
586 try:
588 if ui_value and ui_key:
587 if ui_value and ui_key:
589 model.create_or_update_hook(ui_key, ui_value)
588 model.create_or_update_hook(ui_key, ui_value)
590 h.flash(_('Added new hook'), category='success')
589 h.flash(_('Added new hook'), category='success')
591 new_hook = True
590 new_hook = True
592 elif hook_id:
591 elif hook_id:
593 RhodeCodeUi.delete(hook_id)
592 RhodeCodeUi.delete(hook_id)
594 Session().commit()
593 Session().commit()
595
594
596 # check for edits
595 # check for edits
597 update = False
596 update = False
598 _d = self.request.POST.dict_of_lists()
597 _d = self.request.POST.dict_of_lists()
599 for k, v in zip(_d.get('hook_ui_key', []),
598 for k, v in zip(_d.get('hook_ui_key', []),
600 _d.get('hook_ui_value_new', [])):
599 _d.get('hook_ui_value_new', [])):
601 model.create_or_update_hook(k, v)
600 model.create_or_update_hook(k, v)
602 update = True
601 update = True
603
602
604 if update and not new_hook:
603 if update and not new_hook:
605 h.flash(_('Updated hooks'), category='success')
604 h.flash(_('Updated hooks'), category='success')
606 Session().commit()
605 Session().commit()
607 except Exception:
606 except Exception:
608 log.exception("Exception during hook creation")
607 log.exception("Exception during hook creation")
609 h.flash(_('Error occurred during hook creation'),
608 h.flash(_('Error occurred during hook creation'),
610 category='error')
609 category='error')
611
610
612 raise HTTPFound(h.route_path('admin_settings_hooks'))
611 raise HTTPFound(h.route_path('admin_settings_hooks'))
613
612
614 @LoginRequired()
613 @LoginRequired()
615 @HasPermissionAllDecorator('hg.admin')
614 @HasPermissionAllDecorator('hg.admin')
616 def settings_search(self):
615 def settings_search(self):
617 c = self.load_default_context()
616 c = self.load_default_context()
618 c.active = 'search'
617 c.active = 'search'
619
618
620 c.searcher = searcher_from_config(self.request.registry.settings)
619 c.searcher = searcher_from_config(self.request.registry.settings)
621 c.statistics = c.searcher.statistics(self.request.translate)
620 c.statistics = c.searcher.statistics(self.request.translate)
622
621
623 return self._get_template_context(c)
622 return self._get_template_context(c)
624
623
625 @LoginRequired()
624 @LoginRequired()
626 @HasPermissionAllDecorator('hg.admin')
625 @HasPermissionAllDecorator('hg.admin')
627 def settings_automation(self):
626 def settings_automation(self):
628 c = self.load_default_context()
627 c = self.load_default_context()
629 c.active = 'automation'
628 c.active = 'automation'
630
629
631 return self._get_template_context(c)
630 return self._get_template_context(c)
632
631
633 @LoginRequired()
632 @LoginRequired()
634 @HasPermissionAllDecorator('hg.admin')
633 @HasPermissionAllDecorator('hg.admin')
635 def settings_labs(self):
634 def settings_labs(self):
636 c = self.load_default_context()
635 c = self.load_default_context()
637 if not c.labs_active:
636 if not c.labs_active:
638 raise HTTPFound(h.route_path('admin_settings'))
637 raise HTTPFound(h.route_path('admin_settings'))
639
638
640 c.active = 'labs'
639 c.active = 'labs'
641 c.lab_settings = _LAB_SETTINGS
640 c.lab_settings = _LAB_SETTINGS
642
641
643 data = render('rhodecode:templates/admin/settings/settings.mako',
642 data = render('rhodecode:templates/admin/settings/settings.mako',
644 self._get_template_context(c), self.request)
643 self._get_template_context(c), self.request)
645 html = formencode.htmlfill.render(
644 html = formencode.htmlfill.render(
646 data,
645 data,
647 defaults=self._form_defaults(),
646 defaults=self._form_defaults(),
648 encoding="UTF-8",
647 encoding="UTF-8",
649 force_defaults=False
648 force_defaults=False
650 )
649 )
651 return Response(html)
650 return Response(html)
652
651
653 @LoginRequired()
652 @LoginRequired()
654 @HasPermissionAllDecorator('hg.admin')
653 @HasPermissionAllDecorator('hg.admin')
655 @CSRFRequired()
654 @CSRFRequired()
656 def settings_labs_update(self):
655 def settings_labs_update(self):
657 _ = self.request.translate
656 _ = self.request.translate
658 c = self.load_default_context()
657 c = self.load_default_context()
659 c.active = 'labs'
658 c.active = 'labs'
660
659
661 application_form = LabsSettingsForm(self.request.translate)()
660 application_form = LabsSettingsForm(self.request.translate)()
662 try:
661 try:
663 form_result = application_form.to_python(dict(self.request.POST))
662 form_result = application_form.to_python(dict(self.request.POST))
664 except formencode.Invalid as errors:
663 except formencode.Invalid as errors:
665 h.flash(
664 h.flash(
666 _("Some form inputs contain invalid data."),
665 _("Some form inputs contain invalid data."),
667 category='error')
666 category='error')
668 data = render('rhodecode:templates/admin/settings/settings.mako',
667 data = render('rhodecode:templates/admin/settings/settings.mako',
669 self._get_template_context(c), self.request)
668 self._get_template_context(c), self.request)
670 html = formencode.htmlfill.render(
669 html = formencode.htmlfill.render(
671 data,
670 data,
672 defaults=errors.value,
671 defaults=errors.value,
673 errors=errors.unpack_errors() or {},
672 errors=errors.unpack_errors() or {},
674 prefix_error=False,
673 prefix_error=False,
675 encoding="UTF-8",
674 encoding="UTF-8",
676 force_defaults=False
675 force_defaults=False
677 )
676 )
678 return Response(html)
677 return Response(html)
679
678
680 try:
679 try:
681 session = Session()
680 session = Session()
682 for setting in _LAB_SETTINGS:
681 for setting in _LAB_SETTINGS:
683 setting_name = setting.key[len('rhodecode_'):]
682 setting_name = setting.key[len('rhodecode_'):]
684 sett = SettingsModel().create_or_update_setting(
683 sett = SettingsModel().create_or_update_setting(
685 setting_name, form_result[setting.key], setting.type)
684 setting_name, form_result[setting.key], setting.type)
686 session.add(sett)
685 session.add(sett)
687
686
688 except Exception:
687 except Exception:
689 log.exception('Exception while updating lab settings')
688 log.exception('Exception while updating lab settings')
690 h.flash(_('Error occurred during updating labs settings'),
689 h.flash(_('Error occurred during updating labs settings'),
691 category='error')
690 category='error')
692 else:
691 else:
693 Session().commit()
692 Session().commit()
694 SettingsModel().invalidate_settings_cache()
693 SettingsModel().invalidate_settings_cache()
695 h.flash(_('Updated Labs settings'), category='success')
694 h.flash(_('Updated Labs settings'), category='success')
696 raise HTTPFound(h.route_path('admin_settings_labs'))
695 raise HTTPFound(h.route_path('admin_settings_labs'))
697
696
698 data = render('rhodecode:templates/admin/settings/settings.mako',
697 data = render('rhodecode:templates/admin/settings/settings.mako',
699 self._get_template_context(c), self.request)
698 self._get_template_context(c), self.request)
700 html = formencode.htmlfill.render(
699 html = formencode.htmlfill.render(
701 data,
700 data,
702 defaults=self._form_defaults(),
701 defaults=self._form_defaults(),
703 encoding="UTF-8",
702 encoding="UTF-8",
704 force_defaults=False
703 force_defaults=False
705 )
704 )
706 return Response(html)
705 return Response(html)
707
706
708
707
709 # :param key: name of the setting including the 'rhodecode_' prefix
708 # :param key: name of the setting including the 'rhodecode_' prefix
710 # :param type: the RhodeCodeSetting type to use.
709 # :param type: the RhodeCodeSetting type to use.
711 # :param group: the i18ned group in which we should dispaly this setting
710 # :param group: the i18ned group in which we should dispaly this setting
712 # :param label: the i18ned label we should display for this setting
711 # :param label: the i18ned label we should display for this setting
713 # :param help: the i18ned help we should dispaly for this setting
712 # :param help: the i18ned help we should dispaly for this setting
714 LabSetting = collections.namedtuple(
713 LabSetting = collections.namedtuple(
715 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
714 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
716
715
717
716
718 # This list has to be kept in sync with the form
717 # This list has to be kept in sync with the form
719 # rhodecode.model.forms.LabsSettingsForm.
718 # rhodecode.model.forms.LabsSettingsForm.
720 _LAB_SETTINGS = [
719 _LAB_SETTINGS = [
721
720
722 ]
721 ]
@@ -1,56 +1,54 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
24 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
27 from rhodecode.lib.auth import (
25 from rhodecode.lib.auth import (
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
26 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29
27
30 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
31
29
32
30
33 class AdminSvnConfigView(BaseAppView):
31 class AdminSvnConfigView(BaseAppView):
34
32
35 @LoginRequired()
33 @LoginRequired()
36 @HasPermissionAllDecorator('hg.admin')
34 @HasPermissionAllDecorator('hg.admin')
37 @CSRFRequired()
35 @CSRFRequired()
38 def vcs_svn_generate_config(self):
36 def vcs_svn_generate_config(self):
39 _ = self.request.translate
37 _ = self.request.translate
40 try:
38 try:
41 file_path = generate_mod_dav_svn_config(self.request.registry)
39 file_path = generate_mod_dav_svn_config(self.request.registry)
42 msg = {
40 msg = {
43 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
41 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
44 'level': 'success',
42 'level': 'success',
45 }
43 }
46 except Exception:
44 except Exception:
47 log.exception(
45 log.exception(
48 'Exception while generating the Apache '
46 'Exception while generating the Apache '
49 'configuration for Subversion.')
47 'configuration for Subversion.')
50 msg = {
48 msg = {
51 'message': _('Failed to generate the Apache configuration for Subversion.'),
49 'message': _('Failed to generate the Apache configuration for Subversion.'),
52 'level': 'error',
50 'level': 'error',
53 }
51 }
54
52
55 data = {'message': msg}
53 data = {'message': msg}
56 return data
54 return data
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,784 +1,782 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import datetime
20 import datetime
23 import string
21 import string
24
22
25 import formencode
23 import formencode
26 import formencode.htmlfill
24 import formencode.htmlfill
27 import peppercorn
25 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29
27
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
29 from rhodecode import forms
32 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
31 from rhodecode.lib import audit_logger
34 from rhodecode.lib import ext_json
32 from rhodecode.lib import ext_json
35 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
36 LoginRequired, NotAnonymous, CSRFRequired,
34 LoginRequired, NotAnonymous, CSRFRequired,
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 from rhodecode.lib.channelstream import (
36 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
37 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.hash_utils import md5_safe
38 from rhodecode.lib.hash_utils import md5_safe
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
42 from rhodecode.model.db import (
45 IntegrityError, or_, in_filter_generator,
43 IntegrityError, or_, in_filter_generator,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
48 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
51 from rhodecode.model.user_group import UserGroupModel
49 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
53
51
54 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
55
53
56
54
57 class MyAccountView(BaseAppView, DataGridAppView):
55 class MyAccountView(BaseAppView, DataGridAppView):
58 ALLOW_SCOPED_TOKENS = False
56 ALLOW_SCOPED_TOKENS = False
59 """
57 """
60 This view has alternative version inside EE, if modified please take a look
58 This view has alternative version inside EE, if modified please take a look
61 in there as well.
59 in there as well.
62 """
60 """
63
61
64 def load_default_context(self):
62 def load_default_context(self):
65 c = self._get_local_tmpl_context()
63 c = self._get_local_tmpl_context()
66 c.user = c.auth_user.get_instance()
64 c.user = c.auth_user.get_instance()
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 return c
66 return c
69
67
70 @LoginRequired()
68 @LoginRequired()
71 @NotAnonymous()
69 @NotAnonymous()
72 def my_account_profile(self):
70 def my_account_profile(self):
73 c = self.load_default_context()
71 c = self.load_default_context()
74 c.active = 'profile'
72 c.active = 'profile'
75 c.extern_type = c.user.extern_type
73 c.extern_type = c.user.extern_type
76 return self._get_template_context(c)
74 return self._get_template_context(c)
77
75
78 @LoginRequired()
76 @LoginRequired()
79 @NotAnonymous()
77 @NotAnonymous()
80 def my_account_edit(self):
78 def my_account_edit(self):
81 c = self.load_default_context()
79 c = self.load_default_context()
82 c.active = 'profile_edit'
80 c.active = 'profile_edit'
83 c.extern_type = c.user.extern_type
81 c.extern_type = c.user.extern_type
84 c.extern_name = c.user.extern_name
82 c.extern_name = c.user.extern_name
85
83
86 schema = user_schema.UserProfileSchema().bind(
84 schema = user_schema.UserProfileSchema().bind(
87 username=c.user.username, user_emails=c.user.emails)
85 username=c.user.username, user_emails=c.user.emails)
88 appstruct = {
86 appstruct = {
89 'username': c.user.username,
87 'username': c.user.username,
90 'email': c.user.email,
88 'email': c.user.email,
91 'firstname': c.user.firstname,
89 'firstname': c.user.firstname,
92 'lastname': c.user.lastname,
90 'lastname': c.user.lastname,
93 'description': c.user.description,
91 'description': c.user.description,
94 }
92 }
95 c.form = forms.RcForm(
93 c.form = forms.RcForm(
96 schema, appstruct=appstruct,
94 schema, appstruct=appstruct,
97 action=h.route_path('my_account_update'),
95 action=h.route_path('my_account_update'),
98 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
99
97
100 return self._get_template_context(c)
98 return self._get_template_context(c)
101
99
102 @LoginRequired()
100 @LoginRequired()
103 @NotAnonymous()
101 @NotAnonymous()
104 @CSRFRequired()
102 @CSRFRequired()
105 def my_account_update(self):
103 def my_account_update(self):
106 _ = self.request.translate
104 _ = self.request.translate
107 c = self.load_default_context()
105 c = self.load_default_context()
108 c.active = 'profile_edit'
106 c.active = 'profile_edit'
109 c.perm_user = c.auth_user
107 c.perm_user = c.auth_user
110 c.extern_type = c.user.extern_type
108 c.extern_type = c.user.extern_type
111 c.extern_name = c.user.extern_name
109 c.extern_name = c.user.extern_name
112
110
113 schema = user_schema.UserProfileSchema().bind(
111 schema = user_schema.UserProfileSchema().bind(
114 username=c.user.username, user_emails=c.user.emails)
112 username=c.user.username, user_emails=c.user.emails)
115 form = forms.RcForm(
113 form = forms.RcForm(
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117
115
118 controls = list(self.request.POST.items())
116 controls = list(self.request.POST.items())
119 try:
117 try:
120 valid_data = form.validate(controls)
118 valid_data = form.validate(controls)
121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
122 'new_password', 'password_confirmation']
120 'new_password', 'password_confirmation']
123 if c.extern_type != "rhodecode":
121 if c.extern_type != "rhodecode":
124 # forbid updating username for external accounts
122 # forbid updating username for external accounts
125 skip_attrs.append('username')
123 skip_attrs.append('username')
126 old_email = c.user.email
124 old_email = c.user.email
127 UserModel().update_user(
125 UserModel().update_user(
128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
129 **valid_data)
127 **valid_data)
130 if old_email != valid_data['email']:
128 if old_email != valid_data['email']:
131 old = UserEmailMap.query() \
129 old = UserEmailMap.query() \
132 .filter(UserEmailMap.user == c.user)\
130 .filter(UserEmailMap.user == c.user)\
133 .filter(UserEmailMap.email == valid_data['email'])\
131 .filter(UserEmailMap.email == valid_data['email'])\
134 .first()
132 .first()
135 old.email = old_email
133 old.email = old_email
136 h.flash(_('Your account was updated successfully'), category='success')
134 h.flash(_('Your account was updated successfully'), category='success')
137 Session().commit()
135 Session().commit()
138 except forms.ValidationFailure as e:
136 except forms.ValidationFailure as e:
139 c.form = e
137 c.form = e
140 return self._get_template_context(c)
138 return self._get_template_context(c)
141 except Exception:
139 except Exception:
142 log.exception("Exception updating user")
140 log.exception("Exception updating user")
143 h.flash(_('Error occurred during update of user'),
141 h.flash(_('Error occurred during update of user'),
144 category='error')
142 category='error')
145 raise HTTPFound(h.route_path('my_account_profile'))
143 raise HTTPFound(h.route_path('my_account_profile'))
146
144
147 @LoginRequired()
145 @LoginRequired()
148 @NotAnonymous()
146 @NotAnonymous()
149 def my_account_password(self):
147 def my_account_password(self):
150 c = self.load_default_context()
148 c = self.load_default_context()
151 c.active = 'password'
149 c.active = 'password'
152 c.extern_type = c.user.extern_type
150 c.extern_type = c.user.extern_type
153
151
154 schema = user_schema.ChangePasswordSchema().bind(
152 schema = user_schema.ChangePasswordSchema().bind(
155 username=c.user.username)
153 username=c.user.username)
156
154
157 form = forms.Form(
155 form = forms.Form(
158 schema,
156 schema,
159 action=h.route_path('my_account_password_update'),
157 action=h.route_path('my_account_password_update'),
160 buttons=(forms.buttons.save, forms.buttons.reset))
158 buttons=(forms.buttons.save, forms.buttons.reset))
161
159
162 c.form = form
160 c.form = form
163 return self._get_template_context(c)
161 return self._get_template_context(c)
164
162
165 @LoginRequired()
163 @LoginRequired()
166 @NotAnonymous()
164 @NotAnonymous()
167 @CSRFRequired()
165 @CSRFRequired()
168 def my_account_password_update(self):
166 def my_account_password_update(self):
169 _ = self.request.translate
167 _ = self.request.translate
170 c = self.load_default_context()
168 c = self.load_default_context()
171 c.active = 'password'
169 c.active = 'password'
172 c.extern_type = c.user.extern_type
170 c.extern_type = c.user.extern_type
173
171
174 schema = user_schema.ChangePasswordSchema().bind(
172 schema = user_schema.ChangePasswordSchema().bind(
175 username=c.user.username)
173 username=c.user.username)
176
174
177 form = forms.Form(
175 form = forms.Form(
178 schema, buttons=(forms.buttons.save, forms.buttons.reset))
176 schema, buttons=(forms.buttons.save, forms.buttons.reset))
179
177
180 if c.extern_type != 'rhodecode':
178 if c.extern_type != 'rhodecode':
181 raise HTTPFound(self.request.route_path('my_account_password'))
179 raise HTTPFound(self.request.route_path('my_account_password'))
182
180
183 controls = list(self.request.POST.items())
181 controls = list(self.request.POST.items())
184 try:
182 try:
185 valid_data = form.validate(controls)
183 valid_data = form.validate(controls)
186 UserModel().update_user(c.user.user_id, **valid_data)
184 UserModel().update_user(c.user.user_id, **valid_data)
187 c.user.update_userdata(force_password_change=False)
185 c.user.update_userdata(force_password_change=False)
188 Session().commit()
186 Session().commit()
189 except forms.ValidationFailure as e:
187 except forms.ValidationFailure as e:
190 c.form = e
188 c.form = e
191 return self._get_template_context(c)
189 return self._get_template_context(c)
192
190
193 except Exception:
191 except Exception:
194 log.exception("Exception updating password")
192 log.exception("Exception updating password")
195 h.flash(_('Error occurred during update of user password'),
193 h.flash(_('Error occurred during update of user password'),
196 category='error')
194 category='error')
197 else:
195 else:
198 instance = c.auth_user.get_instance()
196 instance = c.auth_user.get_instance()
199 self.session.setdefault('rhodecode_user', {}).update(
197 self.session.setdefault('rhodecode_user', {}).update(
200 {'password': md5_safe(instance.password)})
198 {'password': md5_safe(instance.password)})
201 self.session.save()
199 self.session.save()
202 h.flash(_("Successfully updated password"), category='success')
200 h.flash(_("Successfully updated password"), category='success')
203
201
204 raise HTTPFound(self.request.route_path('my_account_password'))
202 raise HTTPFound(self.request.route_path('my_account_password'))
205
203
206 @LoginRequired()
204 @LoginRequired()
207 @NotAnonymous()
205 @NotAnonymous()
208 def my_account_auth_tokens(self):
206 def my_account_auth_tokens(self):
209 _ = self.request.translate
207 _ = self.request.translate
210
208
211 c = self.load_default_context()
209 c = self.load_default_context()
212 c.active = 'auth_tokens'
210 c.active = 'auth_tokens'
213 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
211 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
214 c.role_values = [
212 c.role_values = [
215 (x, AuthTokenModel.cls._get_role_name(x))
213 (x, AuthTokenModel.cls._get_role_name(x))
216 for x in AuthTokenModel.cls.ROLES]
214 for x in AuthTokenModel.cls.ROLES]
217 c.role_options = [(c.role_values, _("Role"))]
215 c.role_options = [(c.role_values, _("Role"))]
218 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
216 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
219 c.user.user_id, show_expired=True)
217 c.user.user_id, show_expired=True)
220 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
218 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
221 return self._get_template_context(c)
219 return self._get_template_context(c)
222
220
223 @LoginRequired()
221 @LoginRequired()
224 @NotAnonymous()
222 @NotAnonymous()
225 @CSRFRequired()
223 @CSRFRequired()
226 def my_account_auth_tokens_view(self):
224 def my_account_auth_tokens_view(self):
227 _ = self.request.translate
225 _ = self.request.translate
228 c = self.load_default_context()
226 c = self.load_default_context()
229
227
230 auth_token_id = self.request.POST.get('auth_token_id')
228 auth_token_id = self.request.POST.get('auth_token_id')
231
229
232 if auth_token_id:
230 if auth_token_id:
233 token = UserApiKeys.get_or_404(auth_token_id)
231 token = UserApiKeys.get_or_404(auth_token_id)
234 if token.user.user_id != c.user.user_id:
232 if token.user.user_id != c.user.user_id:
235 raise HTTPNotFound()
233 raise HTTPNotFound()
236
234
237 return {
235 return {
238 'auth_token': token.api_key
236 'auth_token': token.api_key
239 }
237 }
240
238
241 def maybe_attach_token_scope(self, token):
239 def maybe_attach_token_scope(self, token):
242 # implemented in EE edition
240 # implemented in EE edition
243 pass
241 pass
244
242
245 @LoginRequired()
243 @LoginRequired()
246 @NotAnonymous()
244 @NotAnonymous()
247 @CSRFRequired()
245 @CSRFRequired()
248 def my_account_auth_tokens_add(self):
246 def my_account_auth_tokens_add(self):
249 _ = self.request.translate
247 _ = self.request.translate
250 c = self.load_default_context()
248 c = self.load_default_context()
251
249
252 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
250 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
253 description = self.request.POST.get('description')
251 description = self.request.POST.get('description')
254 role = self.request.POST.get('role')
252 role = self.request.POST.get('role')
255
253
256 token = UserModel().add_auth_token(
254 token = UserModel().add_auth_token(
257 user=c.user.user_id,
255 user=c.user.user_id,
258 lifetime_minutes=lifetime, role=role, description=description,
256 lifetime_minutes=lifetime, role=role, description=description,
259 scope_callback=self.maybe_attach_token_scope)
257 scope_callback=self.maybe_attach_token_scope)
260 token_data = token.get_api_data()
258 token_data = token.get_api_data()
261
259
262 audit_logger.store_web(
260 audit_logger.store_web(
263 'user.edit.token.add', action_data={
261 'user.edit.token.add', action_data={
264 'data': {'token': token_data, 'user': 'self'}},
262 'data': {'token': token_data, 'user': 'self'}},
265 user=self._rhodecode_user, )
263 user=self._rhodecode_user, )
266 Session().commit()
264 Session().commit()
267
265
268 h.flash(_("Auth token successfully created"), category='success')
266 h.flash(_("Auth token successfully created"), category='success')
269 return HTTPFound(h.route_path('my_account_auth_tokens'))
267 return HTTPFound(h.route_path('my_account_auth_tokens'))
270
268
271 @LoginRequired()
269 @LoginRequired()
272 @NotAnonymous()
270 @NotAnonymous()
273 @CSRFRequired()
271 @CSRFRequired()
274 def my_account_auth_tokens_delete(self):
272 def my_account_auth_tokens_delete(self):
275 _ = self.request.translate
273 _ = self.request.translate
276 c = self.load_default_context()
274 c = self.load_default_context()
277
275
278 del_auth_token = self.request.POST.get('del_auth_token')
276 del_auth_token = self.request.POST.get('del_auth_token')
279
277
280 if del_auth_token:
278 if del_auth_token:
281 token = UserApiKeys.get_or_404(del_auth_token)
279 token = UserApiKeys.get_or_404(del_auth_token)
282 token_data = token.get_api_data()
280 token_data = token.get_api_data()
283
281
284 AuthTokenModel().delete(del_auth_token, c.user.user_id)
282 AuthTokenModel().delete(del_auth_token, c.user.user_id)
285 audit_logger.store_web(
283 audit_logger.store_web(
286 'user.edit.token.delete', action_data={
284 'user.edit.token.delete', action_data={
287 'data': {'token': token_data, 'user': 'self'}},
285 'data': {'token': token_data, 'user': 'self'}},
288 user=self._rhodecode_user,)
286 user=self._rhodecode_user,)
289 Session().commit()
287 Session().commit()
290 h.flash(_("Auth token successfully deleted"), category='success')
288 h.flash(_("Auth token successfully deleted"), category='success')
291
289
292 return HTTPFound(h.route_path('my_account_auth_tokens'))
290 return HTTPFound(h.route_path('my_account_auth_tokens'))
293
291
294 @LoginRequired()
292 @LoginRequired()
295 @NotAnonymous()
293 @NotAnonymous()
296 def my_account_emails(self):
294 def my_account_emails(self):
297 _ = self.request.translate
295 _ = self.request.translate
298
296
299 c = self.load_default_context()
297 c = self.load_default_context()
300 c.active = 'emails'
298 c.active = 'emails'
301
299
302 c.user_email_map = UserEmailMap.query()\
300 c.user_email_map = UserEmailMap.query()\
303 .filter(UserEmailMap.user == c.user).all()
301 .filter(UserEmailMap.user == c.user).all()
304
302
305 schema = user_schema.AddEmailSchema().bind(
303 schema = user_schema.AddEmailSchema().bind(
306 username=c.user.username, user_emails=c.user.emails)
304 username=c.user.username, user_emails=c.user.emails)
307
305
308 form = forms.RcForm(schema,
306 form = forms.RcForm(schema,
309 action=h.route_path('my_account_emails_add'),
307 action=h.route_path('my_account_emails_add'),
310 buttons=(forms.buttons.save, forms.buttons.reset))
308 buttons=(forms.buttons.save, forms.buttons.reset))
311
309
312 c.form = form
310 c.form = form
313 return self._get_template_context(c)
311 return self._get_template_context(c)
314
312
315 @LoginRequired()
313 @LoginRequired()
316 @NotAnonymous()
314 @NotAnonymous()
317 @CSRFRequired()
315 @CSRFRequired()
318 def my_account_emails_add(self):
316 def my_account_emails_add(self):
319 _ = self.request.translate
317 _ = self.request.translate
320 c = self.load_default_context()
318 c = self.load_default_context()
321 c.active = 'emails'
319 c.active = 'emails'
322
320
323 schema = user_schema.AddEmailSchema().bind(
321 schema = user_schema.AddEmailSchema().bind(
324 username=c.user.username, user_emails=c.user.emails)
322 username=c.user.username, user_emails=c.user.emails)
325
323
326 form = forms.RcForm(
324 form = forms.RcForm(
327 schema, action=h.route_path('my_account_emails_add'),
325 schema, action=h.route_path('my_account_emails_add'),
328 buttons=(forms.buttons.save, forms.buttons.reset))
326 buttons=(forms.buttons.save, forms.buttons.reset))
329
327
330 controls = list(self.request.POST.items())
328 controls = list(self.request.POST.items())
331 try:
329 try:
332 valid_data = form.validate(controls)
330 valid_data = form.validate(controls)
333 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
331 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
334 audit_logger.store_web(
332 audit_logger.store_web(
335 'user.edit.email.add', action_data={
333 'user.edit.email.add', action_data={
336 'data': {'email': valid_data['email'], 'user': 'self'}},
334 'data': {'email': valid_data['email'], 'user': 'self'}},
337 user=self._rhodecode_user,)
335 user=self._rhodecode_user,)
338 Session().commit()
336 Session().commit()
339 except formencode.Invalid as error:
337 except formencode.Invalid as error:
340 h.flash(h.escape(error.error_dict['email']), category='error')
338 h.flash(h.escape(error.error_dict['email']), category='error')
341 except forms.ValidationFailure as e:
339 except forms.ValidationFailure as e:
342 c.user_email_map = UserEmailMap.query() \
340 c.user_email_map = UserEmailMap.query() \
343 .filter(UserEmailMap.user == c.user).all()
341 .filter(UserEmailMap.user == c.user).all()
344 c.form = e
342 c.form = e
345 return self._get_template_context(c)
343 return self._get_template_context(c)
346 except Exception:
344 except Exception:
347 log.exception("Exception adding email")
345 log.exception("Exception adding email")
348 h.flash(_('Error occurred during adding email'),
346 h.flash(_('Error occurred during adding email'),
349 category='error')
347 category='error')
350 else:
348 else:
351 h.flash(_("Successfully added email"), category='success')
349 h.flash(_("Successfully added email"), category='success')
352
350
353 raise HTTPFound(self.request.route_path('my_account_emails'))
351 raise HTTPFound(self.request.route_path('my_account_emails'))
354
352
355 @LoginRequired()
353 @LoginRequired()
356 @NotAnonymous()
354 @NotAnonymous()
357 @CSRFRequired()
355 @CSRFRequired()
358 def my_account_emails_delete(self):
356 def my_account_emails_delete(self):
359 _ = self.request.translate
357 _ = self.request.translate
360 c = self.load_default_context()
358 c = self.load_default_context()
361
359
362 del_email_id = self.request.POST.get('del_email_id')
360 del_email_id = self.request.POST.get('del_email_id')
363 if del_email_id:
361 if del_email_id:
364 email = UserEmailMap.get_or_404(del_email_id).email
362 email = UserEmailMap.get_or_404(del_email_id).email
365 UserModel().delete_extra_email(c.user.user_id, del_email_id)
363 UserModel().delete_extra_email(c.user.user_id, del_email_id)
366 audit_logger.store_web(
364 audit_logger.store_web(
367 'user.edit.email.delete', action_data={
365 'user.edit.email.delete', action_data={
368 'data': {'email': email, 'user': 'self'}},
366 'data': {'email': email, 'user': 'self'}},
369 user=self._rhodecode_user,)
367 user=self._rhodecode_user,)
370 Session().commit()
368 Session().commit()
371 h.flash(_("Email successfully deleted"),
369 h.flash(_("Email successfully deleted"),
372 category='success')
370 category='success')
373 return HTTPFound(h.route_path('my_account_emails'))
371 return HTTPFound(h.route_path('my_account_emails'))
374
372
375 @LoginRequired()
373 @LoginRequired()
376 @NotAnonymous()
374 @NotAnonymous()
377 @CSRFRequired()
375 @CSRFRequired()
378 def my_account_notifications_test_channelstream(self):
376 def my_account_notifications_test_channelstream(self):
379 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
377 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
380 self._rhodecode_user.username, datetime.datetime.now())
378 self._rhodecode_user.username, datetime.datetime.now())
381 payload = {
379 payload = {
382 # 'channel': 'broadcast',
380 # 'channel': 'broadcast',
383 'type': 'message',
381 'type': 'message',
384 'timestamp': datetime.datetime.utcnow(),
382 'timestamp': datetime.datetime.utcnow(),
385 'user': 'system',
383 'user': 'system',
386 'pm_users': [self._rhodecode_user.username],
384 'pm_users': [self._rhodecode_user.username],
387 'message': {
385 'message': {
388 'message': message,
386 'message': message,
389 'level': 'info',
387 'level': 'info',
390 'topic': '/notifications'
388 'topic': '/notifications'
391 }
389 }
392 }
390 }
393
391
394 registry = self.request.registry
392 registry = self.request.registry
395 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
393 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
396 channelstream_config = rhodecode_plugins.get('channelstream', {})
394 channelstream_config = rhodecode_plugins.get('channelstream', {})
397
395
398 try:
396 try:
399 channelstream_request(channelstream_config, [payload], '/message')
397 channelstream_request(channelstream_config, [payload], '/message')
400 except ChannelstreamException as e:
398 except ChannelstreamException as e:
401 log.exception('Failed to send channelstream data')
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 return {"response": 'Channelstream data sent. '
401 return {"response": 'Channelstream data sent. '
404 'You should see a new live message now.'}
402 'You should see a new live message now.'}
405
403
406 def _load_my_repos_data(self, watched=False):
404 def _load_my_repos_data(self, watched=False):
407
405
408 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
406 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
409
407
410 if watched:
408 if watched:
411 # repos user watch
409 # repos user watch
412 repo_list = Session().query(
410 repo_list = Session().query(
413 Repository
411 Repository
414 ) \
412 ) \
415 .join(
413 .join(
416 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
414 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
417 ) \
415 ) \
418 .filter(
416 .filter(
419 UserFollowing.user_id == self._rhodecode_user.user_id
417 UserFollowing.user_id == self._rhodecode_user.user_id
420 ) \
418 ) \
421 .filter(or_(
419 .filter(or_(
422 # generate multiple IN to fix limitation problems
420 # generate multiple IN to fix limitation problems
423 *in_filter_generator(Repository.repo_id, allowed_ids))
421 *in_filter_generator(Repository.repo_id, allowed_ids))
424 ) \
422 ) \
425 .order_by(Repository.repo_name) \
423 .order_by(Repository.repo_name) \
426 .all()
424 .all()
427
425
428 else:
426 else:
429 # repos user is owner of
427 # repos user is owner of
430 repo_list = Session().query(
428 repo_list = Session().query(
431 Repository
429 Repository
432 ) \
430 ) \
433 .filter(
431 .filter(
434 Repository.user_id == self._rhodecode_user.user_id
432 Repository.user_id == self._rhodecode_user.user_id
435 ) \
433 ) \
436 .filter(or_(
434 .filter(or_(
437 # generate multiple IN to fix limitation problems
435 # generate multiple IN to fix limitation problems
438 *in_filter_generator(Repository.repo_id, allowed_ids))
436 *in_filter_generator(Repository.repo_id, allowed_ids))
439 ) \
437 ) \
440 .order_by(Repository.repo_name) \
438 .order_by(Repository.repo_name) \
441 .all()
439 .all()
442
440
443 _render = self.request.get_partial_renderer(
441 _render = self.request.get_partial_renderer(
444 'rhodecode:templates/data_table/_dt_elements.mako')
442 'rhodecode:templates/data_table/_dt_elements.mako')
445
443
446 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
444 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
447 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
445 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
448 short_name=False, admin=False)
446 short_name=False, admin=False)
449
447
450 repos_data = []
448 repos_data = []
451 for repo in repo_list:
449 for repo in repo_list:
452 row = {
450 row = {
453 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
451 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
454 repo.private, repo.archived, repo.fork),
452 repo.private, repo.archived, repo.fork),
455 "name_raw": repo.repo_name.lower(),
453 "name_raw": repo.repo_name.lower(),
456 }
454 }
457
455
458 repos_data.append(row)
456 repos_data.append(row)
459
457
460 # json used to render the grid
458 # json used to render the grid
461 return ext_json.str_json(repos_data)
459 return ext_json.str_json(repos_data)
462
460
463 @LoginRequired()
461 @LoginRequired()
464 @NotAnonymous()
462 @NotAnonymous()
465 def my_account_repos(self):
463 def my_account_repos(self):
466 c = self.load_default_context()
464 c = self.load_default_context()
467 c.active = 'repos'
465 c.active = 'repos'
468
466
469 # json used to render the grid
467 # json used to render the grid
470 c.data = self._load_my_repos_data()
468 c.data = self._load_my_repos_data()
471 return self._get_template_context(c)
469 return self._get_template_context(c)
472
470
473 @LoginRequired()
471 @LoginRequired()
474 @NotAnonymous()
472 @NotAnonymous()
475 def my_account_watched(self):
473 def my_account_watched(self):
476 c = self.load_default_context()
474 c = self.load_default_context()
477 c.active = 'watched'
475 c.active = 'watched'
478
476
479 # json used to render the grid
477 # json used to render the grid
480 c.data = self._load_my_repos_data(watched=True)
478 c.data = self._load_my_repos_data(watched=True)
481 return self._get_template_context(c)
479 return self._get_template_context(c)
482
480
483 @LoginRequired()
481 @LoginRequired()
484 @NotAnonymous()
482 @NotAnonymous()
485 def my_account_bookmarks(self):
483 def my_account_bookmarks(self):
486 c = self.load_default_context()
484 c = self.load_default_context()
487 c.active = 'bookmarks'
485 c.active = 'bookmarks'
488 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
486 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
489 self._rhodecode_db_user.user_id, cache=False)
487 self._rhodecode_db_user.user_id, cache=False)
490 return self._get_template_context(c)
488 return self._get_template_context(c)
491
489
492 def _process_bookmark_entry(self, entry, user_id):
490 def _process_bookmark_entry(self, entry, user_id):
493 position = safe_int(entry.get('position'))
491 position = safe_int(entry.get('position'))
494 cur_position = safe_int(entry.get('cur_position'))
492 cur_position = safe_int(entry.get('cur_position'))
495 if position is None:
493 if position is None:
496 return
494 return
497
495
498 # check if this is an existing entry
496 # check if this is an existing entry
499 is_new = False
497 is_new = False
500 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
498 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
501
499
502 if db_entry and str2bool(entry.get('remove')):
500 if db_entry and str2bool(entry.get('remove')):
503 log.debug('Marked bookmark %s for deletion', db_entry)
501 log.debug('Marked bookmark %s for deletion', db_entry)
504 Session().delete(db_entry)
502 Session().delete(db_entry)
505 return
503 return
506
504
507 if not db_entry:
505 if not db_entry:
508 # new
506 # new
509 db_entry = UserBookmark()
507 db_entry = UserBookmark()
510 is_new = True
508 is_new = True
511
509
512 should_save = False
510 should_save = False
513 default_redirect_url = ''
511 default_redirect_url = ''
514
512
515 # save repo
513 # save repo
516 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
514 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
517 repo = Repository.get(entry['bookmark_repo'])
515 repo = Repository.get(entry['bookmark_repo'])
518 perm_check = HasRepoPermissionAny(
516 perm_check = HasRepoPermissionAny(
519 'repository.read', 'repository.write', 'repository.admin')
517 'repository.read', 'repository.write', 'repository.admin')
520 if repo and perm_check(repo_name=repo.repo_name):
518 if repo and perm_check(repo_name=repo.repo_name):
521 db_entry.repository = repo
519 db_entry.repository = repo
522 should_save = True
520 should_save = True
523 default_redirect_url = '${repo_url}'
521 default_redirect_url = '${repo_url}'
524 # save repo group
522 # save repo group
525 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
523 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
526 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
524 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
527 perm_check = HasRepoGroupPermissionAny(
525 perm_check = HasRepoGroupPermissionAny(
528 'group.read', 'group.write', 'group.admin')
526 'group.read', 'group.write', 'group.admin')
529
527
530 if repo_group and perm_check(group_name=repo_group.group_name):
528 if repo_group and perm_check(group_name=repo_group.group_name):
531 db_entry.repository_group = repo_group
529 db_entry.repository_group = repo_group
532 should_save = True
530 should_save = True
533 default_redirect_url = '${repo_group_url}'
531 default_redirect_url = '${repo_group_url}'
534 # save generic info
532 # save generic info
535 elif entry.get('title') and entry.get('redirect_url'):
533 elif entry.get('title') and entry.get('redirect_url'):
536 should_save = True
534 should_save = True
537
535
538 if should_save:
536 if should_save:
539 # mark user and position
537 # mark user and position
540 db_entry.user_id = user_id
538 db_entry.user_id = user_id
541 db_entry.position = position
539 db_entry.position = position
542 db_entry.title = entry.get('title')
540 db_entry.title = entry.get('title')
543 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
541 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
544 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
542 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
545
543
546 Session().add(db_entry)
544 Session().add(db_entry)
547
545
548 @LoginRequired()
546 @LoginRequired()
549 @NotAnonymous()
547 @NotAnonymous()
550 @CSRFRequired()
548 @CSRFRequired()
551 def my_account_bookmarks_update(self):
549 def my_account_bookmarks_update(self):
552 _ = self.request.translate
550 _ = self.request.translate
553 c = self.load_default_context()
551 c = self.load_default_context()
554 c.active = 'bookmarks'
552 c.active = 'bookmarks'
555
553
556 controls = peppercorn.parse(self.request.POST.items())
554 controls = peppercorn.parse(self.request.POST.items())
557 user_id = c.user.user_id
555 user_id = c.user.user_id
558
556
559 # validate positions
557 # validate positions
560 positions = {}
558 positions = {}
561 for entry in controls.get('bookmarks', []):
559 for entry in controls.get('bookmarks', []):
562 position = safe_int(entry['position'])
560 position = safe_int(entry['position'])
563 if position is None:
561 if position is None:
564 continue
562 continue
565
563
566 if position in positions:
564 if position in positions:
567 h.flash(_("Position {} is defined twice. "
565 h.flash(_("Position {} is defined twice. "
568 "Please correct this error.").format(position), category='error')
566 "Please correct this error.").format(position), category='error')
569 return HTTPFound(h.route_path('my_account_bookmarks'))
567 return HTTPFound(h.route_path('my_account_bookmarks'))
570
568
571 entry['position'] = position
569 entry['position'] = position
572 entry['cur_position'] = safe_int(entry.get('cur_position'))
570 entry['cur_position'] = safe_int(entry.get('cur_position'))
573 positions[position] = entry
571 positions[position] = entry
574
572
575 try:
573 try:
576 for entry in positions.values():
574 for entry in positions.values():
577 self._process_bookmark_entry(entry, user_id)
575 self._process_bookmark_entry(entry, user_id)
578
576
579 Session().commit()
577 Session().commit()
580 h.flash(_("Update Bookmarks"), category='success')
578 h.flash(_("Update Bookmarks"), category='success')
581 except IntegrityError:
579 except IntegrityError:
582 h.flash(_("Failed to update bookmarks. "
580 h.flash(_("Failed to update bookmarks. "
583 "Make sure an unique position is used."), category='error')
581 "Make sure an unique position is used."), category='error')
584
582
585 return HTTPFound(h.route_path('my_account_bookmarks'))
583 return HTTPFound(h.route_path('my_account_bookmarks'))
586
584
587 @LoginRequired()
585 @LoginRequired()
588 @NotAnonymous()
586 @NotAnonymous()
589 def my_account_goto_bookmark(self):
587 def my_account_goto_bookmark(self):
590
588
591 bookmark_id = self.request.matchdict['bookmark_id']
589 bookmark_id = self.request.matchdict['bookmark_id']
592 user_bookmark = UserBookmark().query()\
590 user_bookmark = UserBookmark().query()\
593 .filter(UserBookmark.user_id == self.request.user.user_id) \
591 .filter(UserBookmark.user_id == self.request.user.user_id) \
594 .filter(UserBookmark.position == bookmark_id).scalar()
592 .filter(UserBookmark.position == bookmark_id).scalar()
595
593
596 redirect_url = h.route_path('my_account_bookmarks')
594 redirect_url = h.route_path('my_account_bookmarks')
597 if not user_bookmark:
595 if not user_bookmark:
598 raise HTTPFound(redirect_url)
596 raise HTTPFound(redirect_url)
599
597
600 # repository set
598 # repository set
601 if user_bookmark.repository:
599 if user_bookmark.repository:
602 repo_name = user_bookmark.repository.repo_name
600 repo_name = user_bookmark.repository.repo_name
603 base_redirect_url = h.route_path(
601 base_redirect_url = h.route_path(
604 'repo_summary', repo_name=repo_name)
602 'repo_summary', repo_name=repo_name)
605 if user_bookmark.redirect_url and \
603 if user_bookmark.redirect_url and \
606 '${repo_url}' in user_bookmark.redirect_url:
604 '${repo_url}' in user_bookmark.redirect_url:
607 redirect_url = string.Template(user_bookmark.redirect_url)\
605 redirect_url = string.Template(user_bookmark.redirect_url)\
608 .safe_substitute({'repo_url': base_redirect_url})
606 .safe_substitute({'repo_url': base_redirect_url})
609 else:
607 else:
610 redirect_url = base_redirect_url
608 redirect_url = base_redirect_url
611 # repository group set
609 # repository group set
612 elif user_bookmark.repository_group:
610 elif user_bookmark.repository_group:
613 repo_group_name = user_bookmark.repository_group.group_name
611 repo_group_name = user_bookmark.repository_group.group_name
614 base_redirect_url = h.route_path(
612 base_redirect_url = h.route_path(
615 'repo_group_home', repo_group_name=repo_group_name)
613 'repo_group_home', repo_group_name=repo_group_name)
616 if user_bookmark.redirect_url and \
614 if user_bookmark.redirect_url and \
617 '${repo_group_url}' in user_bookmark.redirect_url:
615 '${repo_group_url}' in user_bookmark.redirect_url:
618 redirect_url = string.Template(user_bookmark.redirect_url)\
616 redirect_url = string.Template(user_bookmark.redirect_url)\
619 .safe_substitute({'repo_group_url': base_redirect_url})
617 .safe_substitute({'repo_group_url': base_redirect_url})
620 else:
618 else:
621 redirect_url = base_redirect_url
619 redirect_url = base_redirect_url
622 # custom URL set
620 # custom URL set
623 elif user_bookmark.redirect_url:
621 elif user_bookmark.redirect_url:
624 server_url = h.route_url('home').rstrip('/')
622 server_url = h.route_url('home').rstrip('/')
625 redirect_url = string.Template(user_bookmark.redirect_url) \
623 redirect_url = string.Template(user_bookmark.redirect_url) \
626 .safe_substitute({'server_url': server_url})
624 .safe_substitute({'server_url': server_url})
627
625
628 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
626 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
629 raise HTTPFound(redirect_url)
627 raise HTTPFound(redirect_url)
630
628
631 @LoginRequired()
629 @LoginRequired()
632 @NotAnonymous()
630 @NotAnonymous()
633 def my_account_perms(self):
631 def my_account_perms(self):
634 c = self.load_default_context()
632 c = self.load_default_context()
635 c.active = 'perms'
633 c.active = 'perms'
636
634
637 c.perm_user = c.auth_user
635 c.perm_user = c.auth_user
638 return self._get_template_context(c)
636 return self._get_template_context(c)
639
637
640 @LoginRequired()
638 @LoginRequired()
641 @NotAnonymous()
639 @NotAnonymous()
642 def my_notifications(self):
640 def my_notifications(self):
643 c = self.load_default_context()
641 c = self.load_default_context()
644 c.active = 'notifications'
642 c.active = 'notifications'
645
643
646 return self._get_template_context(c)
644 return self._get_template_context(c)
647
645
648 @LoginRequired()
646 @LoginRequired()
649 @NotAnonymous()
647 @NotAnonymous()
650 @CSRFRequired()
648 @CSRFRequired()
651 def my_notifications_toggle_visibility(self):
649 def my_notifications_toggle_visibility(self):
652 user = self._rhodecode_db_user
650 user = self._rhodecode_db_user
653 new_status = not user.user_data.get('notification_status', True)
651 new_status = not user.user_data.get('notification_status', True)
654 user.update_userdata(notification_status=new_status)
652 user.update_userdata(notification_status=new_status)
655 Session().commit()
653 Session().commit()
656 return user.user_data['notification_status']
654 return user.user_data['notification_status']
657
655
658 def _get_pull_requests_list(self, statuses, filter_type=None):
656 def _get_pull_requests_list(self, statuses, filter_type=None):
659 draw, start, limit = self._extract_chunk(self.request)
657 draw, start, limit = self._extract_chunk(self.request)
660 search_q, order_by, order_dir = self._extract_ordering(self.request)
658 search_q, order_by, order_dir = self._extract_ordering(self.request)
661
659
662 _render = self.request.get_partial_renderer(
660 _render = self.request.get_partial_renderer(
663 'rhodecode:templates/data_table/_dt_elements.mako')
661 'rhodecode:templates/data_table/_dt_elements.mako')
664
662
665 if filter_type == 'awaiting_my_review':
663 if filter_type == 'awaiting_my_review':
666 pull_requests = PullRequestModel().get_im_participating_in_for_review(
664 pull_requests = PullRequestModel().get_im_participating_in_for_review(
667 user_id=self._rhodecode_user.user_id,
665 user_id=self._rhodecode_user.user_id,
668 statuses=statuses, query=search_q,
666 statuses=statuses, query=search_q,
669 offset=start, length=limit, order_by=order_by,
667 offset=start, length=limit, order_by=order_by,
670 order_dir=order_dir)
668 order_dir=order_dir)
671
669
672 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
670 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
673 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
674 else:
672 else:
675 pull_requests = PullRequestModel().get_im_participating_in(
673 pull_requests = PullRequestModel().get_im_participating_in(
676 user_id=self._rhodecode_user.user_id,
674 user_id=self._rhodecode_user.user_id,
677 statuses=statuses, query=search_q,
675 statuses=statuses, query=search_q,
678 offset=start, length=limit, order_by=order_by,
676 offset=start, length=limit, order_by=order_by,
679 order_dir=order_dir)
677 order_dir=order_dir)
680
678
681 pull_requests_total_count = PullRequestModel().count_im_participating_in(
679 pull_requests_total_count = PullRequestModel().count_im_participating_in(
682 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
680 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
683
681
684 data = []
682 data = []
685 comments_model = CommentsModel()
683 comments_model = CommentsModel()
686 for pr in pull_requests:
684 for pr in pull_requests:
687 repo_id = pr.target_repo_id
685 repo_id = pr.target_repo_id
688 comments_count = comments_model.get_all_comments(
686 comments_count = comments_model.get_all_comments(
689 repo_id, pull_request=pr, include_drafts=False, count_only=True)
687 repo_id, pull_request=pr, include_drafts=False, count_only=True)
690 owned = pr.user_id == self._rhodecode_user.user_id
688 owned = pr.user_id == self._rhodecode_user.user_id
691
689
692 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
690 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
693 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
691 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
694 if review_statuses and review_statuses[4]:
692 if review_statuses and review_statuses[4]:
695 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
693 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
696 my_review_status = statuses[0][1].status
694 my_review_status = statuses[0][1].status
697
695
698 data.append({
696 data.append({
699 'target_repo': _render('pullrequest_target_repo',
697 'target_repo': _render('pullrequest_target_repo',
700 pr.target_repo.repo_name),
698 pr.target_repo.repo_name),
701 'name': _render('pullrequest_name',
699 'name': _render('pullrequest_name',
702 pr.pull_request_id, pr.pull_request_state,
700 pr.pull_request_id, pr.pull_request_state,
703 pr.work_in_progress, pr.target_repo.repo_name,
701 pr.work_in_progress, pr.target_repo.repo_name,
704 short=True),
702 short=True),
705 'name_raw': pr.pull_request_id,
703 'name_raw': pr.pull_request_id,
706 'status': _render('pullrequest_status',
704 'status': _render('pullrequest_status',
707 pr.calculated_review_status()),
705 pr.calculated_review_status()),
708 'my_status': _render('pullrequest_status',
706 'my_status': _render('pullrequest_status',
709 my_review_status),
707 my_review_status),
710 'title': _render('pullrequest_title', pr.title, pr.description),
708 'title': _render('pullrequest_title', pr.title, pr.description),
711 'description': h.escape(pr.description),
709 'description': h.escape(pr.description),
712 'updated_on': _render('pullrequest_updated_on',
710 'updated_on': _render('pullrequest_updated_on',
713 h.datetime_to_time(pr.updated_on),
711 h.datetime_to_time(pr.updated_on),
714 pr.versions_count),
712 pr.versions_count),
715 'updated_on_raw': h.datetime_to_time(pr.updated_on),
713 'updated_on_raw': h.datetime_to_time(pr.updated_on),
716 'created_on': _render('pullrequest_updated_on',
714 'created_on': _render('pullrequest_updated_on',
717 h.datetime_to_time(pr.created_on)),
715 h.datetime_to_time(pr.created_on)),
718 'created_on_raw': h.datetime_to_time(pr.created_on),
716 'created_on_raw': h.datetime_to_time(pr.created_on),
719 'state': pr.pull_request_state,
717 'state': pr.pull_request_state,
720 'author': _render('pullrequest_author',
718 'author': _render('pullrequest_author',
721 pr.author.full_contact, ),
719 pr.author.full_contact, ),
722 'author_raw': pr.author.full_name,
720 'author_raw': pr.author.full_name,
723 'comments': _render('pullrequest_comments', comments_count),
721 'comments': _render('pullrequest_comments', comments_count),
724 'comments_raw': comments_count,
722 'comments_raw': comments_count,
725 'closed': pr.is_closed(),
723 'closed': pr.is_closed(),
726 'owned': owned
724 'owned': owned
727 })
725 })
728
726
729 # json used to render the grid
727 # json used to render the grid
730 data = ({
728 data = ({
731 'draw': draw,
729 'draw': draw,
732 'data': data,
730 'data': data,
733 'recordsTotal': pull_requests_total_count,
731 'recordsTotal': pull_requests_total_count,
734 'recordsFiltered': pull_requests_total_count,
732 'recordsFiltered': pull_requests_total_count,
735 })
733 })
736 return data
734 return data
737
735
738 @LoginRequired()
736 @LoginRequired()
739 @NotAnonymous()
737 @NotAnonymous()
740 def my_account_pullrequests(self):
738 def my_account_pullrequests(self):
741 c = self.load_default_context()
739 c = self.load_default_context()
742 c.active = 'pullrequests'
740 c.active = 'pullrequests'
743 req_get = self.request.GET
741 req_get = self.request.GET
744
742
745 c.closed = str2bool(req_get.get('closed'))
743 c.closed = str2bool(req_get.get('closed'))
746 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
744 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
747
745
748 c.selected_filter = 'all'
746 c.selected_filter = 'all'
749 if c.closed:
747 if c.closed:
750 c.selected_filter = 'all_closed'
748 c.selected_filter = 'all_closed'
751 if c.awaiting_my_review:
749 if c.awaiting_my_review:
752 c.selected_filter = 'awaiting_my_review'
750 c.selected_filter = 'awaiting_my_review'
753
751
754 return self._get_template_context(c)
752 return self._get_template_context(c)
755
753
756 @LoginRequired()
754 @LoginRequired()
757 @NotAnonymous()
755 @NotAnonymous()
758 def my_account_pullrequests_data(self):
756 def my_account_pullrequests_data(self):
759 self.load_default_context()
757 self.load_default_context()
760 req_get = self.request.GET
758 req_get = self.request.GET
761
759
762 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
760 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
763 closed = str2bool(req_get.get('closed'))
761 closed = str2bool(req_get.get('closed'))
764
762
765 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
763 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
766 if closed:
764 if closed:
767 statuses += [PullRequest.STATUS_CLOSED]
765 statuses += [PullRequest.STATUS_CLOSED]
768
766
769 filter_type = \
767 filter_type = \
770 'awaiting_my_review' if awaiting_my_review \
768 'awaiting_my_review' if awaiting_my_review \
771 else None
769 else None
772
770
773 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
771 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
774 return data
772 return data
775
773
776 @LoginRequired()
774 @LoginRequired()
777 @NotAnonymous()
775 @NotAnonymous()
778 def my_account_user_group_membership(self):
776 def my_account_user_group_membership(self):
779 c = self.load_default_context()
777 c = self.load_default_context()
780 c.active = 'user_group_membership'
778 c.active = 'user_group_membership'
781 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
779 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
782 for group in self._rhodecode_db_user.group_member]
780 for group in self._rhodecode_db_user.group_member]
783 c.user_groups = ext_json.str_json(groups)
781 c.user_groups = ext_json.str_json(groups)
784 return self._get_template_context(c)
782 return self._get_template_context(c)
@@ -1,184 +1,183 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import logging
19 import logging
21
20
22 from pyramid.httpexceptions import (
21 from pyramid.httpexceptions import (
23 HTTPFound, HTTPNotFound, HTTPInternalServerError)
22 HTTPFound, HTTPNotFound, HTTPInternalServerError)
24
23
25 from rhodecode.apps._base import BaseAppView
24 from rhodecode.apps._base import BaseAppView
26 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
25 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
27
26
28 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
29 from rhodecode.lib.helpers import SqlPage
28 from rhodecode.lib.helpers import SqlPage
30 from rhodecode.lib.utils2 import safe_int
29 from rhodecode.lib.utils2 import safe_int
31 from rhodecode.model.db import Notification
30 from rhodecode.model.db import Notification
32 from rhodecode.model.notification import NotificationModel
31 from rhodecode.model.notification import NotificationModel
33 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
34
33
35
34
36 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
37
36
38
37
39 class MyAccountNotificationsView(BaseAppView):
38 class MyAccountNotificationsView(BaseAppView):
40
39
41 def load_default_context(self):
40 def load_default_context(self):
42 c = self._get_local_tmpl_context()
41 c = self._get_local_tmpl_context()
43 c.user = c.auth_user.get_instance()
42 c.user = c.auth_user.get_instance()
44
43
45 return c
44 return c
46
45
47 def _has_permissions(self, notification):
46 def _has_permissions(self, notification):
48 def is_owner():
47 def is_owner():
49 user_id = self._rhodecode_db_user.user_id
48 user_id = self._rhodecode_db_user.user_id
50 for user_notification in notification.notifications_to_users:
49 for user_notification in notification.notifications_to_users:
51 if user_notification.user.user_id == user_id:
50 if user_notification.user.user_id == user_id:
52 return True
51 return True
53 return False
52 return False
54 return h.HasPermissionAny('hg.admin')() or is_owner()
53 return h.HasPermissionAny('hg.admin')() or is_owner()
55
54
56 @LoginRequired()
55 @LoginRequired()
57 @NotAnonymous()
56 @NotAnonymous()
58 def notifications_show_all(self):
57 def notifications_show_all(self):
59 c = self.load_default_context()
58 c = self.load_default_context()
60
59
61 c.unread_count = NotificationModel().get_unread_cnt_for_user(
60 c.unread_count = NotificationModel().get_unread_cnt_for_user(
62 self._rhodecode_db_user.user_id)
61 self._rhodecode_db_user.user_id)
63
62
64 _current_filter = self.request.GET.getall('type') or ['unread']
63 _current_filter = self.request.GET.getall('type') or ['unread']
65
64
66 notifications = NotificationModel().get_for_user(
65 notifications = NotificationModel().get_for_user(
67 self._rhodecode_db_user.user_id,
66 self._rhodecode_db_user.user_id,
68 filter_=_current_filter)
67 filter_=_current_filter)
69
68
70 p = safe_int(self.request.GET.get('page', 1), 1)
69 p = safe_int(self.request.GET.get('page', 1), 1)
71
70
72 def url_generator(page_num):
71 def url_generator(page_num):
73 query_params = {
72 query_params = {
74 'page': page_num
73 'page': page_num
75 }
74 }
76 _query = self.request.GET.mixed()
75 _query = self.request.GET.mixed()
77 query_params.update(_query)
76 query_params.update(_query)
78 return self.request.current_route_path(_query=query_params)
77 return self.request.current_route_path(_query=query_params)
79
78
80 c.notifications = SqlPage(notifications, page=p, items_per_page=10,
79 c.notifications = SqlPage(notifications, page=p, items_per_page=10,
81 url_maker=url_generator)
80 url_maker=url_generator)
82
81
83 c.unread_type = 'unread'
82 c.unread_type = 'unread'
84 c.all_type = 'all'
83 c.all_type = 'all'
85 c.pull_request_type = Notification.TYPE_PULL_REQUEST
84 c.pull_request_type = Notification.TYPE_PULL_REQUEST
86 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
85 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
87 Notification.TYPE_PULL_REQUEST_COMMENT]
86 Notification.TYPE_PULL_REQUEST_COMMENT]
88
87
89 c.current_filter = 'unread' # default filter
88 c.current_filter = 'unread' # default filter
90
89
91 if _current_filter == [c.pull_request_type]:
90 if _current_filter == [c.pull_request_type]:
92 c.current_filter = 'pull_request'
91 c.current_filter = 'pull_request'
93 elif _current_filter == c.comment_type:
92 elif _current_filter == c.comment_type:
94 c.current_filter = 'comment'
93 c.current_filter = 'comment'
95 elif _current_filter == [c.unread_type]:
94 elif _current_filter == [c.unread_type]:
96 c.current_filter = 'unread'
95 c.current_filter = 'unread'
97 elif _current_filter == [c.all_type]:
96 elif _current_filter == [c.all_type]:
98 c.current_filter = 'all'
97 c.current_filter = 'all'
99 return self._get_template_context(c)
98 return self._get_template_context(c)
100
99
101 @LoginRequired()
100 @LoginRequired()
102 @NotAnonymous()
101 @NotAnonymous()
103 def notifications_show(self):
102 def notifications_show(self):
104 c = self.load_default_context()
103 c = self.load_default_context()
105 notification_id = self.request.matchdict['notification_id']
104 notification_id = self.request.matchdict['notification_id']
106 notification = Notification.get_or_404(notification_id)
105 notification = Notification.get_or_404(notification_id)
107
106
108 if not self._has_permissions(notification):
107 if not self._has_permissions(notification):
109 log.debug('User %s does not have permission to access notification',
108 log.debug('User %s does not have permission to access notification',
110 self._rhodecode_user)
109 self._rhodecode_user)
111 raise HTTPNotFound()
110 raise HTTPNotFound()
112
111
113 u_notification = NotificationModel().get_user_notification(
112 u_notification = NotificationModel().get_user_notification(
114 self._rhodecode_db_user.user_id, notification)
113 self._rhodecode_db_user.user_id, notification)
115 if not u_notification:
114 if not u_notification:
116 log.debug('User %s notification does not exist',
115 log.debug('User %s notification does not exist',
117 self._rhodecode_user)
116 self._rhodecode_user)
118 raise HTTPNotFound()
117 raise HTTPNotFound()
119
118
120 # when opening this notification, mark it as read for this use
119 # when opening this notification, mark it as read for this use
121 if not u_notification.read:
120 if not u_notification.read:
122 u_notification.mark_as_read()
121 u_notification.mark_as_read()
123 Session().commit()
122 Session().commit()
124
123
125 c.notification = notification
124 c.notification = notification
126
125
127 return self._get_template_context(c)
126 return self._get_template_context(c)
128
127
129 @LoginRequired()
128 @LoginRequired()
130 @NotAnonymous()
129 @NotAnonymous()
131 @CSRFRequired()
130 @CSRFRequired()
132 def notifications_mark_all_read(self):
131 def notifications_mark_all_read(self):
133 NotificationModel().mark_all_read_for_user(
132 NotificationModel().mark_all_read_for_user(
134 self._rhodecode_db_user.user_id,
133 self._rhodecode_db_user.user_id,
135 filter_=self.request.GET.getall('type'))
134 filter_=self.request.GET.getall('type'))
136 Session().commit()
135 Session().commit()
137 raise HTTPFound(h.route_path('notifications_show_all'))
136 raise HTTPFound(h.route_path('notifications_show_all'))
138
137
139 @LoginRequired()
138 @LoginRequired()
140 @NotAnonymous()
139 @NotAnonymous()
141 @CSRFRequired()
140 @CSRFRequired()
142 def notification_update(self):
141 def notification_update(self):
143 notification_id = self.request.matchdict['notification_id']
142 notification_id = self.request.matchdict['notification_id']
144 notification = Notification.get_or_404(notification_id)
143 notification = Notification.get_or_404(notification_id)
145
144
146 if not self._has_permissions(notification):
145 if not self._has_permissions(notification):
147 log.debug('User %s does not have permission to access notification',
146 log.debug('User %s does not have permission to access notification',
148 self._rhodecode_user)
147 self._rhodecode_user)
149 raise HTTPNotFound()
148 raise HTTPNotFound()
150
149
151 try:
150 try:
152 # updates notification read flag
151 # updates notification read flag
153 NotificationModel().mark_read(
152 NotificationModel().mark_read(
154 self._rhodecode_user.user_id, notification)
153 self._rhodecode_user.user_id, notification)
155 Session().commit()
154 Session().commit()
156 return 'ok'
155 return 'ok'
157 except Exception:
156 except Exception:
158 Session().rollback()
157 Session().rollback()
159 log.exception("Exception updating a notification item")
158 log.exception("Exception updating a notification item")
160
159
161 raise HTTPInternalServerError()
160 raise HTTPInternalServerError()
162
161
163 @LoginRequired()
162 @LoginRequired()
164 @NotAnonymous()
163 @NotAnonymous()
165 @CSRFRequired()
164 @CSRFRequired()
166 def notification_delete(self):
165 def notification_delete(self):
167 notification_id = self.request.matchdict['notification_id']
166 notification_id = self.request.matchdict['notification_id']
168 notification = Notification.get_or_404(notification_id)
167 notification = Notification.get_or_404(notification_id)
169 if not self._has_permissions(notification):
168 if not self._has_permissions(notification):
170 log.debug('User %s does not have permission to access notification',
169 log.debug('User %s does not have permission to access notification',
171 self._rhodecode_user)
170 self._rhodecode_user)
172 raise HTTPNotFound()
171 raise HTTPNotFound()
173
172
174 try:
173 try:
175 # deletes only notification2user
174 # deletes only notification2user
176 NotificationModel().delete(
175 NotificationModel().delete(
177 self._rhodecode_user.user_id, notification)
176 self._rhodecode_user.user_id, notification)
178 Session().commit()
177 Session().commit()
179 return 'ok'
178 return 'ok'
180 except Exception:
179 except Exception:
181 Session().rollback()
180 Session().rollback()
182 log.exception("Exception deleting a notification item")
181 log.exception("Exception deleting a notification item")
183
182
184 raise HTTPInternalServerError()
183 raise HTTPInternalServerError()
@@ -1,146 +1,144 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound
21 from pyramid.httpexceptions import HTTPFound
24
22
25 from rhodecode.apps._base import BaseAppView, DataGridAppView
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
26 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
24 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
27 from rhodecode.events import trigger
25 from rhodecode.events import trigger
28 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
29 from rhodecode.lib import audit_logger
27 from rhodecode.lib import audit_logger
30 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
31 from rhodecode.model.db import IntegrityError, UserSshKeys
29 from rhodecode.model.db import IntegrityError, UserSshKeys
32 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
33 from rhodecode.model.ssh_key import SshKeyModel
31 from rhodecode.model.ssh_key import SshKeyModel
34
32
35 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
36
34
37
35
38 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
36 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
39
37
40 def load_default_context(self):
38 def load_default_context(self):
41 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
42 c.user = c.auth_user.get_instance()
40 c.user = c.auth_user.get_instance()
43 c.ssh_enabled = self.request.registry.settings.get(
41 c.ssh_enabled = self.request.registry.settings.get(
44 'ssh.generate_authorized_keyfile')
42 'ssh.generate_authorized_keyfile')
45 return c
43 return c
46
44
47 @LoginRequired()
45 @LoginRequired()
48 @NotAnonymous()
46 @NotAnonymous()
49 def my_account_ssh_keys(self):
47 def my_account_ssh_keys(self):
50 _ = self.request.translate
48 _ = self.request.translate
51
49
52 c = self.load_default_context()
50 c = self.load_default_context()
53 c.active = 'ssh_keys'
51 c.active = 'ssh_keys'
54 c.default_key = self.request.GET.get('default_key')
52 c.default_key = self.request.GET.get('default_key')
55 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
53 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
56 return self._get_template_context(c)
54 return self._get_template_context(c)
57
55
58 @LoginRequired()
56 @LoginRequired()
59 @NotAnonymous()
57 @NotAnonymous()
60 def ssh_keys_generate_keypair(self):
58 def ssh_keys_generate_keypair(self):
61 _ = self.request.translate
59 _ = self.request.translate
62 c = self.load_default_context()
60 c = self.load_default_context()
63
61
64 c.active = 'ssh_keys_generate'
62 c.active = 'ssh_keys_generate'
65 if c.ssh_key_generator_enabled:
63 if c.ssh_key_generator_enabled:
66 private_format = self.request.GET.get('private_format') \
64 private_format = self.request.GET.get('private_format') \
67 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
65 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
68 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
66 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
69 c.private, c.public = SshKeyModel().generate_keypair(
67 c.private, c.public = SshKeyModel().generate_keypair(
70 comment=comment, private_format=private_format)
68 comment=comment, private_format=private_format)
71 c.target_form_url = h.route_path(
69 c.target_form_url = h.route_path(
72 'my_account_ssh_keys', _query=dict(default_key=c.public))
70 'my_account_ssh_keys', _query=dict(default_key=c.public))
73 return self._get_template_context(c)
71 return self._get_template_context(c)
74
72
75 @LoginRequired()
73 @LoginRequired()
76 @NotAnonymous()
74 @NotAnonymous()
77 @CSRFRequired()
75 @CSRFRequired()
78 def my_account_ssh_keys_add(self):
76 def my_account_ssh_keys_add(self):
79 _ = self.request.translate
77 _ = self.request.translate
80 c = self.load_default_context()
78 c = self.load_default_context()
81
79
82 user_data = c.user.get_api_data()
80 user_data = c.user.get_api_data()
83 key_data = self.request.POST.get('key_data')
81 key_data = self.request.POST.get('key_data')
84 description = self.request.POST.get('description')
82 description = self.request.POST.get('description')
85 fingerprint = 'unknown'
83 fingerprint = 'unknown'
86 try:
84 try:
87 if not key_data:
85 if not key_data:
88 raise ValueError('Please add a valid public key')
86 raise ValueError('Please add a valid public key')
89
87
90 key = SshKeyModel().parse_key(key_data.strip())
88 key = SshKeyModel().parse_key(key_data.strip())
91 fingerprint = key.hash_md5()
89 fingerprint = key.hash_md5()
92
90
93 ssh_key = SshKeyModel().create(
91 ssh_key = SshKeyModel().create(
94 c.user.user_id, fingerprint, key.keydata, description)
92 c.user.user_id, fingerprint, key.keydata, description)
95 ssh_key_data = ssh_key.get_api_data()
93 ssh_key_data = ssh_key.get_api_data()
96
94
97 audit_logger.store_web(
95 audit_logger.store_web(
98 'user.edit.ssh_key.add', action_data={
96 'user.edit.ssh_key.add', action_data={
99 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
97 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
100 user=self._rhodecode_user, )
98 user=self._rhodecode_user, )
101 Session().commit()
99 Session().commit()
102
100
103 # Trigger an event on change of keys.
101 # Trigger an event on change of keys.
104 trigger(SshKeyFileChangeEvent(), self.request.registry)
102 trigger(SshKeyFileChangeEvent(), self.request.registry)
105
103
106 h.flash(_("Ssh Key successfully created"), category='success')
104 h.flash(_("Ssh Key successfully created"), category='success')
107
105
108 except IntegrityError:
106 except IntegrityError:
109 log.exception("Exception during ssh key saving")
107 log.exception("Exception during ssh key saving")
110 err = 'Such key with fingerprint `{}` already exists, ' \
108 err = 'Such key with fingerprint `{}` already exists, ' \
111 'please use a different one'.format(fingerprint)
109 'please use a different one'.format(fingerprint)
112 h.flash(_('An error occurred during ssh key saving: {}').format(err),
110 h.flash(_('An error occurred during ssh key saving: {}').format(err),
113 category='error')
111 category='error')
114 except Exception as e:
112 except Exception as e:
115 log.exception("Exception during ssh key saving")
113 log.exception("Exception during ssh key saving")
116 h.flash(_('An error occurred during ssh key saving: {}').format(e),
114 h.flash(_('An error occurred during ssh key saving: {}').format(e),
117 category='error')
115 category='error')
118
116
119 return HTTPFound(h.route_path('my_account_ssh_keys'))
117 return HTTPFound(h.route_path('my_account_ssh_keys'))
120
118
121 @LoginRequired()
119 @LoginRequired()
122 @NotAnonymous()
120 @NotAnonymous()
123 @CSRFRequired()
121 @CSRFRequired()
124 def my_account_ssh_keys_delete(self):
122 def my_account_ssh_keys_delete(self):
125 _ = self.request.translate
123 _ = self.request.translate
126 c = self.load_default_context()
124 c = self.load_default_context()
127
125
128 user_data = c.user.get_api_data()
126 user_data = c.user.get_api_data()
129
127
130 del_ssh_key = self.request.POST.get('del_ssh_key')
128 del_ssh_key = self.request.POST.get('del_ssh_key')
131
129
132 if del_ssh_key:
130 if del_ssh_key:
133 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
131 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
134 ssh_key_data = ssh_key.get_api_data()
132 ssh_key_data = ssh_key.get_api_data()
135
133
136 SshKeyModel().delete(del_ssh_key, c.user.user_id)
134 SshKeyModel().delete(del_ssh_key, c.user.user_id)
137 audit_logger.store_web(
135 audit_logger.store_web(
138 'user.edit.ssh_key.delete', action_data={
136 'user.edit.ssh_key.delete', action_data={
139 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
137 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
140 user=self._rhodecode_user,)
138 user=self._rhodecode_user,)
141 Session().commit()
139 Session().commit()
142 # Trigger an event on change of keys.
140 # Trigger an event on change of keys.
143 trigger(SshKeyFileChangeEvent(), self.request.registry)
141 trigger(SshKeyFileChangeEvent(), self.request.registry)
144 h.flash(_("Ssh key successfully deleted"), category='success')
142 h.flash(_("Ssh key successfully deleted"), category='success')
145
143
146 return HTTPFound(h.route_path('my_account_ssh_keys'))
144 return HTTPFound(h.route_path('my_account_ssh_keys'))
@@ -1,64 +1,62 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 from rhodecode.apps._base import ADMIN_PREFIX
19 from rhodecode.apps._base import ADMIN_PREFIX
22
20
23
21
24 def admin_routes(config):
22 def admin_routes(config):
25 from rhodecode.apps.ops.views import OpsView
23 from rhodecode.apps.ops.views import OpsView
26
24
27 config.add_route(
25 config.add_route(
28 name='ops_ping',
26 name='ops_ping',
29 pattern='/ping')
27 pattern='/ping')
30 config.add_view(
28 config.add_view(
31 OpsView,
29 OpsView,
32 attr='ops_ping',
30 attr='ops_ping',
33 route_name='ops_ping', request_method='GET',
31 route_name='ops_ping', request_method='GET',
34 renderer='json_ext')
32 renderer='json_ext')
35
33
36 config.add_route(
34 config.add_route(
37 name='ops_error_test',
35 name='ops_error_test',
38 pattern='/error')
36 pattern='/error')
39 config.add_view(
37 config.add_view(
40 OpsView,
38 OpsView,
41 attr='ops_error_test',
39 attr='ops_error_test',
42 route_name='ops_error_test', request_method='GET',
40 route_name='ops_error_test', request_method='GET',
43 renderer='json_ext')
41 renderer='json_ext')
44
42
45 config.add_route(
43 config.add_route(
46 name='ops_redirect_test',
44 name='ops_redirect_test',
47 pattern='/redirect')
45 pattern='/redirect')
48 config.add_view(
46 config.add_view(
49 OpsView,
47 OpsView,
50 attr='ops_redirect_test',
48 attr='ops_redirect_test',
51 route_name='ops_redirect_test', request_method='GET',
49 route_name='ops_redirect_test', request_method='GET',
52 renderer='json_ext')
50 renderer='json_ext')
53
51
54 config.add_route(
52 config.add_route(
55 name='ops_healthcheck',
53 name='ops_healthcheck',
56 pattern='/status')
54 pattern='/status')
57 config.add_view(
55 config.add_view(
58 OpsView,
56 OpsView,
59 attr='ops_healthcheck',
57 attr='ops_healthcheck',
60 route_name='ops_healthcheck', request_method='GET',
58 route_name='ops_healthcheck', request_method='GET',
61 renderer='json_ext')
59 renderer='json_ext')
62
60
63 def includeme(config):
61 def includeme(config):
64 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
62 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
@@ -1,96 +1,94 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import time
19 import time
22 import logging
20 import logging
23
21
24
22
25 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
26
24
27 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import LoginRequired
27 from rhodecode.lib.auth import LoginRequired
30 from collections import OrderedDict
28 from collections import OrderedDict
31 from rhodecode.model.db import UserApiKeys
29 from rhodecode.model.db import UserApiKeys
32
30
33 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
34
32
35
33
36 class OpsView(BaseAppView):
34 class OpsView(BaseAppView):
37
35
38 def load_default_context(self):
36 def load_default_context(self):
39 c = self._get_local_tmpl_context()
37 c = self._get_local_tmpl_context()
40 c.user = c.auth_user.get_instance()
38 c.user = c.auth_user.get_instance()
41
39
42 return c
40 return c
43
41
44 def ops_ping(self):
42 def ops_ping(self):
45 data = OrderedDict()
43 data = OrderedDict()
46 data['instance'] = self.request.registry.settings.get('instance_id')
44 data['instance'] = self.request.registry.settings.get('instance_id')
47
45
48 if getattr(self.request, 'user'):
46 if getattr(self.request, 'user'):
49 caller_name = 'anonymous'
47 caller_name = 'anonymous'
50 if self.request.user.user_id:
48 if self.request.user.user_id:
51 caller_name = self.request.user.username
49 caller_name = self.request.user.username
52
50
53 data['caller_ip'] = self.request.user.ip_addr
51 data['caller_ip'] = self.request.user.ip_addr
54 data['caller_name'] = caller_name
52 data['caller_name'] = caller_name
55
53
56 return {'ok': data}
54 return {'ok': data}
57
55
58 def ops_error_test(self):
56 def ops_error_test(self):
59 """
57 """
60 Test exception handling and emails on errors
58 Test exception handling and emails on errors
61 """
59 """
62
60
63 class TestException(Exception):
61 class TestException(Exception):
64 pass
62 pass
65 # add timeout so we add some sort of rate limiter
63 # add timeout so we add some sort of rate limiter
66 time.sleep(2)
64 time.sleep(2)
67 msg = ('RhodeCode Enterprise test exception. '
65 msg = ('RhodeCode Enterprise test exception. '
68 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
66 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
69 raise TestException(msg)
67 raise TestException(msg)
70
68
71 def ops_redirect_test(self):
69 def ops_redirect_test(self):
72 """
70 """
73 Test redirect handling
71 Test redirect handling
74 """
72 """
75 redirect_to = self.request.GET.get('to') or h.route_path('home')
73 redirect_to = self.request.GET.get('to') or h.route_path('home')
76 raise HTTPFound(redirect_to)
74 raise HTTPFound(redirect_to)
77
75
78 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_HTTP])
76 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_HTTP])
79 def ops_healthcheck(self):
77 def ops_healthcheck(self):
80 from rhodecode.lib.system_info import load_system_info
78 from rhodecode.lib.system_info import load_system_info
81
79
82 vcsserver_info = load_system_info('vcs_server')
80 vcsserver_info = load_system_info('vcs_server')
83 if vcsserver_info:
81 if vcsserver_info:
84 vcsserver_info = vcsserver_info['human_value']
82 vcsserver_info = vcsserver_info['human_value']
85
83
86 db_info = load_system_info('database_info')
84 db_info = load_system_info('database_info')
87 if db_info:
85 if db_info:
88 db_info = db_info['human_value']
86 db_info = db_info['human_value']
89
87
90 health_spec = {
88 health_spec = {
91 'caller_ip': self.request.user.ip_addr,
89 'caller_ip': self.request.user.ip_addr,
92 'vcsserver': vcsserver_info,
90 'vcsserver': vcsserver_info,
93 'db': db_info,
91 'db': db_info,
94 }
92 }
95
93
96 return {'healthcheck': health_spec}
94 return {'healthcheck': health_spec}
@@ -1,102 +1,100 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
18 from rhodecode.apps._base import add_route_with_slash
21 from rhodecode.apps.repo_group.views.repo_group_settings import RepoGroupSettingsView
19 from rhodecode.apps.repo_group.views.repo_group_settings import RepoGroupSettingsView
22 from rhodecode.apps.repo_group.views.repo_group_advanced import RepoGroupAdvancedSettingsView
20 from rhodecode.apps.repo_group.views.repo_group_advanced import RepoGroupAdvancedSettingsView
23 from rhodecode.apps.repo_group.views.repo_group_permissions import RepoGroupPermissionsView
21 from rhodecode.apps.repo_group.views.repo_group_permissions import RepoGroupPermissionsView
24 from rhodecode.apps.home.views import HomeView
22 from rhodecode.apps.home.views import HomeView
25
23
26
24
27 def includeme(config):
25 def includeme(config):
28
26
29 # Settings
27 # Settings
30 config.add_route(
28 config.add_route(
31 name='edit_repo_group',
29 name='edit_repo_group',
32 pattern='/{repo_group_name:.*?[^/]}/_edit',
30 pattern='/{repo_group_name:.*?[^/]}/_edit',
33 repo_group_route=True)
31 repo_group_route=True)
34 config.add_view(
32 config.add_view(
35 RepoGroupSettingsView,
33 RepoGroupSettingsView,
36 attr='edit_settings',
34 attr='edit_settings',
37 route_name='edit_repo_group', request_method='GET',
35 route_name='edit_repo_group', request_method='GET',
38 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
36 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
39 config.add_view(
37 config.add_view(
40 RepoGroupSettingsView,
38 RepoGroupSettingsView,
41 attr='edit_settings_update',
39 attr='edit_settings_update',
42 route_name='edit_repo_group', request_method='POST',
40 route_name='edit_repo_group', request_method='POST',
43 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
41 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
44
42
45 # Settings advanced
43 # Settings advanced
46 config.add_route(
44 config.add_route(
47 name='edit_repo_group_advanced',
45 name='edit_repo_group_advanced',
48 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
46 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
49 repo_group_route=True)
47 repo_group_route=True)
50 config.add_view(
48 config.add_view(
51 RepoGroupAdvancedSettingsView,
49 RepoGroupAdvancedSettingsView,
52 attr='edit_repo_group_advanced',
50 attr='edit_repo_group_advanced',
53 route_name='edit_repo_group_advanced', request_method='GET',
51 route_name='edit_repo_group_advanced', request_method='GET',
54 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
52 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
55
53
56 config.add_route(
54 config.add_route(
57 name='edit_repo_group_advanced_delete',
55 name='edit_repo_group_advanced_delete',
58 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
56 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
59 repo_group_route=True)
57 repo_group_route=True)
60 config.add_view(
58 config.add_view(
61 RepoGroupAdvancedSettingsView,
59 RepoGroupAdvancedSettingsView,
62 attr='edit_repo_group_delete',
60 attr='edit_repo_group_delete',
63 route_name='edit_repo_group_advanced_delete', request_method='POST',
61 route_name='edit_repo_group_advanced_delete', request_method='POST',
64 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
62 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
65
63
66 # settings permissions
64 # settings permissions
67 config.add_route(
65 config.add_route(
68 name='edit_repo_group_perms',
66 name='edit_repo_group_perms',
69 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
67 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
70 repo_group_route=True)
68 repo_group_route=True)
71 config.add_view(
69 config.add_view(
72 RepoGroupPermissionsView,
70 RepoGroupPermissionsView,
73 attr='edit_repo_group_permissions',
71 attr='edit_repo_group_permissions',
74 route_name='edit_repo_group_perms', request_method='GET',
72 route_name='edit_repo_group_perms', request_method='GET',
75 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
73 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
76
74
77 config.add_route(
75 config.add_route(
78 name='edit_repo_group_perms_update',
76 name='edit_repo_group_perms_update',
79 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
77 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
80 repo_group_route=True)
78 repo_group_route=True)
81 config.add_view(
79 config.add_view(
82 RepoGroupPermissionsView,
80 RepoGroupPermissionsView,
83 attr='edit_repo_groups_permissions_update',
81 attr='edit_repo_groups_permissions_update',
84 route_name='edit_repo_group_perms_update', request_method='POST',
82 route_name='edit_repo_group_perms_update', request_method='POST',
85 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
83 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
86
84
87 # Summary, NOTE(marcink): needs to be at the end for catch-all
85 # Summary, NOTE(marcink): needs to be at the end for catch-all
88 add_route_with_slash(
86 add_route_with_slash(
89 config,
87 config,
90 name='repo_group_home',
88 name='repo_group_home',
91 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
89 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
92 config.add_view(
90 config.add_view(
93 HomeView,
91 HomeView,
94 attr='repo_group_main_page',
92 attr='repo_group_main_page',
95 route_name='repo_group_home', request_method='GET',
93 route_name='repo_group_home', request_method='GET',
96 renderer='rhodecode:templates/index_repo_group.mako')
94 renderer='rhodecode:templates/index_repo_group.mako')
97 config.add_view(
95 config.add_view(
98 HomeView,
96 HomeView,
99 attr='repo_group_main_page',
97 attr='repo_group_main_page',
100 route_name='repo_group_home_slash', request_method='GET',
98 route_name='repo_group_home_slash', request_method='GET',
101 renderer='rhodecode:templates/index_repo_group.mako')
99 renderer='rhodecode:templates/index_repo_group.mako')
102
100
@@ -1,18 +1,17 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,90 +1,89 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import pytest
19 import pytest
21
20
22 from rhodecode.tests import assert_session_flash
21 from rhodecode.tests import assert_session_flash
23
22
24
23
25 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
26 import urllib.request
25 import urllib.request
27 import urllib.parse
26 import urllib.parse
28 import urllib.error
27 import urllib.error
29
28
30 base_url = {
29 base_url = {
31 'edit_repo_group_advanced':
30 'edit_repo_group_advanced':
32 '/{repo_group_name}/_settings/advanced',
31 '/{repo_group_name}/_settings/advanced',
33 'edit_repo_group_advanced_delete':
32 'edit_repo_group_advanced_delete':
34 '/{repo_group_name}/_settings/advanced/delete',
33 '/{repo_group_name}/_settings/advanced/delete',
35 }[name].format(**kwargs)
34 }[name].format(**kwargs)
36
35
37 if params:
36 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
38 return base_url
40
39
41
40
42 @pytest.mark.usefixtures("app")
41 @pytest.mark.usefixtures("app")
43 class TestRepoGroupsAdvancedView(object):
42 class TestRepoGroupsAdvancedView(object):
44
43
45 @pytest.mark.parametrize('repo_group_name', [
44 @pytest.mark.parametrize('repo_group_name', [
46 'gro',
45 'gro',
47 '12345',
46 '12345',
48 ])
47 ])
49 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
50 user_util._test_name = repo_group_name
49 user_util._test_name = repo_group_name
51 gr = user_util.create_repo_group()
50 gr = user_util.create_repo_group()
52 self.app.get(
51 self.app.get(
53 route_path('edit_repo_group_advanced',
52 route_path('edit_repo_group_advanced',
54 repo_group_name=gr.group_name))
53 repo_group_name=gr.group_name))
55
54
56 def test_show_advanced_settings_delete(self, autologin_user, user_util,
55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
57 csrf_token):
56 csrf_token):
58 gr = user_util.create_repo_group(auto_cleanup=False)
57 gr = user_util.create_repo_group(auto_cleanup=False)
59 repo_group_name = gr.group_name
58 repo_group_name = gr.group_name
60
59
61 params = dict(
60 params = dict(
62 csrf_token=csrf_token
61 csrf_token=csrf_token
63 )
62 )
64 response = self.app.post(
63 response = self.app.post(
65 route_path('edit_repo_group_advanced_delete',
64 route_path('edit_repo_group_advanced_delete',
66 repo_group_name=repo_group_name), params=params)
65 repo_group_name=repo_group_name), params=params)
67 assert_session_flash(
66 assert_session_flash(
68 response, 'Removed repository group `{}`'.format(repo_group_name))
67 response, 'Removed repository group `{}`'.format(repo_group_name))
69
68
70 def test_delete_not_possible_with_objects_inside(self, autologin_user,
69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
71 repo_groups, csrf_token):
70 repo_groups, csrf_token):
72 zombie_group, parent_group, child_group = repo_groups
71 zombie_group, parent_group, child_group = repo_groups
73
72
74 response = self.app.get(
73 response = self.app.get(
75 route_path('edit_repo_group_advanced',
74 route_path('edit_repo_group_advanced',
76 repo_group_name=parent_group.group_name))
75 repo_group_name=parent_group.group_name))
77
76
78 response.mustcontain(
77 response.mustcontain(
79 'This repository group includes 1 children repository group')
78 'This repository group includes 1 children repository group')
80
79
81 params = dict(
80 params = dict(
82 csrf_token=csrf_token
81 csrf_token=csrf_token
83 )
82 )
84 response = self.app.post(
83 response = self.app.post(
85 route_path('edit_repo_group_advanced_delete',
84 route_path('edit_repo_group_advanced_delete',
86 repo_group_name=parent_group.group_name), params=params)
85 repo_group_name=parent_group.group_name), params=params)
87
86
88 assert_session_flash(
87 assert_session_flash(
89 response, 'This repository group contains 1 subgroup '
88 response, 'This repository group contains 1 subgroup '
90 'and cannot be deleted')
89 'and cannot be deleted')
@@ -1,87 +1,86 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import pytest
19 import pytest
21
20
22 from rhodecode.tests.utils import permission_update_data_generator
21 from rhodecode.tests.utils import permission_update_data_generator
23
22
24
23
25 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
26 import urllib.request
25 import urllib.request
27 import urllib.parse
26 import urllib.parse
28 import urllib.error
27 import urllib.error
29
28
30 base_url = {
29 base_url = {
31 'edit_repo_group_perms':
30 'edit_repo_group_perms':
32 '/{repo_group_name:}/_settings/permissions',
31 '/{repo_group_name:}/_settings/permissions',
33 'edit_repo_group_perms_update':
32 'edit_repo_group_perms_update':
34 '/{repo_group_name}/_settings/permissions/update',
33 '/{repo_group_name}/_settings/permissions/update',
35 }[name].format(**kwargs)
34 }[name].format(**kwargs)
36
35
37 if params:
36 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
38 return base_url
40
39
41
40
42 @pytest.mark.usefixtures("app")
41 @pytest.mark.usefixtures("app")
43 class TestRepoGroupPermissionsView(object):
42 class TestRepoGroupPermissionsView(object):
44
43
45 def test_edit_perms_view(self, user_util, autologin_user):
44 def test_edit_perms_view(self, user_util, autologin_user):
46 repo_group = user_util.create_repo_group()
45 repo_group = user_util.create_repo_group()
47
46
48 self.app.get(
47 self.app.get(
49 route_path('edit_repo_group_perms',
48 route_path('edit_repo_group_perms',
50 repo_group_name=repo_group.group_name), status=200)
49 repo_group_name=repo_group.group_name), status=200)
51
50
52 def test_update_permissions(self, csrf_token, user_util):
51 def test_update_permissions(self, csrf_token, user_util):
53 repo_group = user_util.create_repo_group()
52 repo_group = user_util.create_repo_group()
54 repo_group_name = repo_group.group_name
53 repo_group_name = repo_group.group_name
55 user = user_util.create_user()
54 user = user_util.create_user()
56 user_id = user.user_id
55 user_id = user.user_id
57 username = user.username
56 username = user.username
58
57
59 # grant new
58 # grant new
60 form_data = permission_update_data_generator(
59 form_data = permission_update_data_generator(
61 csrf_token,
60 csrf_token,
62 default='group.write',
61 default='group.write',
63 grant=[(user_id, 'group.write', username, 'user')])
62 grant=[(user_id, 'group.write', username, 'user')])
64
63
65 # recursive flag required for repo groups
64 # recursive flag required for repo groups
66 form_data.extend([('recursive', u'none')])
65 form_data.extend([('recursive', u'none')])
67
66
68 response = self.app.post(
67 response = self.app.post(
69 route_path('edit_repo_group_perms_update',
68 route_path('edit_repo_group_perms_update',
70 repo_group_name=repo_group_name), form_data).follow()
69 repo_group_name=repo_group_name), form_data).follow()
71
70
72 assert 'Repository Group permissions updated' in response
71 assert 'Repository Group permissions updated' in response
73
72
74 # revoke given
73 # revoke given
75 form_data = permission_update_data_generator(
74 form_data = permission_update_data_generator(
76 csrf_token,
75 csrf_token,
77 default='group.read',
76 default='group.read',
78 revoke=[(user_id, 'user')])
77 revoke=[(user_id, 'user')])
79
78
80 # recursive flag required for repo groups
79 # recursive flag required for repo groups
81 form_data.extend([('recursive', u'none')])
80 form_data.extend([('recursive', u'none')])
82
81
83 response = self.app.post(
82 response = self.app.post(
84 route_path('edit_repo_group_perms_update',
83 route_path('edit_repo_group_perms_update',
85 repo_group_name=repo_group_name), form_data).follow()
84 repo_group_name=repo_group_name), form_data).follow()
86
85
87 assert 'Repository Group permissions updated' in response
86 assert 'Repository Group permissions updated' in response
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/ No newline at end of file
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,105 +1,103 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25
23
26 from rhodecode.apps._base import RepoGroupAppView
24 from rhodecode.apps._base import RepoGroupAppView
27 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
26 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
30 LoginRequired, CSRFRequired, HasRepoGroupPermissionAnyDecorator)
28 LoginRequired, CSRFRequired, HasRepoGroupPermissionAnyDecorator)
31 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.model.repo_group import RepoGroupModel
32 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
33
31
34 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
35
33
36
34
37 class RepoGroupAdvancedSettingsView(RepoGroupAppView):
35 class RepoGroupAdvancedSettingsView(RepoGroupAppView):
38 def load_default_context(self):
36 def load_default_context(self):
39 c = self._get_local_tmpl_context()
37 c = self._get_local_tmpl_context()
40 return c
38 return c
41
39
42 @LoginRequired()
40 @LoginRequired()
43 @HasRepoGroupPermissionAnyDecorator('group.admin')
41 @HasRepoGroupPermissionAnyDecorator('group.admin')
44 def edit_repo_group_advanced(self):
42 def edit_repo_group_advanced(self):
45 _ = self.request.translate
43 _ = self.request.translate
46 c = self.load_default_context()
44 c = self.load_default_context()
47 c.active = 'advanced'
45 c.active = 'advanced'
48 c.repo_group = self.db_repo_group
46 c.repo_group = self.db_repo_group
49
47
50 # update commit cache if GET flag is present
48 # update commit cache if GET flag is present
51 if self.request.GET.get('update_commit_cache'):
49 if self.request.GET.get('update_commit_cache'):
52 self.db_repo_group.update_commit_cache()
50 self.db_repo_group.update_commit_cache()
53 h.flash(_('updated commit cache'), category='success')
51 h.flash(_('updated commit cache'), category='success')
54
52
55 return self._get_template_context(c)
53 return self._get_template_context(c)
56
54
57 @LoginRequired()
55 @LoginRequired()
58 @HasRepoGroupPermissionAnyDecorator('group.admin')
56 @HasRepoGroupPermissionAnyDecorator('group.admin')
59 @CSRFRequired()
57 @CSRFRequired()
60 def edit_repo_group_delete(self):
58 def edit_repo_group_delete(self):
61 _ = self.request.translate
59 _ = self.request.translate
62 _ungettext = self.request.plularize
60 _ungettext = self.request.plularize
63 c = self.load_default_context()
61 c = self.load_default_context()
64 c.repo_group = self.db_repo_group
62 c.repo_group = self.db_repo_group
65
63
66 repos = c.repo_group.repositories.all()
64 repos = c.repo_group.repositories.all()
67 if repos:
65 if repos:
68 msg = _ungettext(
66 msg = _ungettext(
69 'This repository group contains %(num)d repository and cannot be deleted',
67 'This repository group contains %(num)d repository and cannot be deleted',
70 'This repository group contains %(num)d repositories and cannot be'
68 'This repository group contains %(num)d repositories and cannot be'
71 ' deleted',
69 ' deleted',
72 len(repos)) % {'num': len(repos)}
70 len(repos)) % {'num': len(repos)}
73 h.flash(msg, category='warning')
71 h.flash(msg, category='warning')
74 raise HTTPFound(
72 raise HTTPFound(
75 h.route_path('edit_repo_group_advanced',
73 h.route_path('edit_repo_group_advanced',
76 repo_group_name=self.db_repo_group_name))
74 repo_group_name=self.db_repo_group_name))
77
75
78 children = c.repo_group.children.all()
76 children = c.repo_group.children.all()
79 if children:
77 if children:
80 msg = _ungettext(
78 msg = _ungettext(
81 'This repository group contains %(num)d subgroup and cannot be deleted',
79 'This repository group contains %(num)d subgroup and cannot be deleted',
82 'This repository group contains %(num)d subgroups and cannot be deleted',
80 'This repository group contains %(num)d subgroups and cannot be deleted',
83 len(children)) % {'num': len(children)}
81 len(children)) % {'num': len(children)}
84 h.flash(msg, category='warning')
82 h.flash(msg, category='warning')
85 raise HTTPFound(
83 raise HTTPFound(
86 h.route_path('edit_repo_group_advanced',
84 h.route_path('edit_repo_group_advanced',
87 repo_group_name=self.db_repo_group_name))
85 repo_group_name=self.db_repo_group_name))
88
86
89 try:
87 try:
90 old_values = c.repo_group.get_api_data()
88 old_values = c.repo_group.get_api_data()
91 RepoGroupModel().delete(self.db_repo_group_name)
89 RepoGroupModel().delete(self.db_repo_group_name)
92
90
93 audit_logger.store_web(
91 audit_logger.store_web(
94 'repo_group.delete', action_data={'old_data': old_values},
92 'repo_group.delete', action_data={'old_data': old_values},
95 user=c.rhodecode_user)
93 user=c.rhodecode_user)
96
94
97 Session().commit()
95 Session().commit()
98 h.flash(_('Removed repository group `%s`') % self.db_repo_group_name,
96 h.flash(_('Removed repository group `%s`') % self.db_repo_group_name,
99 category='success')
97 category='success')
100 except Exception:
98 except Exception:
101 log.exception("Exception during deletion of repository group")
99 log.exception("Exception during deletion of repository group")
102 h.flash(_('Error occurred during deletion of repository group %s')
100 h.flash(_('Error occurred during deletion of repository group %s')
103 % self.db_repo_group_name, category='error')
101 % self.db_repo_group_name, category='error')
104
102
105 raise HTTPFound(h.route_path('repo_groups'))
103 raise HTTPFound(h.route_path('repo_groups'))
@@ -1,104 +1,102 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25
23
26 from rhodecode.apps._base import RepoGroupAppView
24 from rhodecode.apps._base import RepoGroupAppView
27 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
26 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
28 LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.model.db import User
29 from rhodecode.model.db import User
32 from rhodecode.model.permission import PermissionModel
30 from rhodecode.model.permission import PermissionModel
33 from rhodecode.model.repo_group import RepoGroupModel
31 from rhodecode.model.repo_group import RepoGroupModel
34 from rhodecode.model.forms import RepoGroupPermsForm
32 from rhodecode.model.forms import RepoGroupPermsForm
35 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
36
34
37 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
38
36
39
37
40 class RepoGroupPermissionsView(RepoGroupAppView):
38 class RepoGroupPermissionsView(RepoGroupAppView):
41 def load_default_context(self):
39 def load_default_context(self):
42 c = self._get_local_tmpl_context()
40 c = self._get_local_tmpl_context()
43
41
44 return c
42 return c
45
43
46 @LoginRequired()
44 @LoginRequired()
47 @HasRepoGroupPermissionAnyDecorator('group.admin')
45 @HasRepoGroupPermissionAnyDecorator('group.admin')
48 def edit_repo_group_permissions(self):
46 def edit_repo_group_permissions(self):
49 c = self.load_default_context()
47 c = self.load_default_context()
50 c.active = 'permissions'
48 c.active = 'permissions'
51 c.repo_group = self.db_repo_group
49 c.repo_group = self.db_repo_group
52 return self._get_template_context(c)
50 return self._get_template_context(c)
53
51
54 @LoginRequired()
52 @LoginRequired()
55 @HasRepoGroupPermissionAnyDecorator('group.admin')
53 @HasRepoGroupPermissionAnyDecorator('group.admin')
56 @CSRFRequired()
54 @CSRFRequired()
57 def edit_repo_groups_permissions_update(self):
55 def edit_repo_groups_permissions_update(self):
58 _ = self.request.translate
56 _ = self.request.translate
59 c = self.load_default_context()
57 c = self.load_default_context()
60 c.active = 'perms'
58 c.active = 'perms'
61 c.repo_group = self.db_repo_group
59 c.repo_group = self.db_repo_group
62
60
63 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
61 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
64 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
62 form = RepoGroupPermsForm(self.request.translate, valid_recursive_choices)()\
65 .to_python(self.request.POST)
63 .to_python(self.request.POST)
66
64
67 if not c.rhodecode_user.is_admin:
65 if not c.rhodecode_user.is_admin:
68 if self._revoke_perms_on_yourself(form):
66 if self._revoke_perms_on_yourself(form):
69 msg = _('Cannot change permission for yourself as admin')
67 msg = _('Cannot change permission for yourself as admin')
70 h.flash(msg, category='warning')
68 h.flash(msg, category='warning')
71 raise HTTPFound(
69 raise HTTPFound(
72 h.route_path('edit_repo_group_perms',
70 h.route_path('edit_repo_group_perms',
73 repo_group_name=self.db_repo_group_name))
71 repo_group_name=self.db_repo_group_name))
74
72
75 # iterate over all members(if in recursive mode) of this groups and
73 # iterate over all members(if in recursive mode) of this groups and
76 # set the permissions !
74 # set the permissions !
77 # this can be potentially heavy operation
75 # this can be potentially heavy operation
78 changes = RepoGroupModel().update_permissions(
76 changes = RepoGroupModel().update_permissions(
79 c.repo_group,
77 c.repo_group,
80 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
78 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
81 form['recursive'])
79 form['recursive'])
82
80
83 action_data = {
81 action_data = {
84 'added': changes['added'],
82 'added': changes['added'],
85 'updated': changes['updated'],
83 'updated': changes['updated'],
86 'deleted': changes['deleted'],
84 'deleted': changes['deleted'],
87 }
85 }
88 audit_logger.store_web(
86 audit_logger.store_web(
89 'repo_group.edit.permissions', action_data=action_data,
87 'repo_group.edit.permissions', action_data=action_data,
90 user=c.rhodecode_user)
88 user=c.rhodecode_user)
91
89
92 Session().commit()
90 Session().commit()
93 h.flash(_('Repository Group permissions updated'), category='success')
91 h.flash(_('Repository Group permissions updated'), category='success')
94
92
95 affected_user_ids = None
93 affected_user_ids = None
96 if changes.get('default_user_changed', False):
94 if changes.get('default_user_changed', False):
97 # if we change the default user, we need to flush everyone permissions
95 # if we change the default user, we need to flush everyone permissions
98 affected_user_ids = User.get_all_user_ids()
96 affected_user_ids = User.get_all_user_ids()
99 PermissionModel().flush_user_permission_caches(
97 PermissionModel().flush_user_permission_caches(
100 changes, affected_user_ids=affected_user_ids)
98 changes, affected_user_ids=affected_user_ids)
101
99
102 raise HTTPFound(
100 raise HTTPFound(
103 h.route_path('edit_repo_group_perms',
101 h.route_path('edit_repo_group_perms',
104 repo_group_name=self.db_repo_group_name))
102 repo_group_name=self.db_repo_group_name))
@@ -1,187 +1,185 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import deform
20 import deform
23
21
24
22
25 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
26
24
27 from rhodecode import events
25 from rhodecode import events
28 from rhodecode.apps._base import RepoGroupAppView
26 from rhodecode.apps._base import RepoGroupAppView
29 from rhodecode.forms import RcForm
27 from rhodecode.forms import RcForm
30 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
31 from rhodecode.lib import audit_logger
29 from rhodecode.lib import audit_logger
32 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
33 LoginRequired, HasPermissionAll,
31 LoginRequired, HasPermissionAll,
34 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
32 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
35 from rhodecode.model.db import Session, RepoGroup, User
33 from rhodecode.model.db import Session, RepoGroup, User
36 from rhodecode.model.permission import PermissionModel
34 from rhodecode.model.permission import PermissionModel
37 from rhodecode.model.scm import RepoGroupList
35 from rhodecode.model.scm import RepoGroupList
38 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.repo_group import RepoGroupModel
39 from rhodecode.model.validation_schema.schemas import repo_group_schema
37 from rhodecode.model.validation_schema.schemas import repo_group_schema
40
38
41 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
42
40
43
41
44 class RepoGroupSettingsView(RepoGroupAppView):
42 class RepoGroupSettingsView(RepoGroupAppView):
45 def load_default_context(self):
43 def load_default_context(self):
46 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
47 c.repo_group = self.db_repo_group
45 c.repo_group = self.db_repo_group
48 no_parrent = not c.repo_group.parent_group
46 no_parrent = not c.repo_group.parent_group
49 can_create_in_root = self._can_create_repo_group()
47 can_create_in_root = self._can_create_repo_group()
50
48
51 show_root_location = False
49 show_root_location = False
52 if no_parrent or can_create_in_root:
50 if no_parrent or can_create_in_root:
53 # we're global admin, we're ok and we can create TOP level groups
51 # we're global admin, we're ok and we can create TOP level groups
54 # or in case this group is already at top-level we also allow
52 # or in case this group is already at top-level we also allow
55 # creation in root
53 # creation in root
56 show_root_location = True
54 show_root_location = True
57
55
58 acl_groups = RepoGroupList(
56 acl_groups = RepoGroupList(
59 RepoGroup.query().all(),
57 RepoGroup.query().all(),
60 perm_set=['group.admin'])
58 perm_set=['group.admin'])
61 c.repo_groups = RepoGroup.groups_choices(
59 c.repo_groups = RepoGroup.groups_choices(
62 groups=acl_groups,
60 groups=acl_groups,
63 show_empty_group=show_root_location)
61 show_empty_group=show_root_location)
64 # filter out current repo group
62 # filter out current repo group
65 exclude_group_ids = [c.repo_group.group_id]
63 exclude_group_ids = [c.repo_group.group_id]
66 c.repo_groups = [x for x in c.repo_groups if x[0] not in exclude_group_ids]
64 c.repo_groups = [x for x in c.repo_groups if x[0] not in exclude_group_ids]
67 c.repo_groups_choices = [k[0] for k in c.repo_groups]
65 c.repo_groups_choices = [k[0] for k in c.repo_groups]
68
66
69 parent_group = c.repo_group.parent_group
67 parent_group = c.repo_group.parent_group
70
68
71 add_parent_group = (parent_group and (
69 add_parent_group = (parent_group and (
72 parent_group.group_id not in c.repo_groups_choices))
70 parent_group.group_id not in c.repo_groups_choices))
73 if add_parent_group:
71 if add_parent_group:
74 c.repo_groups_choices.append(parent_group.group_id)
72 c.repo_groups_choices.append(parent_group.group_id)
75 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
73 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
76 return c
74 return c
77
75
78 def _can_create_repo_group(self, parent_group_id=None):
76 def _can_create_repo_group(self, parent_group_id=None):
79 is_admin = HasPermissionAll('hg.admin')('group create controller')
77 is_admin = HasPermissionAll('hg.admin')('group create controller')
80 create_repo_group = HasPermissionAll(
78 create_repo_group = HasPermissionAll(
81 'hg.repogroup.create.true')('group create controller')
79 'hg.repogroup.create.true')('group create controller')
82 if is_admin or (create_repo_group and not parent_group_id):
80 if is_admin or (create_repo_group and not parent_group_id):
83 # we're global admin, or we have global repo group create
81 # we're global admin, or we have global repo group create
84 # permission
82 # permission
85 # we're ok and we can create TOP level groups
83 # we're ok and we can create TOP level groups
86 return True
84 return True
87 elif parent_group_id:
85 elif parent_group_id:
88 # we check the permission if we can write to parent group
86 # we check the permission if we can write to parent group
89 group = RepoGroup.get(parent_group_id)
87 group = RepoGroup.get(parent_group_id)
90 group_name = group.group_name if group else None
88 group_name = group.group_name if group else None
91 if HasRepoGroupPermissionAny('group.admin')(
89 if HasRepoGroupPermissionAny('group.admin')(
92 group_name, 'check if user is an admin of group'):
90 group_name, 'check if user is an admin of group'):
93 # we're an admin of passed in group, we're ok.
91 # we're an admin of passed in group, we're ok.
94 return True
92 return True
95 else:
93 else:
96 return False
94 return False
97 return False
95 return False
98
96
99 def _get_schema(self, c, old_values=None):
97 def _get_schema(self, c, old_values=None):
100 return repo_group_schema.RepoGroupSettingsSchema().bind(
98 return repo_group_schema.RepoGroupSettingsSchema().bind(
101 repo_group_repo_group_options=c.repo_groups_choices,
99 repo_group_repo_group_options=c.repo_groups_choices,
102 repo_group_repo_group_items=c.repo_groups,
100 repo_group_repo_group_items=c.repo_groups,
103
101
104 # user caller
102 # user caller
105 user=self._rhodecode_user,
103 user=self._rhodecode_user,
106 old_values=old_values
104 old_values=old_values
107 )
105 )
108
106
109 @LoginRequired()
107 @LoginRequired()
110 @HasRepoGroupPermissionAnyDecorator('group.admin')
108 @HasRepoGroupPermissionAnyDecorator('group.admin')
111 def edit_settings(self):
109 def edit_settings(self):
112 c = self.load_default_context()
110 c = self.load_default_context()
113 c.active = 'settings'
111 c.active = 'settings'
114
112
115 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
113 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
116 defaults['repo_group_owner'] = defaults['user']
114 defaults['repo_group_owner'] = defaults['user']
117
115
118 schema = self._get_schema(c)
116 schema = self._get_schema(c)
119 c.form = RcForm(schema, appstruct=defaults)
117 c.form = RcForm(schema, appstruct=defaults)
120 return self._get_template_context(c)
118 return self._get_template_context(c)
121
119
122 @LoginRequired()
120 @LoginRequired()
123 @HasRepoGroupPermissionAnyDecorator('group.admin')
121 @HasRepoGroupPermissionAnyDecorator('group.admin')
124 @CSRFRequired()
122 @CSRFRequired()
125 def edit_settings_update(self):
123 def edit_settings_update(self):
126 _ = self.request.translate
124 _ = self.request.translate
127 c = self.load_default_context()
125 c = self.load_default_context()
128 c.active = 'settings'
126 c.active = 'settings'
129
127
130 old_repo_group_name = self.db_repo_group_name
128 old_repo_group_name = self.db_repo_group_name
131 new_repo_group_name = old_repo_group_name
129 new_repo_group_name = old_repo_group_name
132
130
133 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
131 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
134 schema = self._get_schema(c, old_values=old_values)
132 schema = self._get_schema(c, old_values=old_values)
135
133
136 c.form = RcForm(schema)
134 c.form = RcForm(schema)
137 pstruct = list(self.request.POST.items())
135 pstruct = list(self.request.POST.items())
138
136
139 try:
137 try:
140 schema_data = c.form.validate(pstruct)
138 schema_data = c.form.validate(pstruct)
141 except deform.ValidationFailure as err_form:
139 except deform.ValidationFailure as err_form:
142 return self._get_template_context(c)
140 return self._get_template_context(c)
143
141
144 # data is now VALID, proceed with updates
142 # data is now VALID, proceed with updates
145 # save validated data back into the updates dict
143 # save validated data back into the updates dict
146 validated_updates = dict(
144 validated_updates = dict(
147 group_name=schema_data['repo_group']['repo_group_name_without_group'],
145 group_name=schema_data['repo_group']['repo_group_name_without_group'],
148 group_parent_id=schema_data['repo_group']['repo_group_id'],
146 group_parent_id=schema_data['repo_group']['repo_group_id'],
149 user=schema_data['repo_group_owner'],
147 user=schema_data['repo_group_owner'],
150 group_description=schema_data['repo_group_description'],
148 group_description=schema_data['repo_group_description'],
151 enable_locking=schema_data['repo_group_enable_locking'],
149 enable_locking=schema_data['repo_group_enable_locking'],
152 )
150 )
153
151
154 try:
152 try:
155 RepoGroupModel().update(self.db_repo_group, validated_updates)
153 RepoGroupModel().update(self.db_repo_group, validated_updates)
156
154
157 audit_logger.store_web(
155 audit_logger.store_web(
158 'repo_group.edit', action_data={'old_data': old_values},
156 'repo_group.edit', action_data={'old_data': old_values},
159 user=c.rhodecode_user)
157 user=c.rhodecode_user)
160
158
161 Session().commit()
159 Session().commit()
162
160
163 # use the new full name for redirect once we know we updated
161 # use the new full name for redirect once we know we updated
164 # the name on filesystem and in DB
162 # the name on filesystem and in DB
165 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
163 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
166
164
167 h.flash(_('Repository Group `{}` updated successfully').format(
165 h.flash(_('Repository Group `{}` updated successfully').format(
168 old_repo_group_name), category='success')
166 old_repo_group_name), category='success')
169
167
170 except Exception:
168 except Exception:
171 log.exception("Exception during update or repository group")
169 log.exception("Exception during update or repository group")
172 h.flash(_('Error occurred during update of repository group %s')
170 h.flash(_('Error occurred during update of repository group %s')
173 % old_repo_group_name, category='error')
171 % old_repo_group_name, category='error')
174
172
175 name_changed = old_repo_group_name != new_repo_group_name
173 name_changed = old_repo_group_name != new_repo_group_name
176 if name_changed:
174 if name_changed:
177 current_perms = self.db_repo_group.permissions(expand_from_user_groups=True)
175 current_perms = self.db_repo_group.permissions(expand_from_user_groups=True)
178 affected_user_ids = [perm['user_id'] for perm in current_perms]
176 affected_user_ids = [perm['user_id'] for perm in current_perms]
179
177
180 # NOTE(marcink): also add owner maybe it has changed
178 # NOTE(marcink): also add owner maybe it has changed
181 owner = User.get_by_username(schema_data['repo_group_owner'])
179 owner = User.get_by_username(schema_data['repo_group_owner'])
182 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
183 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
184 PermissionModel().trigger_permission_flush(affected_user_ids)
182 PermissionModel().trigger_permission_flush(affected_user_ids)
185
183
186 raise HTTPFound(
184 raise HTTPFound(
187 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
185 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
@@ -1,1227 +1,1225 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
18 from rhodecode.apps._base import add_route_with_slash
21
19
22
20
23 def includeme(config):
21 def includeme(config):
24 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
22 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
25 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
23 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
26 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
24 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
27 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
25 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
28 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
26 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
29 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
27 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
30 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
28 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
31 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
29 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
32 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
30 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
33 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
31 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
34 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
32 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
35 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
33 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
36 from rhodecode.apps.repository.views.repo_files import RepoFilesView
34 from rhodecode.apps.repository.views.repo_files import RepoFilesView
37 from rhodecode.apps.repository.views.repo_forks import RepoForksView
35 from rhodecode.apps.repository.views.repo_forks import RepoForksView
38 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
36 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
39 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
37 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
40 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
38 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
41 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
39 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
42 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
40 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
43 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
41 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
44 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
42 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
45 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
43 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
46 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
44 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
47 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
45 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
48 from rhodecode.apps.repository.views.repo_strip import RepoStripView
46 from rhodecode.apps.repository.views.repo_strip import RepoStripView
49 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
47 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
50 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
48 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
51
49
52 # repo creating checks, special cases that aren't repo routes
50 # repo creating checks, special cases that aren't repo routes
53 config.add_route(
51 config.add_route(
54 name='repo_creating',
52 name='repo_creating',
55 pattern='/{repo_name:.*?[^/]}/repo_creating')
53 pattern='/{repo_name:.*?[^/]}/repo_creating')
56 config.add_view(
54 config.add_view(
57 RepoChecksView,
55 RepoChecksView,
58 attr='repo_creating',
56 attr='repo_creating',
59 route_name='repo_creating', request_method='GET',
57 route_name='repo_creating', request_method='GET',
60 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
58 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
61
59
62 config.add_route(
60 config.add_route(
63 name='repo_creating_check',
61 name='repo_creating_check',
64 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
62 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
65 config.add_view(
63 config.add_view(
66 RepoChecksView,
64 RepoChecksView,
67 attr='repo_creating_check',
65 attr='repo_creating_check',
68 route_name='repo_creating_check', request_method='GET',
66 route_name='repo_creating_check', request_method='GET',
69 renderer='json_ext')
67 renderer='json_ext')
70
68
71 # Summary
69 # Summary
72 # NOTE(marcink): one additional route is defined in very bottom, catch
70 # NOTE(marcink): one additional route is defined in very bottom, catch
73 # all pattern
71 # all pattern
74 config.add_route(
72 config.add_route(
75 name='repo_summary_explicit',
73 name='repo_summary_explicit',
76 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
74 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
77 config.add_view(
75 config.add_view(
78 RepoSummaryView,
76 RepoSummaryView,
79 attr='summary',
77 attr='summary',
80 route_name='repo_summary_explicit', request_method='GET',
78 route_name='repo_summary_explicit', request_method='GET',
81 renderer='rhodecode:templates/summary/summary.mako')
79 renderer='rhodecode:templates/summary/summary.mako')
82
80
83 config.add_route(
81 config.add_route(
84 name='repo_summary_commits',
82 name='repo_summary_commits',
85 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
83 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
86 config.add_view(
84 config.add_view(
87 RepoSummaryView,
85 RepoSummaryView,
88 attr='summary_commits',
86 attr='summary_commits',
89 route_name='repo_summary_commits', request_method='GET',
87 route_name='repo_summary_commits', request_method='GET',
90 renderer='rhodecode:templates/summary/summary_commits.mako')
88 renderer='rhodecode:templates/summary/summary_commits.mako')
91
89
92 # Commits
90 # Commits
93 config.add_route(
91 config.add_route(
94 name='repo_commit',
92 name='repo_commit',
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
93 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
96 config.add_view(
94 config.add_view(
97 RepoCommitsView,
95 RepoCommitsView,
98 attr='repo_commit_show',
96 attr='repo_commit_show',
99 route_name='repo_commit', request_method='GET',
97 route_name='repo_commit', request_method='GET',
100 renderer=None)
98 renderer=None)
101
99
102 config.add_route(
100 config.add_route(
103 name='repo_commit_children',
101 name='repo_commit_children',
104 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
102 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
105 config.add_view(
103 config.add_view(
106 RepoCommitsView,
104 RepoCommitsView,
107 attr='repo_commit_children',
105 attr='repo_commit_children',
108 route_name='repo_commit_children', request_method='GET',
106 route_name='repo_commit_children', request_method='GET',
109 renderer='json_ext', xhr=True)
107 renderer='json_ext', xhr=True)
110
108
111 config.add_route(
109 config.add_route(
112 name='repo_commit_parents',
110 name='repo_commit_parents',
113 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
111 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
114 config.add_view(
112 config.add_view(
115 RepoCommitsView,
113 RepoCommitsView,
116 attr='repo_commit_parents',
114 attr='repo_commit_parents',
117 route_name='repo_commit_parents', request_method='GET',
115 route_name='repo_commit_parents', request_method='GET',
118 renderer='json_ext')
116 renderer='json_ext')
119
117
120 config.add_route(
118 config.add_route(
121 name='repo_commit_raw',
119 name='repo_commit_raw',
122 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
120 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
123 config.add_view(
121 config.add_view(
124 RepoCommitsView,
122 RepoCommitsView,
125 attr='repo_commit_raw',
123 attr='repo_commit_raw',
126 route_name='repo_commit_raw', request_method='GET',
124 route_name='repo_commit_raw', request_method='GET',
127 renderer=None)
125 renderer=None)
128
126
129 config.add_route(
127 config.add_route(
130 name='repo_commit_patch',
128 name='repo_commit_patch',
131 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
129 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
132 config.add_view(
130 config.add_view(
133 RepoCommitsView,
131 RepoCommitsView,
134 attr='repo_commit_patch',
132 attr='repo_commit_patch',
135 route_name='repo_commit_patch', request_method='GET',
133 route_name='repo_commit_patch', request_method='GET',
136 renderer=None)
134 renderer=None)
137
135
138 config.add_route(
136 config.add_route(
139 name='repo_commit_download',
137 name='repo_commit_download',
140 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
138 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
141 config.add_view(
139 config.add_view(
142 RepoCommitsView,
140 RepoCommitsView,
143 attr='repo_commit_download',
141 attr='repo_commit_download',
144 route_name='repo_commit_download', request_method='GET',
142 route_name='repo_commit_download', request_method='GET',
145 renderer=None)
143 renderer=None)
146
144
147 config.add_route(
145 config.add_route(
148 name='repo_commit_data',
146 name='repo_commit_data',
149 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
147 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
150 config.add_view(
148 config.add_view(
151 RepoCommitsView,
149 RepoCommitsView,
152 attr='repo_commit_data',
150 attr='repo_commit_data',
153 route_name='repo_commit_data', request_method='GET',
151 route_name='repo_commit_data', request_method='GET',
154 renderer='json_ext', xhr=True)
152 renderer='json_ext', xhr=True)
155
153
156 config.add_route(
154 config.add_route(
157 name='repo_commit_comment_create',
155 name='repo_commit_comment_create',
158 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
156 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
159 config.add_view(
157 config.add_view(
160 RepoCommitsView,
158 RepoCommitsView,
161 attr='repo_commit_comment_create',
159 attr='repo_commit_comment_create',
162 route_name='repo_commit_comment_create', request_method='POST',
160 route_name='repo_commit_comment_create', request_method='POST',
163 renderer='json_ext')
161 renderer='json_ext')
164
162
165 config.add_route(
163 config.add_route(
166 name='repo_commit_comment_preview',
164 name='repo_commit_comment_preview',
167 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
165 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
168 config.add_view(
166 config.add_view(
169 RepoCommitsView,
167 RepoCommitsView,
170 attr='repo_commit_comment_preview',
168 attr='repo_commit_comment_preview',
171 route_name='repo_commit_comment_preview', request_method='POST',
169 route_name='repo_commit_comment_preview', request_method='POST',
172 renderer='string', xhr=True)
170 renderer='string', xhr=True)
173
171
174 config.add_route(
172 config.add_route(
175 name='repo_commit_comment_history_view',
173 name='repo_commit_comment_history_view',
176 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
174 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
177 config.add_view(
175 config.add_view(
178 RepoCommitsView,
176 RepoCommitsView,
179 attr='repo_commit_comment_history_view',
177 attr='repo_commit_comment_history_view',
180 route_name='repo_commit_comment_history_view', request_method='POST',
178 route_name='repo_commit_comment_history_view', request_method='POST',
181 renderer='string', xhr=True)
179 renderer='string', xhr=True)
182
180
183 config.add_route(
181 config.add_route(
184 name='repo_commit_comment_attachment_upload',
182 name='repo_commit_comment_attachment_upload',
185 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
183 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
186 config.add_view(
184 config.add_view(
187 RepoCommitsView,
185 RepoCommitsView,
188 attr='repo_commit_comment_attachment_upload',
186 attr='repo_commit_comment_attachment_upload',
189 route_name='repo_commit_comment_attachment_upload', request_method='POST',
187 route_name='repo_commit_comment_attachment_upload', request_method='POST',
190 renderer='json_ext', xhr=True)
188 renderer='json_ext', xhr=True)
191
189
192 config.add_route(
190 config.add_route(
193 name='repo_commit_comment_delete',
191 name='repo_commit_comment_delete',
194 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
192 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
195 config.add_view(
193 config.add_view(
196 RepoCommitsView,
194 RepoCommitsView,
197 attr='repo_commit_comment_delete',
195 attr='repo_commit_comment_delete',
198 route_name='repo_commit_comment_delete', request_method='POST',
196 route_name='repo_commit_comment_delete', request_method='POST',
199 renderer='json_ext')
197 renderer='json_ext')
200
198
201 config.add_route(
199 config.add_route(
202 name='repo_commit_comment_edit',
200 name='repo_commit_comment_edit',
203 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
201 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
204 config.add_view(
202 config.add_view(
205 RepoCommitsView,
203 RepoCommitsView,
206 attr='repo_commit_comment_edit',
204 attr='repo_commit_comment_edit',
207 route_name='repo_commit_comment_edit', request_method='POST',
205 route_name='repo_commit_comment_edit', request_method='POST',
208 renderer='json_ext')
206 renderer='json_ext')
209
207
210 # still working url for backward compat.
208 # still working url for backward compat.
211 config.add_route(
209 config.add_route(
212 name='repo_commit_raw_deprecated',
210 name='repo_commit_raw_deprecated',
213 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
211 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
214 config.add_view(
212 config.add_view(
215 RepoCommitsView,
213 RepoCommitsView,
216 attr='repo_commit_raw',
214 attr='repo_commit_raw',
217 route_name='repo_commit_raw_deprecated', request_method='GET',
215 route_name='repo_commit_raw_deprecated', request_method='GET',
218 renderer=None)
216 renderer=None)
219
217
220 # Files
218 # Files
221 config.add_route(
219 config.add_route(
222 name='repo_archivefile',
220 name='repo_archivefile',
223 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
221 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
224 config.add_view(
222 config.add_view(
225 RepoFilesView,
223 RepoFilesView,
226 attr='repo_archivefile',
224 attr='repo_archivefile',
227 route_name='repo_archivefile', request_method='GET',
225 route_name='repo_archivefile', request_method='GET',
228 renderer=None)
226 renderer=None)
229
227
230 config.add_route(
228 config.add_route(
231 name='repo_files_diff',
229 name='repo_files_diff',
232 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
230 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
233 config.add_view(
231 config.add_view(
234 RepoFilesView,
232 RepoFilesView,
235 attr='repo_files_diff',
233 attr='repo_files_diff',
236 route_name='repo_files_diff', request_method='GET',
234 route_name='repo_files_diff', request_method='GET',
237 renderer=None)
235 renderer=None)
238
236
239 config.add_route( # legacy route to make old links work
237 config.add_route( # legacy route to make old links work
240 name='repo_files_diff_2way_redirect',
238 name='repo_files_diff_2way_redirect',
241 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
239 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
242 config.add_view(
240 config.add_view(
243 RepoFilesView,
241 RepoFilesView,
244 attr='repo_files_diff_2way_redirect',
242 attr='repo_files_diff_2way_redirect',
245 route_name='repo_files_diff_2way_redirect', request_method='GET',
243 route_name='repo_files_diff_2way_redirect', request_method='GET',
246 renderer=None)
244 renderer=None)
247
245
248 config.add_route(
246 config.add_route(
249 name='repo_files',
247 name='repo_files',
250 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
248 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
251 config.add_view(
249 config.add_view(
252 RepoFilesView,
250 RepoFilesView,
253 attr='repo_files',
251 attr='repo_files',
254 route_name='repo_files', request_method='GET',
252 route_name='repo_files', request_method='GET',
255 renderer=None)
253 renderer=None)
256
254
257 config.add_route(
255 config.add_route(
258 name='repo_files:default_path',
256 name='repo_files:default_path',
259 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
257 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
260 config.add_view(
258 config.add_view(
261 RepoFilesView,
259 RepoFilesView,
262 attr='repo_files',
260 attr='repo_files',
263 route_name='repo_files:default_path', request_method='GET',
261 route_name='repo_files:default_path', request_method='GET',
264 renderer=None)
262 renderer=None)
265
263
266 config.add_route(
264 config.add_route(
267 name='repo_files:default_commit',
265 name='repo_files:default_commit',
268 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
266 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
269 config.add_view(
267 config.add_view(
270 RepoFilesView,
268 RepoFilesView,
271 attr='repo_files',
269 attr='repo_files',
272 route_name='repo_files:default_commit', request_method='GET',
270 route_name='repo_files:default_commit', request_method='GET',
273 renderer=None)
271 renderer=None)
274
272
275 config.add_route(
273 config.add_route(
276 name='repo_files:rendered',
274 name='repo_files:rendered',
277 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
275 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
278 config.add_view(
276 config.add_view(
279 RepoFilesView,
277 RepoFilesView,
280 attr='repo_files',
278 attr='repo_files',
281 route_name='repo_files:rendered', request_method='GET',
279 route_name='repo_files:rendered', request_method='GET',
282 renderer=None)
280 renderer=None)
283
281
284 config.add_route(
282 config.add_route(
285 name='repo_files:annotated',
283 name='repo_files:annotated',
286 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
284 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
287 config.add_view(
285 config.add_view(
288 RepoFilesView,
286 RepoFilesView,
289 attr='repo_files',
287 attr='repo_files',
290 route_name='repo_files:annotated', request_method='GET',
288 route_name='repo_files:annotated', request_method='GET',
291 renderer=None)
289 renderer=None)
292
290
293 config.add_route(
291 config.add_route(
294 name='repo_files:annotated_previous',
292 name='repo_files:annotated_previous',
295 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
293 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
296 config.add_view(
294 config.add_view(
297 RepoFilesView,
295 RepoFilesView,
298 attr='repo_files_annotated_previous',
296 attr='repo_files_annotated_previous',
299 route_name='repo_files:annotated_previous', request_method='GET',
297 route_name='repo_files:annotated_previous', request_method='GET',
300 renderer=None)
298 renderer=None)
301
299
302 config.add_route(
300 config.add_route(
303 name='repo_nodetree_full',
301 name='repo_nodetree_full',
304 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
302 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
305 config.add_view(
303 config.add_view(
306 RepoFilesView,
304 RepoFilesView,
307 attr='repo_nodetree_full',
305 attr='repo_nodetree_full',
308 route_name='repo_nodetree_full', request_method='GET',
306 route_name='repo_nodetree_full', request_method='GET',
309 renderer=None, xhr=True)
307 renderer=None, xhr=True)
310
308
311 config.add_route(
309 config.add_route(
312 name='repo_nodetree_full:default_path',
310 name='repo_nodetree_full:default_path',
313 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
311 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
314 config.add_view(
312 config.add_view(
315 RepoFilesView,
313 RepoFilesView,
316 attr='repo_nodetree_full',
314 attr='repo_nodetree_full',
317 route_name='repo_nodetree_full:default_path', request_method='GET',
315 route_name='repo_nodetree_full:default_path', request_method='GET',
318 renderer=None, xhr=True)
316 renderer=None, xhr=True)
319
317
320 config.add_route(
318 config.add_route(
321 name='repo_files_nodelist',
319 name='repo_files_nodelist',
322 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
320 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
323 config.add_view(
321 config.add_view(
324 RepoFilesView,
322 RepoFilesView,
325 attr='repo_nodelist',
323 attr='repo_nodelist',
326 route_name='repo_files_nodelist', request_method='GET',
324 route_name='repo_files_nodelist', request_method='GET',
327 renderer='json_ext', xhr=True)
325 renderer='json_ext', xhr=True)
328
326
329 config.add_route(
327 config.add_route(
330 name='repo_file_raw',
328 name='repo_file_raw',
331 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
329 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
332 config.add_view(
330 config.add_view(
333 RepoFilesView,
331 RepoFilesView,
334 attr='repo_file_raw',
332 attr='repo_file_raw',
335 route_name='repo_file_raw', request_method='GET',
333 route_name='repo_file_raw', request_method='GET',
336 renderer=None)
334 renderer=None)
337
335
338 config.add_route(
336 config.add_route(
339 name='repo_file_download',
337 name='repo_file_download',
340 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
338 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
341 config.add_view(
339 config.add_view(
342 RepoFilesView,
340 RepoFilesView,
343 attr='repo_file_download',
341 attr='repo_file_download',
344 route_name='repo_file_download', request_method='GET',
342 route_name='repo_file_download', request_method='GET',
345 renderer=None)
343 renderer=None)
346
344
347 config.add_route( # backward compat to keep old links working
345 config.add_route( # backward compat to keep old links working
348 name='repo_file_download:legacy',
346 name='repo_file_download:legacy',
349 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
347 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
350 repo_route=True)
348 repo_route=True)
351 config.add_view(
349 config.add_view(
352 RepoFilesView,
350 RepoFilesView,
353 attr='repo_file_download',
351 attr='repo_file_download',
354 route_name='repo_file_download:legacy', request_method='GET',
352 route_name='repo_file_download:legacy', request_method='GET',
355 renderer=None)
353 renderer=None)
356
354
357 config.add_route(
355 config.add_route(
358 name='repo_file_history',
356 name='repo_file_history',
359 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
357 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
360 config.add_view(
358 config.add_view(
361 RepoFilesView,
359 RepoFilesView,
362 attr='repo_file_history',
360 attr='repo_file_history',
363 route_name='repo_file_history', request_method='GET',
361 route_name='repo_file_history', request_method='GET',
364 renderer='json_ext')
362 renderer='json_ext')
365
363
366 config.add_route(
364 config.add_route(
367 name='repo_file_authors',
365 name='repo_file_authors',
368 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
366 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
369 config.add_view(
367 config.add_view(
370 RepoFilesView,
368 RepoFilesView,
371 attr='repo_file_authors',
369 attr='repo_file_authors',
372 route_name='repo_file_authors', request_method='GET',
370 route_name='repo_file_authors', request_method='GET',
373 renderer='rhodecode:templates/files/file_authors_box.mako')
371 renderer='rhodecode:templates/files/file_authors_box.mako')
374
372
375 config.add_route(
373 config.add_route(
376 name='repo_files_check_head',
374 name='repo_files_check_head',
377 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
375 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
378 repo_route=True)
376 repo_route=True)
379 config.add_view(
377 config.add_view(
380 RepoFilesView,
378 RepoFilesView,
381 attr='repo_files_check_head',
379 attr='repo_files_check_head',
382 route_name='repo_files_check_head', request_method='POST',
380 route_name='repo_files_check_head', request_method='POST',
383 renderer='json_ext', xhr=True)
381 renderer='json_ext', xhr=True)
384
382
385 config.add_route(
383 config.add_route(
386 name='repo_files_remove_file',
384 name='repo_files_remove_file',
387 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
385 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
388 repo_route=True)
386 repo_route=True)
389 config.add_view(
387 config.add_view(
390 RepoFilesView,
388 RepoFilesView,
391 attr='repo_files_remove_file',
389 attr='repo_files_remove_file',
392 route_name='repo_files_remove_file', request_method='GET',
390 route_name='repo_files_remove_file', request_method='GET',
393 renderer='rhodecode:templates/files/files_delete.mako')
391 renderer='rhodecode:templates/files/files_delete.mako')
394
392
395 config.add_route(
393 config.add_route(
396 name='repo_files_delete_file',
394 name='repo_files_delete_file',
397 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
395 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
398 repo_route=True)
396 repo_route=True)
399 config.add_view(
397 config.add_view(
400 RepoFilesView,
398 RepoFilesView,
401 attr='repo_files_delete_file',
399 attr='repo_files_delete_file',
402 route_name='repo_files_delete_file', request_method='POST',
400 route_name='repo_files_delete_file', request_method='POST',
403 renderer=None)
401 renderer=None)
404
402
405 config.add_route(
403 config.add_route(
406 name='repo_files_edit_file',
404 name='repo_files_edit_file',
407 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
405 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
408 repo_route=True)
406 repo_route=True)
409 config.add_view(
407 config.add_view(
410 RepoFilesView,
408 RepoFilesView,
411 attr='repo_files_edit_file',
409 attr='repo_files_edit_file',
412 route_name='repo_files_edit_file', request_method='GET',
410 route_name='repo_files_edit_file', request_method='GET',
413 renderer='rhodecode:templates/files/files_edit.mako')
411 renderer='rhodecode:templates/files/files_edit.mako')
414
412
415 config.add_route(
413 config.add_route(
416 name='repo_files_update_file',
414 name='repo_files_update_file',
417 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
415 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
418 repo_route=True)
416 repo_route=True)
419 config.add_view(
417 config.add_view(
420 RepoFilesView,
418 RepoFilesView,
421 attr='repo_files_update_file',
419 attr='repo_files_update_file',
422 route_name='repo_files_update_file', request_method='POST',
420 route_name='repo_files_update_file', request_method='POST',
423 renderer=None)
421 renderer=None)
424
422
425 config.add_route(
423 config.add_route(
426 name='repo_files_add_file',
424 name='repo_files_add_file',
427 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
425 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
428 repo_route=True)
426 repo_route=True)
429 config.add_view(
427 config.add_view(
430 RepoFilesView,
428 RepoFilesView,
431 attr='repo_files_add_file',
429 attr='repo_files_add_file',
432 route_name='repo_files_add_file', request_method='GET',
430 route_name='repo_files_add_file', request_method='GET',
433 renderer='rhodecode:templates/files/files_add.mako')
431 renderer='rhodecode:templates/files/files_add.mako')
434
432
435 config.add_route(
433 config.add_route(
436 name='repo_files_upload_file',
434 name='repo_files_upload_file',
437 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
435 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
438 repo_route=True)
436 repo_route=True)
439 config.add_view(
437 config.add_view(
440 RepoFilesView,
438 RepoFilesView,
441 attr='repo_files_add_file',
439 attr='repo_files_add_file',
442 route_name='repo_files_upload_file', request_method='GET',
440 route_name='repo_files_upload_file', request_method='GET',
443 renderer='rhodecode:templates/files/files_upload.mako')
441 renderer='rhodecode:templates/files/files_upload.mako')
444 config.add_view( # POST creates
442 config.add_view( # POST creates
445 RepoFilesView,
443 RepoFilesView,
446 attr='repo_files_upload_file',
444 attr='repo_files_upload_file',
447 route_name='repo_files_upload_file', request_method='POST',
445 route_name='repo_files_upload_file', request_method='POST',
448 renderer='json_ext')
446 renderer='json_ext')
449
447
450 config.add_route(
448 config.add_route(
451 name='repo_files_create_file',
449 name='repo_files_create_file',
452 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
450 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
453 repo_route=True)
451 repo_route=True)
454 config.add_view( # POST creates
452 config.add_view( # POST creates
455 RepoFilesView,
453 RepoFilesView,
456 attr='repo_files_create_file',
454 attr='repo_files_create_file',
457 route_name='repo_files_create_file', request_method='POST',
455 route_name='repo_files_create_file', request_method='POST',
458 renderer=None)
456 renderer=None)
459
457
460 # Refs data
458 # Refs data
461 config.add_route(
459 config.add_route(
462 name='repo_refs_data',
460 name='repo_refs_data',
463 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
461 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
464 config.add_view(
462 config.add_view(
465 RepoSummaryView,
463 RepoSummaryView,
466 attr='repo_refs_data',
464 attr='repo_refs_data',
467 route_name='repo_refs_data', request_method='GET',
465 route_name='repo_refs_data', request_method='GET',
468 renderer='json_ext')
466 renderer='json_ext')
469
467
470 config.add_route(
468 config.add_route(
471 name='repo_refs_changelog_data',
469 name='repo_refs_changelog_data',
472 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
470 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
473 config.add_view(
471 config.add_view(
474 RepoSummaryView,
472 RepoSummaryView,
475 attr='repo_refs_changelog_data',
473 attr='repo_refs_changelog_data',
476 route_name='repo_refs_changelog_data', request_method='GET',
474 route_name='repo_refs_changelog_data', request_method='GET',
477 renderer='json_ext')
475 renderer='json_ext')
478
476
479 config.add_route(
477 config.add_route(
480 name='repo_stats',
478 name='repo_stats',
481 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
479 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
482 config.add_view(
480 config.add_view(
483 RepoSummaryView,
481 RepoSummaryView,
484 attr='repo_stats',
482 attr='repo_stats',
485 route_name='repo_stats', request_method='GET',
483 route_name='repo_stats', request_method='GET',
486 renderer='json_ext')
484 renderer='json_ext')
487
485
488 # Commits
486 # Commits
489 config.add_route(
487 config.add_route(
490 name='repo_commits',
488 name='repo_commits',
491 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
489 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
492 config.add_view(
490 config.add_view(
493 RepoChangelogView,
491 RepoChangelogView,
494 attr='repo_changelog',
492 attr='repo_changelog',
495 route_name='repo_commits', request_method='GET',
493 route_name='repo_commits', request_method='GET',
496 renderer='rhodecode:templates/commits/changelog.mako')
494 renderer='rhodecode:templates/commits/changelog.mako')
497 # old routes for backward compat
495 # old routes for backward compat
498 config.add_view(
496 config.add_view(
499 RepoChangelogView,
497 RepoChangelogView,
500 attr='repo_changelog',
498 attr='repo_changelog',
501 route_name='repo_changelog', request_method='GET',
499 route_name='repo_changelog', request_method='GET',
502 renderer='rhodecode:templates/commits/changelog.mako')
500 renderer='rhodecode:templates/commits/changelog.mako')
503
501
504 config.add_route(
502 config.add_route(
505 name='repo_commits_elements',
503 name='repo_commits_elements',
506 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
504 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
507 config.add_view(
505 config.add_view(
508 RepoChangelogView,
506 RepoChangelogView,
509 attr='repo_commits_elements',
507 attr='repo_commits_elements',
510 route_name='repo_commits_elements', request_method=('GET', 'POST'),
508 route_name='repo_commits_elements', request_method=('GET', 'POST'),
511 renderer='rhodecode:templates/commits/changelog_elements.mako',
509 renderer='rhodecode:templates/commits/changelog_elements.mako',
512 xhr=True)
510 xhr=True)
513
511
514 config.add_route(
512 config.add_route(
515 name='repo_commits_elements_file',
513 name='repo_commits_elements_file',
516 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
514 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
517 config.add_view(
515 config.add_view(
518 RepoChangelogView,
516 RepoChangelogView,
519 attr='repo_commits_elements',
517 attr='repo_commits_elements',
520 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
518 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
521 renderer='rhodecode:templates/commits/changelog_elements.mako',
519 renderer='rhodecode:templates/commits/changelog_elements.mako',
522 xhr=True)
520 xhr=True)
523
521
524 config.add_route(
522 config.add_route(
525 name='repo_commits_file',
523 name='repo_commits_file',
526 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
524 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
527 config.add_view(
525 config.add_view(
528 RepoChangelogView,
526 RepoChangelogView,
529 attr='repo_changelog',
527 attr='repo_changelog',
530 route_name='repo_commits_file', request_method='GET',
528 route_name='repo_commits_file', request_method='GET',
531 renderer='rhodecode:templates/commits/changelog.mako')
529 renderer='rhodecode:templates/commits/changelog.mako')
532 # old routes for backward compat
530 # old routes for backward compat
533 config.add_view(
531 config.add_view(
534 RepoChangelogView,
532 RepoChangelogView,
535 attr='repo_changelog',
533 attr='repo_changelog',
536 route_name='repo_changelog_file', request_method='GET',
534 route_name='repo_changelog_file', request_method='GET',
537 renderer='rhodecode:templates/commits/changelog.mako')
535 renderer='rhodecode:templates/commits/changelog.mako')
538
536
539 # Changelog (old deprecated name for commits page)
537 # Changelog (old deprecated name for commits page)
540 config.add_route(
538 config.add_route(
541 name='repo_changelog',
539 name='repo_changelog',
542 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
540 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
543 config.add_route(
541 config.add_route(
544 name='repo_changelog_file',
542 name='repo_changelog_file',
545 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
543 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
546
544
547 # Compare
545 # Compare
548 config.add_route(
546 config.add_route(
549 name='repo_compare_select',
547 name='repo_compare_select',
550 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
548 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
551 config.add_view(
549 config.add_view(
552 RepoCompareView,
550 RepoCompareView,
553 attr='compare_select',
551 attr='compare_select',
554 route_name='repo_compare_select', request_method='GET',
552 route_name='repo_compare_select', request_method='GET',
555 renderer='rhodecode:templates/compare/compare_diff.mako')
553 renderer='rhodecode:templates/compare/compare_diff.mako')
556
554
557 config.add_route(
555 config.add_route(
558 name='repo_compare',
556 name='repo_compare',
559 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
557 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
560 config.add_view(
558 config.add_view(
561 RepoCompareView,
559 RepoCompareView,
562 attr='compare',
560 attr='compare',
563 route_name='repo_compare', request_method='GET',
561 route_name='repo_compare', request_method='GET',
564 renderer=None)
562 renderer=None)
565
563
566 # Tags
564 # Tags
567 config.add_route(
565 config.add_route(
568 name='tags_home',
566 name='tags_home',
569 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
567 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
570 config.add_view(
568 config.add_view(
571 RepoTagsView,
569 RepoTagsView,
572 attr='tags',
570 attr='tags',
573 route_name='tags_home', request_method='GET',
571 route_name='tags_home', request_method='GET',
574 renderer='rhodecode:templates/tags/tags.mako')
572 renderer='rhodecode:templates/tags/tags.mako')
575
573
576 # Branches
574 # Branches
577 config.add_route(
575 config.add_route(
578 name='branches_home',
576 name='branches_home',
579 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
577 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
580 config.add_view(
578 config.add_view(
581 RepoBranchesView,
579 RepoBranchesView,
582 attr='branches',
580 attr='branches',
583 route_name='branches_home', request_method='GET',
581 route_name='branches_home', request_method='GET',
584 renderer='rhodecode:templates/branches/branches.mako')
582 renderer='rhodecode:templates/branches/branches.mako')
585
583
586 # Bookmarks
584 # Bookmarks
587 config.add_route(
585 config.add_route(
588 name='bookmarks_home',
586 name='bookmarks_home',
589 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
587 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
590 config.add_view(
588 config.add_view(
591 RepoBookmarksView,
589 RepoBookmarksView,
592 attr='bookmarks',
590 attr='bookmarks',
593 route_name='bookmarks_home', request_method='GET',
591 route_name='bookmarks_home', request_method='GET',
594 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
592 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
595
593
596 # Forks
594 # Forks
597 config.add_route(
595 config.add_route(
598 name='repo_fork_new',
596 name='repo_fork_new',
599 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
597 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
600 repo_forbid_when_archived=True,
598 repo_forbid_when_archived=True,
601 repo_accepted_types=['hg', 'git'])
599 repo_accepted_types=['hg', 'git'])
602 config.add_view(
600 config.add_view(
603 RepoForksView,
601 RepoForksView,
604 attr='repo_fork_new',
602 attr='repo_fork_new',
605 route_name='repo_fork_new', request_method='GET',
603 route_name='repo_fork_new', request_method='GET',
606 renderer='rhodecode:templates/forks/forks.mako')
604 renderer='rhodecode:templates/forks/forks.mako')
607
605
608 config.add_route(
606 config.add_route(
609 name='repo_fork_create',
607 name='repo_fork_create',
610 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
608 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
611 repo_forbid_when_archived=True,
609 repo_forbid_when_archived=True,
612 repo_accepted_types=['hg', 'git'])
610 repo_accepted_types=['hg', 'git'])
613 config.add_view(
611 config.add_view(
614 RepoForksView,
612 RepoForksView,
615 attr='repo_fork_create',
613 attr='repo_fork_create',
616 route_name='repo_fork_create', request_method='POST',
614 route_name='repo_fork_create', request_method='POST',
617 renderer='rhodecode:templates/forks/fork.mako')
615 renderer='rhodecode:templates/forks/fork.mako')
618
616
619 config.add_route(
617 config.add_route(
620 name='repo_forks_show_all',
618 name='repo_forks_show_all',
621 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
619 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
622 repo_accepted_types=['hg', 'git'])
620 repo_accepted_types=['hg', 'git'])
623 config.add_view(
621 config.add_view(
624 RepoForksView,
622 RepoForksView,
625 attr='repo_forks_show_all',
623 attr='repo_forks_show_all',
626 route_name='repo_forks_show_all', request_method='GET',
624 route_name='repo_forks_show_all', request_method='GET',
627 renderer='rhodecode:templates/forks/forks.mako')
625 renderer='rhodecode:templates/forks/forks.mako')
628
626
629 config.add_route(
627 config.add_route(
630 name='repo_forks_data',
628 name='repo_forks_data',
631 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
629 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
632 repo_accepted_types=['hg', 'git'])
630 repo_accepted_types=['hg', 'git'])
633 config.add_view(
631 config.add_view(
634 RepoForksView,
632 RepoForksView,
635 attr='repo_forks_data',
633 attr='repo_forks_data',
636 route_name='repo_forks_data', request_method='GET',
634 route_name='repo_forks_data', request_method='GET',
637 renderer='json_ext', xhr=True)
635 renderer='json_ext', xhr=True)
638
636
639 # Pull Requests
637 # Pull Requests
640 config.add_route(
638 config.add_route(
641 name='pullrequest_show',
639 name='pullrequest_show',
642 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
640 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
643 repo_route=True)
641 repo_route=True)
644 config.add_view(
642 config.add_view(
645 RepoPullRequestsView,
643 RepoPullRequestsView,
646 attr='pull_request_show',
644 attr='pull_request_show',
647 route_name='pullrequest_show', request_method='GET',
645 route_name='pullrequest_show', request_method='GET',
648 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
646 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
649
647
650 config.add_route(
648 config.add_route(
651 name='pullrequest_show_all',
649 name='pullrequest_show_all',
652 pattern='/{repo_name:.*?[^/]}/pull-request',
650 pattern='/{repo_name:.*?[^/]}/pull-request',
653 repo_route=True, repo_accepted_types=['hg', 'git'])
651 repo_route=True, repo_accepted_types=['hg', 'git'])
654 config.add_view(
652 config.add_view(
655 RepoPullRequestsView,
653 RepoPullRequestsView,
656 attr='pull_request_list',
654 attr='pull_request_list',
657 route_name='pullrequest_show_all', request_method='GET',
655 route_name='pullrequest_show_all', request_method='GET',
658 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
656 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
659
657
660 config.add_route(
658 config.add_route(
661 name='pullrequest_show_all_data',
659 name='pullrequest_show_all_data',
662 pattern='/{repo_name:.*?[^/]}/pull-request-data',
660 pattern='/{repo_name:.*?[^/]}/pull-request-data',
663 repo_route=True, repo_accepted_types=['hg', 'git'])
661 repo_route=True, repo_accepted_types=['hg', 'git'])
664 config.add_view(
662 config.add_view(
665 RepoPullRequestsView,
663 RepoPullRequestsView,
666 attr='pull_request_list_data',
664 attr='pull_request_list_data',
667 route_name='pullrequest_show_all_data', request_method='GET',
665 route_name='pullrequest_show_all_data', request_method='GET',
668 renderer='json_ext', xhr=True)
666 renderer='json_ext', xhr=True)
669
667
670 config.add_route(
668 config.add_route(
671 name='pullrequest_repo_refs',
669 name='pullrequest_repo_refs',
672 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
670 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
673 repo_route=True)
671 repo_route=True)
674 config.add_view(
672 config.add_view(
675 RepoPullRequestsView,
673 RepoPullRequestsView,
676 attr='pull_request_repo_refs',
674 attr='pull_request_repo_refs',
677 route_name='pullrequest_repo_refs', request_method='GET',
675 route_name='pullrequest_repo_refs', request_method='GET',
678 renderer='json_ext', xhr=True)
676 renderer='json_ext', xhr=True)
679
677
680 config.add_route(
678 config.add_route(
681 name='pullrequest_repo_targets',
679 name='pullrequest_repo_targets',
682 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
680 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
683 repo_route=True)
681 repo_route=True)
684 config.add_view(
682 config.add_view(
685 RepoPullRequestsView,
683 RepoPullRequestsView,
686 attr='pullrequest_repo_targets',
684 attr='pullrequest_repo_targets',
687 route_name='pullrequest_repo_targets', request_method='GET',
685 route_name='pullrequest_repo_targets', request_method='GET',
688 renderer='json_ext', xhr=True)
686 renderer='json_ext', xhr=True)
689
687
690 config.add_route(
688 config.add_route(
691 name='pullrequest_new',
689 name='pullrequest_new',
692 pattern='/{repo_name:.*?[^/]}/pull-request/new',
690 pattern='/{repo_name:.*?[^/]}/pull-request/new',
693 repo_route=True, repo_accepted_types=['hg', 'git'],
691 repo_route=True, repo_accepted_types=['hg', 'git'],
694 repo_forbid_when_archived=True)
692 repo_forbid_when_archived=True)
695 config.add_view(
693 config.add_view(
696 RepoPullRequestsView,
694 RepoPullRequestsView,
697 attr='pull_request_new',
695 attr='pull_request_new',
698 route_name='pullrequest_new', request_method='GET',
696 route_name='pullrequest_new', request_method='GET',
699 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
697 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
700
698
701 config.add_route(
699 config.add_route(
702 name='pullrequest_create',
700 name='pullrequest_create',
703 pattern='/{repo_name:.*?[^/]}/pull-request/create',
701 pattern='/{repo_name:.*?[^/]}/pull-request/create',
704 repo_route=True, repo_accepted_types=['hg', 'git'],
702 repo_route=True, repo_accepted_types=['hg', 'git'],
705 repo_forbid_when_archived=True)
703 repo_forbid_when_archived=True)
706 config.add_view(
704 config.add_view(
707 RepoPullRequestsView,
705 RepoPullRequestsView,
708 attr='pull_request_create',
706 attr='pull_request_create',
709 route_name='pullrequest_create', request_method='POST',
707 route_name='pullrequest_create', request_method='POST',
710 renderer=None)
708 renderer=None)
711
709
712 config.add_route(
710 config.add_route(
713 name='pullrequest_update',
711 name='pullrequest_update',
714 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
712 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
715 repo_route=True, repo_forbid_when_archived=True)
713 repo_route=True, repo_forbid_when_archived=True)
716 config.add_view(
714 config.add_view(
717 RepoPullRequestsView,
715 RepoPullRequestsView,
718 attr='pull_request_update',
716 attr='pull_request_update',
719 route_name='pullrequest_update', request_method='POST',
717 route_name='pullrequest_update', request_method='POST',
720 renderer='json_ext')
718 renderer='json_ext')
721
719
722 config.add_route(
720 config.add_route(
723 name='pullrequest_merge',
721 name='pullrequest_merge',
724 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
722 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
725 repo_route=True, repo_forbid_when_archived=True)
723 repo_route=True, repo_forbid_when_archived=True)
726 config.add_view(
724 config.add_view(
727 RepoPullRequestsView,
725 RepoPullRequestsView,
728 attr='pull_request_merge',
726 attr='pull_request_merge',
729 route_name='pullrequest_merge', request_method='POST',
727 route_name='pullrequest_merge', request_method='POST',
730 renderer='json_ext')
728 renderer='json_ext')
731
729
732 config.add_route(
730 config.add_route(
733 name='pullrequest_delete',
731 name='pullrequest_delete',
734 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
732 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
735 repo_route=True, repo_forbid_when_archived=True)
733 repo_route=True, repo_forbid_when_archived=True)
736 config.add_view(
734 config.add_view(
737 RepoPullRequestsView,
735 RepoPullRequestsView,
738 attr='pull_request_delete',
736 attr='pull_request_delete',
739 route_name='pullrequest_delete', request_method='POST',
737 route_name='pullrequest_delete', request_method='POST',
740 renderer='json_ext')
738 renderer='json_ext')
741
739
742 config.add_route(
740 config.add_route(
743 name='pullrequest_comment_create',
741 name='pullrequest_comment_create',
744 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
742 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
745 repo_route=True)
743 repo_route=True)
746 config.add_view(
744 config.add_view(
747 RepoPullRequestsView,
745 RepoPullRequestsView,
748 attr='pull_request_comment_create',
746 attr='pull_request_comment_create',
749 route_name='pullrequest_comment_create', request_method='POST',
747 route_name='pullrequest_comment_create', request_method='POST',
750 renderer='json_ext')
748 renderer='json_ext')
751
749
752 config.add_route(
750 config.add_route(
753 name='pullrequest_comment_edit',
751 name='pullrequest_comment_edit',
754 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
752 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
755 repo_route=True, repo_accepted_types=['hg', 'git'])
753 repo_route=True, repo_accepted_types=['hg', 'git'])
756 config.add_view(
754 config.add_view(
757 RepoPullRequestsView,
755 RepoPullRequestsView,
758 attr='pull_request_comment_edit',
756 attr='pull_request_comment_edit',
759 route_name='pullrequest_comment_edit', request_method='POST',
757 route_name='pullrequest_comment_edit', request_method='POST',
760 renderer='json_ext')
758 renderer='json_ext')
761
759
762 config.add_route(
760 config.add_route(
763 name='pullrequest_comment_delete',
761 name='pullrequest_comment_delete',
764 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
762 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
765 repo_route=True, repo_accepted_types=['hg', 'git'])
763 repo_route=True, repo_accepted_types=['hg', 'git'])
766 config.add_view(
764 config.add_view(
767 RepoPullRequestsView,
765 RepoPullRequestsView,
768 attr='pull_request_comment_delete',
766 attr='pull_request_comment_delete',
769 route_name='pullrequest_comment_delete', request_method='POST',
767 route_name='pullrequest_comment_delete', request_method='POST',
770 renderer='json_ext')
768 renderer='json_ext')
771
769
772 config.add_route(
770 config.add_route(
773 name='pullrequest_comments',
771 name='pullrequest_comments',
774 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
772 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
775 repo_route=True)
773 repo_route=True)
776 config.add_view(
774 config.add_view(
777 RepoPullRequestsView,
775 RepoPullRequestsView,
778 attr='pullrequest_comments',
776 attr='pullrequest_comments',
779 route_name='pullrequest_comments', request_method='POST',
777 route_name='pullrequest_comments', request_method='POST',
780 renderer='string_html', xhr=True)
778 renderer='string_html', xhr=True)
781
779
782 config.add_route(
780 config.add_route(
783 name='pullrequest_todos',
781 name='pullrequest_todos',
784 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
782 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
785 repo_route=True)
783 repo_route=True)
786 config.add_view(
784 config.add_view(
787 RepoPullRequestsView,
785 RepoPullRequestsView,
788 attr='pullrequest_todos',
786 attr='pullrequest_todos',
789 route_name='pullrequest_todos', request_method='POST',
787 route_name='pullrequest_todos', request_method='POST',
790 renderer='string_html', xhr=True)
788 renderer='string_html', xhr=True)
791
789
792 config.add_route(
790 config.add_route(
793 name='pullrequest_drafts',
791 name='pullrequest_drafts',
794 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
792 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
795 repo_route=True)
793 repo_route=True)
796 config.add_view(
794 config.add_view(
797 RepoPullRequestsView,
795 RepoPullRequestsView,
798 attr='pullrequest_drafts',
796 attr='pullrequest_drafts',
799 route_name='pullrequest_drafts', request_method='POST',
797 route_name='pullrequest_drafts', request_method='POST',
800 renderer='string_html', xhr=True)
798 renderer='string_html', xhr=True)
801
799
802 # Artifacts, (EE feature)
800 # Artifacts, (EE feature)
803 config.add_route(
801 config.add_route(
804 name='repo_artifacts_list',
802 name='repo_artifacts_list',
805 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
803 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
806 config.add_view(
804 config.add_view(
807 RepoArtifactsView,
805 RepoArtifactsView,
808 attr='repo_artifacts',
806 attr='repo_artifacts',
809 route_name='repo_artifacts_list', request_method='GET',
807 route_name='repo_artifacts_list', request_method='GET',
810 renderer='rhodecode:templates/artifacts/artifact_list.mako')
808 renderer='rhodecode:templates/artifacts/artifact_list.mako')
811
809
812 # Settings
810 # Settings
813 config.add_route(
811 config.add_route(
814 name='edit_repo',
812 name='edit_repo',
815 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
813 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
816 config.add_view(
814 config.add_view(
817 RepoSettingsView,
815 RepoSettingsView,
818 attr='edit_settings',
816 attr='edit_settings',
819 route_name='edit_repo', request_method='GET',
817 route_name='edit_repo', request_method='GET',
820 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
818 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
821 # update is POST on edit_repo
819 # update is POST on edit_repo
822 config.add_view(
820 config.add_view(
823 RepoSettingsView,
821 RepoSettingsView,
824 attr='edit_settings_update',
822 attr='edit_settings_update',
825 route_name='edit_repo', request_method='POST',
823 route_name='edit_repo', request_method='POST',
826 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
824 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
827
825
828 # Settings advanced
826 # Settings advanced
829 config.add_route(
827 config.add_route(
830 name='edit_repo_advanced',
828 name='edit_repo_advanced',
831 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
829 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
832 config.add_view(
830 config.add_view(
833 RepoSettingsAdvancedView,
831 RepoSettingsAdvancedView,
834 attr='edit_advanced',
832 attr='edit_advanced',
835 route_name='edit_repo_advanced', request_method='GET',
833 route_name='edit_repo_advanced', request_method='GET',
836 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
834 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
837
835
838 config.add_route(
836 config.add_route(
839 name='edit_repo_advanced_archive',
837 name='edit_repo_advanced_archive',
840 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
838 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
841 config.add_view(
839 config.add_view(
842 RepoSettingsAdvancedView,
840 RepoSettingsAdvancedView,
843 attr='edit_advanced_archive',
841 attr='edit_advanced_archive',
844 route_name='edit_repo_advanced_archive', request_method='POST',
842 route_name='edit_repo_advanced_archive', request_method='POST',
845 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
843 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
846
844
847 config.add_route(
845 config.add_route(
848 name='edit_repo_advanced_delete',
846 name='edit_repo_advanced_delete',
849 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
847 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
850 config.add_view(
848 config.add_view(
851 RepoSettingsAdvancedView,
849 RepoSettingsAdvancedView,
852 attr='edit_advanced_delete',
850 attr='edit_advanced_delete',
853 route_name='edit_repo_advanced_delete', request_method='POST',
851 route_name='edit_repo_advanced_delete', request_method='POST',
854 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
852 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
855
853
856 config.add_route(
854 config.add_route(
857 name='edit_repo_advanced_locking',
855 name='edit_repo_advanced_locking',
858 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
856 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
859 config.add_view(
857 config.add_view(
860 RepoSettingsAdvancedView,
858 RepoSettingsAdvancedView,
861 attr='edit_advanced_toggle_locking',
859 attr='edit_advanced_toggle_locking',
862 route_name='edit_repo_advanced_locking', request_method='POST',
860 route_name='edit_repo_advanced_locking', request_method='POST',
863 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
861 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
864
862
865 config.add_route(
863 config.add_route(
866 name='edit_repo_advanced_journal',
864 name='edit_repo_advanced_journal',
867 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
865 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
868 config.add_view(
866 config.add_view(
869 RepoSettingsAdvancedView,
867 RepoSettingsAdvancedView,
870 attr='edit_advanced_journal',
868 attr='edit_advanced_journal',
871 route_name='edit_repo_advanced_journal', request_method='POST',
869 route_name='edit_repo_advanced_journal', request_method='POST',
872 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
870 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
873
871
874 config.add_route(
872 config.add_route(
875 name='edit_repo_advanced_fork',
873 name='edit_repo_advanced_fork',
876 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
874 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
877 config.add_view(
875 config.add_view(
878 RepoSettingsAdvancedView,
876 RepoSettingsAdvancedView,
879 attr='edit_advanced_fork',
877 attr='edit_advanced_fork',
880 route_name='edit_repo_advanced_fork', request_method='POST',
878 route_name='edit_repo_advanced_fork', request_method='POST',
881 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
879 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
882
880
883 config.add_route(
881 config.add_route(
884 name='edit_repo_advanced_hooks',
882 name='edit_repo_advanced_hooks',
885 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
883 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
886 config.add_view(
884 config.add_view(
887 RepoSettingsAdvancedView,
885 RepoSettingsAdvancedView,
888 attr='edit_advanced_install_hooks',
886 attr='edit_advanced_install_hooks',
889 route_name='edit_repo_advanced_hooks', request_method='GET',
887 route_name='edit_repo_advanced_hooks', request_method='GET',
890 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
888 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
891
889
892 # Caches
890 # Caches
893 config.add_route(
891 config.add_route(
894 name='edit_repo_caches',
892 name='edit_repo_caches',
895 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
893 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
896 config.add_view(
894 config.add_view(
897 RepoCachesView,
895 RepoCachesView,
898 attr='repo_caches',
896 attr='repo_caches',
899 route_name='edit_repo_caches', request_method='GET',
897 route_name='edit_repo_caches', request_method='GET',
900 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
898 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
901 config.add_view(
899 config.add_view(
902 RepoCachesView,
900 RepoCachesView,
903 attr='repo_caches_purge',
901 attr='repo_caches_purge',
904 route_name='edit_repo_caches', request_method='POST')
902 route_name='edit_repo_caches', request_method='POST')
905
903
906 # Permissions
904 # Permissions
907 config.add_route(
905 config.add_route(
908 name='edit_repo_perms',
906 name='edit_repo_perms',
909 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
907 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
910 config.add_view(
908 config.add_view(
911 RepoSettingsPermissionsView,
909 RepoSettingsPermissionsView,
912 attr='edit_permissions',
910 attr='edit_permissions',
913 route_name='edit_repo_perms', request_method='GET',
911 route_name='edit_repo_perms', request_method='GET',
914 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
912 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
915 config.add_view(
913 config.add_view(
916 RepoSettingsPermissionsView,
914 RepoSettingsPermissionsView,
917 attr='edit_permissions_update',
915 attr='edit_permissions_update',
918 route_name='edit_repo_perms', request_method='POST',
916 route_name='edit_repo_perms', request_method='POST',
919 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
917 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
920
918
921 config.add_route(
919 config.add_route(
922 name='edit_repo_perms_set_private',
920 name='edit_repo_perms_set_private',
923 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
921 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
924 config.add_view(
922 config.add_view(
925 RepoSettingsPermissionsView,
923 RepoSettingsPermissionsView,
926 attr='edit_permissions_set_private_repo',
924 attr='edit_permissions_set_private_repo',
927 route_name='edit_repo_perms_set_private', request_method='POST',
925 route_name='edit_repo_perms_set_private', request_method='POST',
928 renderer='json_ext')
926 renderer='json_ext')
929
927
930 # Permissions Branch (EE feature)
928 # Permissions Branch (EE feature)
931 config.add_route(
929 config.add_route(
932 name='edit_repo_perms_branch',
930 name='edit_repo_perms_branch',
933 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
931 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
934 config.add_view(
932 config.add_view(
935 RepoSettingsBranchPermissionsView,
933 RepoSettingsBranchPermissionsView,
936 attr='branch_permissions',
934 attr='branch_permissions',
937 route_name='edit_repo_perms_branch', request_method='GET',
935 route_name='edit_repo_perms_branch', request_method='GET',
938 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
936 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
939
937
940 config.add_route(
938 config.add_route(
941 name='edit_repo_perms_branch_delete',
939 name='edit_repo_perms_branch_delete',
942 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
940 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
943 repo_route=True)
941 repo_route=True)
944 ## Only implemented in EE
942 ## Only implemented in EE
945
943
946 # Maintenance
944 # Maintenance
947 config.add_route(
945 config.add_route(
948 name='edit_repo_maintenance',
946 name='edit_repo_maintenance',
949 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
947 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
950 config.add_view(
948 config.add_view(
951 RepoMaintenanceView,
949 RepoMaintenanceView,
952 attr='repo_maintenance',
950 attr='repo_maintenance',
953 route_name='edit_repo_maintenance', request_method='GET',
951 route_name='edit_repo_maintenance', request_method='GET',
954 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
952 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
955
953
956 config.add_route(
954 config.add_route(
957 name='edit_repo_maintenance_execute',
955 name='edit_repo_maintenance_execute',
958 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
956 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
959 config.add_view(
957 config.add_view(
960 RepoMaintenanceView,
958 RepoMaintenanceView,
961 attr='repo_maintenance_execute',
959 attr='repo_maintenance_execute',
962 route_name='edit_repo_maintenance_execute', request_method='GET',
960 route_name='edit_repo_maintenance_execute', request_method='GET',
963 renderer='json', xhr=True)
961 renderer='json', xhr=True)
964
962
965 # Fields
963 # Fields
966 config.add_route(
964 config.add_route(
967 name='edit_repo_fields',
965 name='edit_repo_fields',
968 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
966 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
969 config.add_view(
967 config.add_view(
970 RepoSettingsFieldsView,
968 RepoSettingsFieldsView,
971 attr='repo_field_edit',
969 attr='repo_field_edit',
972 route_name='edit_repo_fields', request_method='GET',
970 route_name='edit_repo_fields', request_method='GET',
973 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
971 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
974
972
975 config.add_route(
973 config.add_route(
976 name='edit_repo_fields_create',
974 name='edit_repo_fields_create',
977 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
975 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
978 config.add_view(
976 config.add_view(
979 RepoSettingsFieldsView,
977 RepoSettingsFieldsView,
980 attr='repo_field_create',
978 attr='repo_field_create',
981 route_name='edit_repo_fields_create', request_method='POST',
979 route_name='edit_repo_fields_create', request_method='POST',
982 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
980 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
983
981
984 config.add_route(
982 config.add_route(
985 name='edit_repo_fields_delete',
983 name='edit_repo_fields_delete',
986 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
984 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
987 config.add_view(
985 config.add_view(
988 RepoSettingsFieldsView,
986 RepoSettingsFieldsView,
989 attr='repo_field_delete',
987 attr='repo_field_delete',
990 route_name='edit_repo_fields_delete', request_method='POST',
988 route_name='edit_repo_fields_delete', request_method='POST',
991 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
989 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
992
990
993 # quick actions: locking
991 # quick actions: locking
994 config.add_route(
992 config.add_route(
995 name='repo_settings_quick_actions',
993 name='repo_settings_quick_actions',
996 pattern='/{repo_name:.*?[^/]}/settings/quick-action', repo_route=True)
994 pattern='/{repo_name:.*?[^/]}/settings/quick-action', repo_route=True)
997 config.add_view(
995 config.add_view(
998 RepoSettingsView,
996 RepoSettingsView,
999 attr='repo_settings_quick_actions',
997 attr='repo_settings_quick_actions',
1000 route_name='repo_settings_quick_actions', request_method='GET',
998 route_name='repo_settings_quick_actions', request_method='GET',
1001 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
999 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1002
1000
1003 # Remote
1001 # Remote
1004 config.add_route(
1002 config.add_route(
1005 name='edit_repo_remote',
1003 name='edit_repo_remote',
1006 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1004 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1007 config.add_view(
1005 config.add_view(
1008 RepoSettingsRemoteView,
1006 RepoSettingsRemoteView,
1009 attr='repo_remote_edit_form',
1007 attr='repo_remote_edit_form',
1010 route_name='edit_repo_remote', request_method='GET',
1008 route_name='edit_repo_remote', request_method='GET',
1011 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1009 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1012
1010
1013 config.add_route(
1011 config.add_route(
1014 name='edit_repo_remote_pull',
1012 name='edit_repo_remote_pull',
1015 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1013 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1016 config.add_view(
1014 config.add_view(
1017 RepoSettingsRemoteView,
1015 RepoSettingsRemoteView,
1018 attr='repo_remote_pull_changes',
1016 attr='repo_remote_pull_changes',
1019 route_name='edit_repo_remote_pull', request_method='POST',
1017 route_name='edit_repo_remote_pull', request_method='POST',
1020 renderer=None)
1018 renderer=None)
1021
1019
1022 config.add_route(
1020 config.add_route(
1023 name='edit_repo_remote_push',
1021 name='edit_repo_remote_push',
1024 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1022 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1025
1023
1026 # Statistics
1024 # Statistics
1027 config.add_route(
1025 config.add_route(
1028 name='edit_repo_statistics',
1026 name='edit_repo_statistics',
1029 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1027 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1030 config.add_view(
1028 config.add_view(
1031 RepoSettingsView,
1029 RepoSettingsView,
1032 attr='edit_statistics_form',
1030 attr='edit_statistics_form',
1033 route_name='edit_repo_statistics', request_method='GET',
1031 route_name='edit_repo_statistics', request_method='GET',
1034 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1032 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1035
1033
1036 config.add_route(
1034 config.add_route(
1037 name='edit_repo_statistics_reset',
1035 name='edit_repo_statistics_reset',
1038 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1036 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1039 config.add_view(
1037 config.add_view(
1040 RepoSettingsView,
1038 RepoSettingsView,
1041 attr='repo_statistics_reset',
1039 attr='repo_statistics_reset',
1042 route_name='edit_repo_statistics_reset', request_method='POST',
1040 route_name='edit_repo_statistics_reset', request_method='POST',
1043 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1041 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1044
1042
1045 # Issue trackers
1043 # Issue trackers
1046 config.add_route(
1044 config.add_route(
1047 name='edit_repo_issuetracker',
1045 name='edit_repo_issuetracker',
1048 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1046 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1049 config.add_view(
1047 config.add_view(
1050 RepoSettingsIssueTrackersView,
1048 RepoSettingsIssueTrackersView,
1051 attr='repo_issuetracker',
1049 attr='repo_issuetracker',
1052 route_name='edit_repo_issuetracker', request_method='GET',
1050 route_name='edit_repo_issuetracker', request_method='GET',
1053 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1051 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1054
1052
1055 config.add_route(
1053 config.add_route(
1056 name='edit_repo_issuetracker_test',
1054 name='edit_repo_issuetracker_test',
1057 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1055 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1058 config.add_view(
1056 config.add_view(
1059 RepoSettingsIssueTrackersView,
1057 RepoSettingsIssueTrackersView,
1060 attr='repo_issuetracker_test',
1058 attr='repo_issuetracker_test',
1061 route_name='edit_repo_issuetracker_test', request_method='POST',
1059 route_name='edit_repo_issuetracker_test', request_method='POST',
1062 renderer='string', xhr=True)
1060 renderer='string', xhr=True)
1063
1061
1064 config.add_route(
1062 config.add_route(
1065 name='edit_repo_issuetracker_delete',
1063 name='edit_repo_issuetracker_delete',
1066 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1064 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1067 config.add_view(
1065 config.add_view(
1068 RepoSettingsIssueTrackersView,
1066 RepoSettingsIssueTrackersView,
1069 attr='repo_issuetracker_delete',
1067 attr='repo_issuetracker_delete',
1070 route_name='edit_repo_issuetracker_delete', request_method='POST',
1068 route_name='edit_repo_issuetracker_delete', request_method='POST',
1071 renderer='json_ext', xhr=True)
1069 renderer='json_ext', xhr=True)
1072
1070
1073 config.add_route(
1071 config.add_route(
1074 name='edit_repo_issuetracker_update',
1072 name='edit_repo_issuetracker_update',
1075 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1073 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1076 config.add_view(
1074 config.add_view(
1077 RepoSettingsIssueTrackersView,
1075 RepoSettingsIssueTrackersView,
1078 attr='repo_issuetracker_update',
1076 attr='repo_issuetracker_update',
1079 route_name='edit_repo_issuetracker_update', request_method='POST',
1077 route_name='edit_repo_issuetracker_update', request_method='POST',
1080 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1078 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1081
1079
1082 # VCS Settings
1080 # VCS Settings
1083 config.add_route(
1081 config.add_route(
1084 name='edit_repo_vcs',
1082 name='edit_repo_vcs',
1085 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1083 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1086 config.add_view(
1084 config.add_view(
1087 RepoSettingsVcsView,
1085 RepoSettingsVcsView,
1088 attr='repo_vcs_settings',
1086 attr='repo_vcs_settings',
1089 route_name='edit_repo_vcs', request_method='GET',
1087 route_name='edit_repo_vcs', request_method='GET',
1090 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1088 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1091
1089
1092 config.add_route(
1090 config.add_route(
1093 name='edit_repo_vcs_update',
1091 name='edit_repo_vcs_update',
1094 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1092 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1095 config.add_view(
1093 config.add_view(
1096 RepoSettingsVcsView,
1094 RepoSettingsVcsView,
1097 attr='repo_settings_vcs_update',
1095 attr='repo_settings_vcs_update',
1098 route_name='edit_repo_vcs_update', request_method='POST',
1096 route_name='edit_repo_vcs_update', request_method='POST',
1099 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1097 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1100
1098
1101 # svn pattern
1099 # svn pattern
1102 config.add_route(
1100 config.add_route(
1103 name='edit_repo_vcs_svn_pattern_delete',
1101 name='edit_repo_vcs_svn_pattern_delete',
1104 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1102 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1105 config.add_view(
1103 config.add_view(
1106 RepoSettingsVcsView,
1104 RepoSettingsVcsView,
1107 attr='repo_settings_delete_svn_pattern',
1105 attr='repo_settings_delete_svn_pattern',
1108 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1106 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1109 renderer='json_ext', xhr=True)
1107 renderer='json_ext', xhr=True)
1110
1108
1111 # Repo Review Rules (EE feature)
1109 # Repo Review Rules (EE feature)
1112 config.add_route(
1110 config.add_route(
1113 name='repo_reviewers',
1111 name='repo_reviewers',
1114 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1112 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1115 config.add_view(
1113 config.add_view(
1116 RepoReviewRulesView,
1114 RepoReviewRulesView,
1117 attr='repo_review_rules',
1115 attr='repo_review_rules',
1118 route_name='repo_reviewers', request_method='GET',
1116 route_name='repo_reviewers', request_method='GET',
1119 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1117 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1120
1118
1121 config.add_route(
1119 config.add_route(
1122 name='repo_default_reviewers_data',
1120 name='repo_default_reviewers_data',
1123 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1121 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1124 config.add_view(
1122 config.add_view(
1125 RepoReviewRulesView,
1123 RepoReviewRulesView,
1126 attr='repo_default_reviewers_data',
1124 attr='repo_default_reviewers_data',
1127 route_name='repo_default_reviewers_data', request_method='GET',
1125 route_name='repo_default_reviewers_data', request_method='GET',
1128 renderer='json_ext')
1126 renderer='json_ext')
1129
1127
1130 # Repo Automation (EE feature)
1128 # Repo Automation (EE feature)
1131 config.add_route(
1129 config.add_route(
1132 name='repo_automation',
1130 name='repo_automation',
1133 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1131 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1134 config.add_view(
1132 config.add_view(
1135 RepoAutomationView,
1133 RepoAutomationView,
1136 attr='repo_automation',
1134 attr='repo_automation',
1137 route_name='repo_automation', request_method='GET',
1135 route_name='repo_automation', request_method='GET',
1138 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1136 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1139
1137
1140 # Strip
1138 # Strip
1141 config.add_route(
1139 config.add_route(
1142 name='edit_repo_strip',
1140 name='edit_repo_strip',
1143 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1141 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1144 config.add_view(
1142 config.add_view(
1145 RepoStripView,
1143 RepoStripView,
1146 attr='strip',
1144 attr='strip',
1147 route_name='edit_repo_strip', request_method='GET',
1145 route_name='edit_repo_strip', request_method='GET',
1148 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1146 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1149
1147
1150 config.add_route(
1148 config.add_route(
1151 name='strip_check',
1149 name='strip_check',
1152 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1150 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1153 config.add_view(
1151 config.add_view(
1154 RepoStripView,
1152 RepoStripView,
1155 attr='strip_check',
1153 attr='strip_check',
1156 route_name='strip_check', request_method='POST',
1154 route_name='strip_check', request_method='POST',
1157 renderer='json', xhr=True)
1155 renderer='json', xhr=True)
1158
1156
1159 config.add_route(
1157 config.add_route(
1160 name='strip_execute',
1158 name='strip_execute',
1161 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1159 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1162 config.add_view(
1160 config.add_view(
1163 RepoStripView,
1161 RepoStripView,
1164 attr='strip_execute',
1162 attr='strip_execute',
1165 route_name='strip_execute', request_method='POST',
1163 route_name='strip_execute', request_method='POST',
1166 renderer='json', xhr=True)
1164 renderer='json', xhr=True)
1167
1165
1168 # Audit logs
1166 # Audit logs
1169 config.add_route(
1167 config.add_route(
1170 name='edit_repo_audit_logs',
1168 name='edit_repo_audit_logs',
1171 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1169 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1172 config.add_view(
1170 config.add_view(
1173 AuditLogsView,
1171 AuditLogsView,
1174 attr='repo_audit_logs',
1172 attr='repo_audit_logs',
1175 route_name='edit_repo_audit_logs', request_method='GET',
1173 route_name='edit_repo_audit_logs', request_method='GET',
1176 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1174 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1177
1175
1178 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1176 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1179 config.add_route(
1177 config.add_route(
1180 name='rss_feed_home',
1178 name='rss_feed_home',
1181 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1179 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1182 config.add_view(
1180 config.add_view(
1183 RepoFeedView,
1181 RepoFeedView,
1184 attr='rss',
1182 attr='rss',
1185 route_name='rss_feed_home', request_method='GET', renderer=None)
1183 route_name='rss_feed_home', request_method='GET', renderer=None)
1186
1184
1187 config.add_route(
1185 config.add_route(
1188 name='rss_feed_home_old',
1186 name='rss_feed_home_old',
1189 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1187 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1190 config.add_view(
1188 config.add_view(
1191 RepoFeedView,
1189 RepoFeedView,
1192 attr='rss',
1190 attr='rss',
1193 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1191 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1194
1192
1195 config.add_route(
1193 config.add_route(
1196 name='atom_feed_home',
1194 name='atom_feed_home',
1197 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1195 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1198 config.add_view(
1196 config.add_view(
1199 RepoFeedView,
1197 RepoFeedView,
1200 attr='atom',
1198 attr='atom',
1201 route_name='atom_feed_home', request_method='GET', renderer=None)
1199 route_name='atom_feed_home', request_method='GET', renderer=None)
1202
1200
1203 config.add_route(
1201 config.add_route(
1204 name='atom_feed_home_old',
1202 name='atom_feed_home_old',
1205 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1203 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1206 config.add_view(
1204 config.add_view(
1207 RepoFeedView,
1205 RepoFeedView,
1208 attr='atom',
1206 attr='atom',
1209 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1207 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1210
1208
1211 # NOTE(marcink): needs to be at the end for catch-all
1209 # NOTE(marcink): needs to be at the end for catch-all
1212 add_route_with_slash(
1210 add_route_with_slash(
1213 config,
1211 config,
1214 name='repo_summary',
1212 name='repo_summary',
1215 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1213 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1216 config.add_view(
1214 config.add_view(
1217 RepoSummaryView,
1215 RepoSummaryView,
1218 attr='summary',
1216 attr='summary',
1219 route_name='repo_summary', request_method='GET',
1217 route_name='repo_summary', request_method='GET',
1220 renderer='rhodecode:templates/summary/summary.mako')
1218 renderer='rhodecode:templates/summary/summary.mako')
1221
1219
1222 # TODO(marcink): there's no such route??
1220 # TODO(marcink): there's no such route??
1223 config.add_view(
1221 config.add_view(
1224 RepoSummaryView,
1222 RepoSummaryView,
1225 attr='summary',
1223 attr='summary',
1226 route_name='repo_summary_slash', request_method='GET',
1224 route_name='repo_summary_slash', request_method='GET',
1227 renderer='rhodecode:templates/summary/summary.mako') No newline at end of file
1225 renderer='rhodecode:templates/summary/summary.mako')
@@ -1,18 +1,17 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,113 +1,111 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 from rhodecode.lib import helpers as h, rc_cache
19 from rhodecode.lib import helpers as h, rc_cache
22 from rhodecode.lib.utils2 import safe_int
20 from rhodecode.lib.utils2 import safe_int
23 from rhodecode.model.pull_request import get_diff_info
21 from rhodecode.model.pull_request import get_diff_info
24 from rhodecode.model.db import PullRequestReviewers
22 from rhodecode.model.db import PullRequestReviewers
25 # V3 - Reviewers, with default rules data
23 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
24 # v4 - Added observers metadata
27 # v5 - pr_author/commit_author include/exclude logic
25 # v5 - pr_author/commit_author include/exclude logic
28 REVIEWER_API_VERSION = 'V5'
26 REVIEWER_API_VERSION = 'V5'
29
27
30
28
31 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
29 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
32 """
30 """
33 Returns json struct of a reviewer for frontend
31 Returns json struct of a reviewer for frontend
34
32
35 :param user: the reviewer
33 :param user: the reviewer
36 :param reasons: list of strings of why they are reviewers
34 :param reasons: list of strings of why they are reviewers
37 :param mandatory: bool, to set user as mandatory
35 :param mandatory: bool, to set user as mandatory
38 """
36 """
39 role = role or PullRequestReviewers.ROLE_REVIEWER
37 role = role or PullRequestReviewers.ROLE_REVIEWER
40 if role not in PullRequestReviewers.ROLES:
38 if role not in PullRequestReviewers.ROLES:
41 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
39 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
42
40
43 return {
41 return {
44 'user_id': user.user_id,
42 'user_id': user.user_id,
45 'reasons': reasons or [],
43 'reasons': reasons or [],
46 'rules': rules or [],
44 'rules': rules or [],
47 'role': role,
45 'role': role,
48 'mandatory': mandatory,
46 'mandatory': mandatory,
49 'user_group': user_group,
47 'user_group': user_group,
50 'username': user.username,
48 'username': user.username,
51 'first_name': user.first_name,
49 'first_name': user.first_name,
52 'last_name': user.last_name,
50 'last_name': user.last_name,
53 'user_link': h.link_to_user(user),
51 'user_link': h.link_to_user(user),
54 'gravatar_link': h.gravatar_url(user.email, 14),
52 'gravatar_link': h.gravatar_url(user.email, 14),
55 }
53 }
56
54
57
55
58 def to_reviewers(e):
56 def to_reviewers(e):
59 if isinstance(e, (tuple, list)):
57 if isinstance(e, (tuple, list)):
60 return map(reviewer_as_json, e)
58 return map(reviewer_as_json, e)
61 else:
59 else:
62 return reviewer_as_json(e)
60 return reviewer_as_json(e)
63
61
64
62
65 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
63 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
66 include_diff_info=True):
64 include_diff_info=True):
67 """
65 """
68 Return json for default reviewers of a repository
66 Return json for default reviewers of a repository
69 """
67 """
70
68
71 diff_info = {}
69 diff_info = {}
72 if include_diff_info:
70 if include_diff_info:
73 diff_info = get_diff_info(
71 diff_info = get_diff_info(
74 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
72 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
75
73
76 reasons = ['Default reviewer', 'Repository owner']
74 reasons = ['Default reviewer', 'Repository owner']
77 json_reviewers = [reviewer_as_json(
75 json_reviewers = [reviewer_as_json(
78 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
76 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
79
77
80 compute_key = rc_cache.utils.compute_key_from_params(
78 compute_key = rc_cache.utils.compute_key_from_params(
81 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
79 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
82 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
80 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
83 target_ref.commit_id)
81 target_ref.commit_id)
84
82
85 return {
83 return {
86 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
84 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
87 'compute_key': compute_key,
85 'compute_key': compute_key,
88 'diff_info': diff_info,
86 'diff_info': diff_info,
89 'reviewers': json_reviewers,
87 'reviewers': json_reviewers,
90 'rules': {},
88 'rules': {},
91 'rules_data': {},
89 'rules_data': {},
92 'rules_humanized': [],
90 'rules_humanized': [],
93 }
91 }
94
92
95
93
96 def validate_default_reviewers(review_members, reviewer_rules):
94 def validate_default_reviewers(review_members, reviewer_rules):
97 """
95 """
98 Function to validate submitted reviewers against the saved rules
96 Function to validate submitted reviewers against the saved rules
99 """
97 """
100 reviewers = []
98 reviewers = []
101 reviewer_by_id = {}
99 reviewer_by_id = {}
102 for r in review_members:
100 for r in review_members:
103 reviewer_user_id = safe_int(r['user_id'])
101 reviewer_user_id = safe_int(r['user_id'])
104 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
102 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
105
103
106 reviewer_by_id[reviewer_user_id] = entry
104 reviewer_by_id[reviewer_user_id] = entry
107 reviewers.append(entry)
105 reviewers.append(entry)
108
106
109 return reviewers
107 return reviewers
110
108
111
109
112 def validate_observers(observer_members, reviewer_rules):
110 def validate_observers(observer_members, reviewer_rules):
113 return {}
111 return {}
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,45 +1,43 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.lib.auth import (
24 from rhodecode.lib.auth import (
27 LoginRequired, HasRepoPermissionAnyDecorator)
25 LoginRequired, HasRepoPermissionAnyDecorator)
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 class RepoArtifactsView(RepoAppView):
30 class RepoArtifactsView(RepoAppView):
33
31
34 def load_default_context(self):
32 def load_default_context(self):
35 c = self._get_local_tmpl_context(include_app_defaults=True)
33 c = self._get_local_tmpl_context(include_app_defaults=True)
36 c.rhodecode_repo = self.rhodecode_vcs_repo
34 c.rhodecode_repo = self.rhodecode_vcs_repo
37 return c
35 return c
38
36
39 @LoginRequired()
37 @LoginRequired()
40 @HasRepoPermissionAnyDecorator(
38 @HasRepoPermissionAnyDecorator(
41 'repository.read', 'repository.write', 'repository.admin')
39 'repository.read', 'repository.write', 'repository.admin')
42 def repo_artifacts(self):
40 def repo_artifacts(self):
43 c = self.load_default_context()
41 c = self.load_default_context()
44 c.active = 'artifacts'
42 c.active = 'artifacts'
45 return self._get_template_context(c)
43 return self._get_template_context(c)
@@ -1,64 +1,62 b''
1
2
3 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from rhodecode.apps._base import RepoAppView
22 from rhodecode.apps._base import RepoAppView
25 from rhodecode.lib.helpers import SqlPage
23 from rhodecode.lib.helpers import SqlPage
26 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
25 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 from rhodecode.lib.utils2 import safe_int
26 from rhodecode.lib.utils2 import safe_int
29 from rhodecode.model.repo import RepoModel
27 from rhodecode.model.repo import RepoModel
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class AuditLogsView(RepoAppView):
32 class AuditLogsView(RepoAppView):
35 def load_default_context(self):
33 def load_default_context(self):
36 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
37 return c
35 return c
38
36
39 @LoginRequired()
37 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.admin')
38 @HasRepoPermissionAnyDecorator('repository.admin')
41 def repo_audit_logs(self):
39 def repo_audit_logs(self):
42 _ = self.request.translate
40 _ = self.request.translate
43 c = self.load_default_context()
41 c = self.load_default_context()
44 c.db_repo = self.db_repo
42 c.db_repo = self.db_repo
45
43
46 c.active = 'audit'
44 c.active = 'audit'
47
45
48 p = safe_int(self.request.GET.get('page', 1), 1)
46 p = safe_int(self.request.GET.get('page', 1), 1)
49
47
50 filter_term = self.request.GET.get('filter')
48 filter_term = self.request.GET.get('filter')
51 user_log = RepoModel().get_repo_log(c.db_repo, filter_term)
49 user_log = RepoModel().get_repo_log(c.db_repo, filter_term)
52
50
53 def url_generator(page_num):
51 def url_generator(page_num):
54 query_params = {
52 query_params = {
55 'page': page_num
53 'page': page_num
56 }
54 }
57 if filter_term:
55 if filter_term:
58 query_params['filter'] = filter_term
56 query_params['filter'] = filter_term
59 return self.request.current_route_path(_query=query_params)
57 return self.request.current_route_path(_query=query_params)
60
58
61 c.audit_logs = SqlPage(
59 c.audit_logs = SqlPage(
62 user_log, page=p, items_per_page=10, url_maker=url_generator)
60 user_log, page=p, items_per_page=10, url_maker=url_generator)
63 c.filter_term = filter_term
61 c.filter_term = filter_term
64 return self._get_template_context(c)
62 return self._get_template_context(c)
@@ -1,43 +1,41 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
24 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
25 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 class RepoAutomationView(RepoAppView):
30 class RepoAutomationView(RepoAppView):
33 def load_default_context(self):
31 def load_default_context(self):
34 c = self._get_local_tmpl_context()
32 c = self._get_local_tmpl_context()
35 return c
33 return c
36
34
37 @LoginRequired()
35 @LoginRequired()
38 @HasRepoPermissionAnyDecorator('repository.admin')
36 @HasRepoPermissionAnyDecorator('repository.admin')
39 def repo_automation(self):
37 def repo_automation(self):
40 c = self.load_default_context()
38 c = self.load_default_context()
41 c.active = 'automation'
39 c.active = 'automation'
42
40
43 return self._get_template_context(c)
41 return self._get_template_context(c)
@@ -1,53 +1,51 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import logging
18 import logging
21
19
22 from pyramid.httpexceptions import HTTPNotFound
20 from pyramid.httpexceptions import HTTPNotFound
23
21
24 from rhodecode.apps._base import BaseReferencesView
22 from rhodecode.apps._base import BaseReferencesView
25 from rhodecode.lib import ext_json
23 from rhodecode.lib import ext_json
26 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 from rhodecode.model.scm import ScmModel
26 from rhodecode.model.scm import ScmModel
29
27
30 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
31
29
32
30
33 class RepoBookmarksView(BaseReferencesView):
31 class RepoBookmarksView(BaseReferencesView):
34
32
35 @LoginRequired()
33 @LoginRequired()
36 @HasRepoPermissionAnyDecorator(
34 @HasRepoPermissionAnyDecorator(
37 'repository.read', 'repository.write', 'repository.admin')
35 'repository.read', 'repository.write', 'repository.admin')
38 def bookmarks(self):
36 def bookmarks(self):
39 c = self.load_default_context()
37 c = self.load_default_context()
40 self._prepare_and_set_clone_url(c)
38 self._prepare_and_set_clone_url(c)
41 c.rhodecode_repo = self.rhodecode_vcs_repo
39 c.rhodecode_repo = self.rhodecode_vcs_repo
42 c.repository_forks = ScmModel().get_forks(self.db_repo)
40 c.repository_forks = ScmModel().get_forks(self.db_repo)
43
41
44 if not h.is_hg(self.db_repo):
42 if not h.is_hg(self.db_repo):
45 raise HTTPNotFound()
43 raise HTTPNotFound()
46
44
47 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
45 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
48 data = self.load_refs_context(
46 data = self.load_refs_context(
49 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
47 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
50
48
51 c.has_references = bool(data)
49 c.has_references = bool(data)
52 c.data = ext_json.str_json(data)
50 c.data = ext_json.str_json(data)
53 return self._get_template_context(c)
51 return self._get_template_context(c)
@@ -1,42 +1,40 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
24 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27
25
28 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
29
27
30
28
31 class RepoSettingsBranchPermissionsView(RepoAppView):
29 class RepoSettingsBranchPermissionsView(RepoAppView):
32
30
33 def load_default_context(self):
31 def load_default_context(self):
34 c = self._get_local_tmpl_context()
32 c = self._get_local_tmpl_context()
35 return c
33 return c
36
34
37 @LoginRequired()
35 @LoginRequired()
38 @HasRepoPermissionAnyDecorator('repository.admin')
36 @HasRepoPermissionAnyDecorator('repository.admin')
39 def branch_permissions(self):
37 def branch_permissions(self):
40 c = self.load_default_context()
38 c = self.load_default_context()
41 c.active = 'permissions_branch'
39 c.active = 'permissions_branch'
42 return self._get_template_context(c)
40 return self._get_template_context(c)
@@ -1,49 +1,47 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from rhodecode.apps._base import BaseReferencesView
22 from rhodecode.apps._base import BaseReferencesView
25 from rhodecode.lib import ext_json
23 from rhodecode.lib import ext_json
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
24 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 from rhodecode.model.scm import ScmModel
25 from rhodecode.model.scm import ScmModel
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 class RepoBranchesView(BaseReferencesView):
30 class RepoBranchesView(BaseReferencesView):
33
31
34 @LoginRequired()
32 @LoginRequired()
35 @HasRepoPermissionAnyDecorator(
33 @HasRepoPermissionAnyDecorator(
36 'repository.read', 'repository.write', 'repository.admin')
34 'repository.read', 'repository.write', 'repository.admin')
37 def branches(self):
35 def branches(self):
38 c = self.load_default_context()
36 c = self.load_default_context()
39 self._prepare_and_set_clone_url(c)
37 self._prepare_and_set_clone_url(c)
40 c.rhodecode_repo = self.rhodecode_vcs_repo
38 c.rhodecode_repo = self.rhodecode_vcs_repo
41 c.repository_forks = ScmModel().get_forks(self.db_repo)
39 c.repository_forks = ScmModel().get_forks(self.db_repo)
42
40
43 ref_items = self.rhodecode_vcs_repo.branches_all.items()
41 ref_items = self.rhodecode_vcs_repo.branches_all.items()
44 data = self.load_refs_context(
42 data = self.load_refs_context(
45 ref_items=ref_items, partials_template='branches/branches_data.mako')
43 ref_items=ref_items, partials_template='branches/branches_data.mako')
46
44
47 c.has_references = bool(data)
45 c.has_references = bool(data)
48 c.data = ext_json.str_json(data)
46 c.data = ext_json.str_json(data)
49 return self._get_template_context(c)
47 return self._get_template_context(c)
@@ -1,93 +1,91 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import logging
20 import logging
23
21
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25
23
26
24
27 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
28 from rhodecode.lib.auth import (
26 from rhodecode.lib.auth import (
29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
27 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 from rhodecode.lib import helpers as h, rc_cache
28 from rhodecode.lib import helpers as h, rc_cache
31 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
32 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
33 from rhodecode.model.scm import ScmModel
31 from rhodecode.model.scm import ScmModel
34
32
35 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
36
34
37
35
38 class RepoCachesView(RepoAppView):
36 class RepoCachesView(RepoAppView):
39 def load_default_context(self):
37 def load_default_context(self):
40 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
41 return c
39 return c
42
40
43 @LoginRequired()
41 @LoginRequired()
44 @HasRepoPermissionAnyDecorator('repository.admin')
42 @HasRepoPermissionAnyDecorator('repository.admin')
45 def repo_caches(self):
43 def repo_caches(self):
46 c = self.load_default_context()
44 c = self.load_default_context()
47 c.active = 'caches'
45 c.active = 'caches'
48 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
46 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
49 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
47 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
50 c.cached_diff_size = 0
48 c.cached_diff_size = 0
51 if os.path.isdir(cached_diffs_dir):
49 if os.path.isdir(cached_diffs_dir):
52 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
50 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
53 c.shadow_repos = c.rhodecode_db_repo.shadow_repos()
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 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
54 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
57 c.backend = c.region.backend
55 c.backend = c.region.backend
58 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
56 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
59
57
60 return self._get_template_context(c)
58 return self._get_template_context(c)
61
59
62 @LoginRequired()
60 @LoginRequired()
63 @HasRepoPermissionAnyDecorator('repository.admin')
61 @HasRepoPermissionAnyDecorator('repository.admin')
64 @CSRFRequired()
62 @CSRFRequired()
65 def repo_caches_purge(self):
63 def repo_caches_purge(self):
66 _ = self.request.translate
64 _ = self.request.translate
67 c = self.load_default_context()
65 c = self.load_default_context()
68 c.active = 'caches'
66 c.active = 'caches'
69 invalidated = 0
67 invalidated = 0
70
68
71 try:
69 try:
72 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
70 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
73 Session().commit()
71 Session().commit()
74 invalidated +=1
72 invalidated +=1
75 except Exception:
73 except Exception:
76 log.exception("Exception during cache invalidation")
74 log.exception("Exception during cache invalidation")
77 h.flash(_('An error occurred during cache invalidation'),
75 h.flash(_('An error occurred during cache invalidation'),
78 category='error')
76 category='error')
79
77
80 try:
78 try:
81 invalidated += 1
79 invalidated += 1
82 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
80 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
83 except Exception:
81 except Exception:
84 log.exception("Exception during vcsserver cache invalidation")
82 log.exception("Exception during vcsserver cache invalidation")
85 h.flash(_('An error occurred during vcsserver cache invalidation'),
83 h.flash(_('An error occurred during vcsserver cache invalidation'),
86 category='error')
84 category='error')
87
85
88 if invalidated:
86 if invalidated:
89 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
87 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
90 category='success')
88 category='success')
91
89
92 raise HTTPFound(h.route_path(
90 raise HTTPFound(h.route_path(
93 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
91 'edit_repo_caches', repo_name=self.db_repo_name))
@@ -1,356 +1,355 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20
19
21 import logging
20 import logging
22
21
23 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
22 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24
23
25 from pyramid.renderers import render
24 from pyramid.renderers import render
26 from pyramid.response import Response
25 from pyramid.response import Response
27
26
28 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
29 import rhodecode.lib.helpers as h
28 import rhodecode.lib.helpers as h
30 from rhodecode.lib import ext_json
29 from rhodecode.lib import ext_json
31 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
31 LoginRequired, HasRepoPermissionAnyDecorator)
33
32
34 from rhodecode.lib.graphmod import _colored, _dagwalker
33 from rhodecode.lib.graphmod import _colored, _dagwalker
35 from rhodecode.lib.helpers import RepoPage
34 from rhodecode.lib.helpers import RepoPage
36 from rhodecode.lib.utils2 import str2bool
35 from rhodecode.lib.utils2 import str2bool
37 from rhodecode.lib.str_utils import safe_int, safe_str
36 from rhodecode.lib.str_utils import safe_int, safe_str
38 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
38 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
39 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
40
42 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
43
42
44 DEFAULT_CHANGELOG_SIZE = 20
43 DEFAULT_CHANGELOG_SIZE = 20
45
44
46
45
47 class RepoChangelogView(RepoAppView):
46 class RepoChangelogView(RepoAppView):
48
47
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
48 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
49 """
51 This is a safe way to get commit. If an error occurs it redirects to
50 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
51 tip with proper message
53
52
54 :param commit_id: id of commit to fetch
53 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
54 :param redirect_after: toggle redirection
56 """
55 """
57 _ = self.request.translate
56 _ = self.request.translate
58
57
59 try:
58 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
59 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
60 except EmptyRepositoryError:
62 if not redirect_after:
61 if not redirect_after:
63 return None
62 return None
64
63
65 h.flash(h.literal(
64 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
65 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
66 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
67 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
68
70 except (CommitDoesNotExistError, LookupError):
69 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
70 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
71 h.flash(msg, category='error')
73 raise HTTPNotFound()
72 raise HTTPNotFound()
74 except RepositoryError as e:
73 except RepositoryError as e:
75 h.flash(h.escape(safe_str(e)), category='error')
74 h.flash(h.escape(safe_str(e)), category='error')
76 raise HTTPNotFound()
75 raise HTTPNotFound()
77
76
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
77 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
78 """
80 Generates a DAG graph for repo
79 Generates a DAG graph for repo
81
80
82 :param repo: repo instance
81 :param repo: repo instance
83 :param commits: list of commits
82 :param commits: list of commits
84 """
83 """
85 if not commits:
84 if not commits:
86 return ext_json.str_json([]), ext_json.str_json([])
85 return ext_json.str_json([]), ext_json.str_json([])
87
86
88 def serialize(commit, parents=True):
87 def serialize(commit, parents=True):
89 data = dict(
88 data = dict(
90 raw_id=commit.raw_id,
89 raw_id=commit.raw_id,
91 idx=commit.idx,
90 idx=commit.idx,
92 branch=None,
91 branch=None,
93 )
92 )
94 if parents:
93 if parents:
95 data['parents'] = [
94 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
95 serialize(x, parents=False) for x in commit.parents]
97 return data
96 return data
98
97
99 prev_data = prev_data or []
98 prev_data = prev_data or []
100 next_data = next_data or []
99 next_data = next_data or []
101
100
102 current = [serialize(x) for x in commits]
101 current = [serialize(x) for x in commits]
103
102
104 commits = prev_data + current + next_data
103 commits = prev_data + current + next_data
105
104
106 dag = _dagwalker(repo, commits)
105 dag = _dagwalker(repo, commits)
107
106
108 data = [[commit_id, vtx, edges, branch]
107 data = [[commit_id, vtx, edges, branch]
109 for commit_id, vtx, edges, branch in _colored(dag)]
108 for commit_id, vtx, edges, branch in _colored(dag)]
110 return ext_json.str_json(data), ext_json.str_json(current)
109 return ext_json.str_json(data), ext_json.str_json(current)
111
110
112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
113 if branch_name not in self.rhodecode_vcs_repo.branches_all:
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 category='warning')
114 category='warning')
116 redirect_url = h.route_path(
115 redirect_url = h.route_path(
117 'repo_commits_file', repo_name=repo_name,
116 'repo_commits_file', repo_name=repo_name,
118 commit_id=branch_name, f_path=f_path or '')
117 commit_id=branch_name, f_path=f_path or '')
119 raise HTTPFound(redirect_url)
118 raise HTTPFound(redirect_url)
120
119
121 def _load_changelog_data(
120 def _load_changelog_data(
122 self, c, collection, page, chunk_size, branch_name=None,
121 self, c, collection, page, chunk_size, branch_name=None,
123 dynamic=False, f_path=None, commit_id=None):
122 dynamic=False, f_path=None, commit_id=None):
124
123
125 def url_generator(page_num):
124 def url_generator(page_num):
126 query_params = {
125 query_params = {
127 'page': page_num
126 'page': page_num
128 }
127 }
129
128
130 if branch_name:
129 if branch_name:
131 query_params.update({
130 query_params.update({
132 'branch': branch_name
131 'branch': branch_name
133 })
132 })
134
133
135 if f_path:
134 if f_path:
136 # changelog for file
135 # changelog for file
137 return h.route_path(
136 return h.route_path(
138 'repo_commits_file',
137 'repo_commits_file',
139 repo_name=c.rhodecode_db_repo.repo_name,
138 repo_name=c.rhodecode_db_repo.repo_name,
140 commit_id=commit_id, f_path=f_path,
139 commit_id=commit_id, f_path=f_path,
141 _query=query_params)
140 _query=query_params)
142 else:
141 else:
143 return h.route_path(
142 return h.route_path(
144 'repo_commits',
143 'repo_commits',
145 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
146
145
147 c.total_cs = len(collection)
146 c.total_cs = len(collection)
148 c.showing_commits = min(chunk_size, c.total_cs)
147 c.showing_commits = min(chunk_size, c.total_cs)
149 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
150 items_per_page=chunk_size, url_maker=url_generator)
149 items_per_page=chunk_size, url_maker=url_generator)
151
150
152 c.next_page = c.pagination.next_page
151 c.next_page = c.pagination.next_page
153 c.prev_page = c.pagination.previous_page
152 c.prev_page = c.pagination.previous_page
154
153
155 if dynamic:
154 if dynamic:
156 if self.request.GET.get('chunk') != 'next':
155 if self.request.GET.get('chunk') != 'next':
157 c.next_page = None
156 c.next_page = None
158 if self.request.GET.get('chunk') != 'prev':
157 if self.request.GET.get('chunk') != 'prev':
159 c.prev_page = None
158 c.prev_page = None
160
159
161 page_commit_ids = [x.raw_id for x in c.pagination]
160 page_commit_ids = [x.raw_id for x in c.pagination]
162 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
163 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
164
163
165 def load_default_context(self):
164 def load_default_context(self):
166 c = self._get_local_tmpl_context(include_app_defaults=True)
165 c = self._get_local_tmpl_context(include_app_defaults=True)
167
166
168 c.rhodecode_repo = self.rhodecode_vcs_repo
167 c.rhodecode_repo = self.rhodecode_vcs_repo
169
168
170 return c
169 return c
171
170
172 @LoginRequired()
171 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
175 def repo_changelog(self):
174 def repo_changelog(self):
176 c = self.load_default_context()
175 c = self.load_default_context()
177
176
178 commit_id = self.request.matchdict.get('commit_id')
177 commit_id = self.request.matchdict.get('commit_id')
179 f_path = self._get_f_path(self.request.matchdict)
178 f_path = self._get_f_path(self.request.matchdict)
180 show_hidden = str2bool(self.request.GET.get('evolve'))
179 show_hidden = str2bool(self.request.GET.get('evolve'))
181
180
182 chunk_size = 20
181 chunk_size = 20
183
182
184 c.branch_name = branch_name = self.request.GET.get('branch') or ''
183 c.branch_name = branch_name = self.request.GET.get('branch') or ''
185 c.book_name = book_name = self.request.GET.get('bookmark') or ''
184 c.book_name = book_name = self.request.GET.get('bookmark') or ''
186 c.f_path = f_path
185 c.f_path = f_path
187 c.commit_id = commit_id
186 c.commit_id = commit_id
188 c.show_hidden = show_hidden
187 c.show_hidden = show_hidden
189
188
190 hist_limit = safe_int(self.request.GET.get('limit')) or None
189 hist_limit = safe_int(self.request.GET.get('limit')) or None
191
190
192 p = safe_int(self.request.GET.get('page', 1), 1)
191 p = safe_int(self.request.GET.get('page', 1), 1)
193
192
194 c.selected_name = branch_name or book_name
193 c.selected_name = branch_name or book_name
195 if not commit_id and branch_name:
194 if not commit_id and branch_name:
196 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
195 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
197
196
198 c.changelog_for_path = f_path
197 c.changelog_for_path = f_path
199 pre_load = self.get_commit_preload_attrs()
198 pre_load = self.get_commit_preload_attrs()
200
199
201 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
200 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
202
201
203 try:
202 try:
204 if f_path:
203 if f_path:
205 log.debug('generating changelog for path %s', f_path)
204 log.debug('generating changelog for path %s', f_path)
206 # get the history for the file !
205 # get the history for the file !
207 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
208
207
209 try:
208 try:
210 collection = base_commit.get_path_history(
209 collection = base_commit.get_path_history(
211 f_path, limit=hist_limit, pre_load=pre_load)
210 f_path, limit=hist_limit, pre_load=pre_load)
212 if collection and partial_xhr:
211 if collection and partial_xhr:
213 # for ajax call we remove first one since we're looking
212 # for ajax call we remove first one since we're looking
214 # at it right now in the context of a file commit
213 # at it right now in the context of a file commit
215 collection.pop(0)
214 collection.pop(0)
216 except (NodeDoesNotExistError, CommitError):
215 except (NodeDoesNotExistError, CommitError):
217 # this node is not present at tip!
216 # this node is not present at tip!
218 try:
217 try:
219 commit = self._get_commit_or_redirect(commit_id)
218 commit = self._get_commit_or_redirect(commit_id)
220 collection = commit.get_path_history(f_path)
219 collection = commit.get_path_history(f_path)
221 except RepositoryError as e:
220 except RepositoryError as e:
222 h.flash(safe_str(e), category='warning')
221 h.flash(safe_str(e), category='warning')
223 redirect_url = h.route_path(
222 redirect_url = h.route_path(
224 'repo_commits', repo_name=self.db_repo_name)
223 'repo_commits', repo_name=self.db_repo_name)
225 raise HTTPFound(redirect_url)
224 raise HTTPFound(redirect_url)
226 collection = list(reversed(collection))
225 collection = list(reversed(collection))
227 else:
226 else:
228 collection = self.rhodecode_vcs_repo.get_commits(
227 collection = self.rhodecode_vcs_repo.get_commits(
229 branch_name=branch_name, show_hidden=show_hidden,
228 branch_name=branch_name, show_hidden=show_hidden,
230 pre_load=pre_load, translate_tags=False)
229 pre_load=pre_load, translate_tags=False)
231
230
232 self._load_changelog_data(
231 self._load_changelog_data(
233 c, collection, p, chunk_size, c.branch_name,
232 c, collection, p, chunk_size, c.branch_name,
234 f_path=f_path, commit_id=commit_id)
233 f_path=f_path, commit_id=commit_id)
235
234
236 except EmptyRepositoryError as e:
235 except EmptyRepositoryError as e:
237 h.flash(h.escape(safe_str(e)), category='warning')
236 h.flash(h.escape(safe_str(e)), category='warning')
238 raise HTTPFound(
237 raise HTTPFound(
239 h.route_path('repo_summary', repo_name=self.db_repo_name))
238 h.route_path('repo_summary', repo_name=self.db_repo_name))
240 except HTTPFound:
239 except HTTPFound:
241 raise
240 raise
242 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
241 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
243 log.exception(safe_str(e))
242 log.exception(safe_str(e))
244 h.flash(h.escape(safe_str(e)), category='error')
243 h.flash(h.escape(safe_str(e)), category='error')
245
244
246 if commit_id:
245 if commit_id:
247 # from single commit page, we redirect to main commits
246 # from single commit page, we redirect to main commits
248 raise HTTPFound(
247 raise HTTPFound(
249 h.route_path('repo_commits', repo_name=self.db_repo_name))
248 h.route_path('repo_commits', repo_name=self.db_repo_name))
250 else:
249 else:
251 # otherwise we redirect to summary
250 # otherwise we redirect to summary
252 raise HTTPFound(
251 raise HTTPFound(
253 h.route_path('repo_summary', repo_name=self.db_repo_name))
252 h.route_path('repo_summary', repo_name=self.db_repo_name))
254
253
255
254
256
255
257 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
256 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
258 # case when loading dynamic file history in file view
257 # case when loading dynamic file history in file view
259 # loading from ajax, we don't want the first result, it's popped
258 # loading from ajax, we don't want the first result, it's popped
260 # in the code above
259 # in the code above
261 html = render(
260 html = render(
262 'rhodecode:templates/commits/changelog_file_history.mako',
261 'rhodecode:templates/commits/changelog_file_history.mako',
263 self._get_template_context(c), self.request)
262 self._get_template_context(c), self.request)
264 return Response(html)
263 return Response(html)
265
264
266 commit_ids = []
265 commit_ids = []
267 if not f_path:
266 if not f_path:
268 # only load graph data when not in file history mode
267 # only load graph data when not in file history mode
269 commit_ids = c.pagination
268 commit_ids = c.pagination
270
269
271 c.graph_data, c.graph_commits = self._graph(
270 c.graph_data, c.graph_commits = self._graph(
272 self.rhodecode_vcs_repo, commit_ids)
271 self.rhodecode_vcs_repo, commit_ids)
273
272
274 return self._get_template_context(c)
273 return self._get_template_context(c)
275
274
276 @LoginRequired()
275 @LoginRequired()
277 @HasRepoPermissionAnyDecorator(
276 @HasRepoPermissionAnyDecorator(
278 'repository.read', 'repository.write', 'repository.admin')
277 'repository.read', 'repository.write', 'repository.admin')
279 def repo_commits_elements(self):
278 def repo_commits_elements(self):
280 c = self.load_default_context()
279 c = self.load_default_context()
281 commit_id = self.request.matchdict.get('commit_id')
280 commit_id = self.request.matchdict.get('commit_id')
282 f_path = self._get_f_path(self.request.matchdict)
281 f_path = self._get_f_path(self.request.matchdict)
283 show_hidden = str2bool(self.request.GET.get('evolve'))
282 show_hidden = str2bool(self.request.GET.get('evolve'))
284
283
285 chunk_size = 20
284 chunk_size = 20
286 hist_limit = safe_int(self.request.GET.get('limit')) or None
285 hist_limit = safe_int(self.request.GET.get('limit')) or None
287
286
288 def wrap_for_error(err):
287 def wrap_for_error(err):
289 html = '<tr>' \
288 html = '<tr>' \
290 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
289 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
291 '</tr>'.format(err)
290 '</tr>'.format(err)
292 return Response(html)
291 return Response(html)
293
292
294 c.branch_name = branch_name = self.request.GET.get('branch') or ''
293 c.branch_name = branch_name = self.request.GET.get('branch') or ''
295 c.book_name = book_name = self.request.GET.get('bookmark') or ''
294 c.book_name = book_name = self.request.GET.get('bookmark') or ''
296 c.f_path = f_path
295 c.f_path = f_path
297 c.commit_id = commit_id
296 c.commit_id = commit_id
298 c.show_hidden = show_hidden
297 c.show_hidden = show_hidden
299
298
300 c.selected_name = branch_name or book_name
299 c.selected_name = branch_name or book_name
301 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
300 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
302 return wrap_for_error(
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 pre_load = self.get_commit_preload_attrs()
304 pre_load = self.get_commit_preload_attrs()
306
305
307 if f_path:
306 if f_path:
308 try:
307 try:
309 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
308 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
310 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
309 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
311 log.exception(safe_str(e))
310 log.exception(safe_str(e))
312 raise HTTPFound(
311 raise HTTPFound(
313 h.route_path('repo_commits', repo_name=self.db_repo_name))
312 h.route_path('repo_commits', repo_name=self.db_repo_name))
314
313
315 collection = base_commit.get_path_history(
314 collection = base_commit.get_path_history(
316 f_path, limit=hist_limit, pre_load=pre_load)
315 f_path, limit=hist_limit, pre_load=pre_load)
317 collection = list(reversed(collection))
316 collection = list(reversed(collection))
318 else:
317 else:
319 collection = self.rhodecode_vcs_repo.get_commits(
318 collection = self.rhodecode_vcs_repo.get_commits(
320 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
319 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
321 translate_tags=False)
320 translate_tags=False)
322
321
323 p = safe_int(self.request.GET.get('page', 1), 1)
322 p = safe_int(self.request.GET.get('page', 1), 1)
324 try:
323 try:
325 self._load_changelog_data(
324 self._load_changelog_data(
326 c, collection, p, chunk_size, dynamic=True,
325 c, collection, p, chunk_size, dynamic=True,
327 f_path=f_path, commit_id=commit_id)
326 f_path=f_path, commit_id=commit_id)
328 except EmptyRepositoryError as e:
327 except EmptyRepositoryError as e:
329 return wrap_for_error(safe_str(e))
328 return wrap_for_error(safe_str(e))
330 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
329 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
331 log.exception('Failed to fetch commits')
330 log.exception('Failed to fetch commits')
332 return wrap_for_error(safe_str(e))
331 return wrap_for_error(safe_str(e))
333
332
334 prev_data = None
333 prev_data = None
335 next_data = None
334 next_data = None
336
335
337 try:
336 try:
338 prev_graph = ext_json.json.loads(self.request.POST.get('graph') or '{}')
337 prev_graph = ext_json.json.loads(self.request.POST.get('graph') or '{}')
339 except ext_json.json.JSONDecodeError:
338 except ext_json.json.JSONDecodeError:
340 prev_graph = {}
339 prev_graph = {}
341
340
342 if self.request.GET.get('chunk') == 'prev':
341 if self.request.GET.get('chunk') == 'prev':
343 next_data = prev_graph
342 next_data = prev_graph
344 elif self.request.GET.get('chunk') == 'next':
343 elif self.request.GET.get('chunk') == 'next':
345 prev_data = prev_graph
344 prev_data = prev_graph
346
345
347 commit_ids = []
346 commit_ids = []
348 if not f_path:
347 if not f_path:
349 # only load graph data when not in file history mode
348 # only load graph data when not in file history mode
350 commit_ids = c.pagination
349 commit_ids = c.pagination
351
350
352 c.graph_data, c.graph_commits = self._graph(
351 c.graph_data, c.graph_commits = self._graph(
353 self.rhodecode_vcs_repo, commit_ids,
352 self.rhodecode_vcs_repo, commit_ids,
354 prev_data=prev_data, next_data=next_data)
353 prev_data=prev_data, next_data=next_data)
355
354
356 return self._get_template_context(c)
355 return self._get_template_context(c)
@@ -1,117 +1,115 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import (NotAnonymous, HasRepoPermissionAny)
25 from rhodecode.lib.auth import (NotAnonymous, HasRepoPermissionAny)
28 from rhodecode.model.db import Repository
26 from rhodecode.model.db import Repository
29 from rhodecode.model.permission import PermissionModel
27 from rhodecode.model.permission import PermissionModel
30 from rhodecode.model.validation_schema.types import RepoNameType
28 from rhodecode.model.validation_schema.types import RepoNameType
31
29
32 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
33
31
34
32
35 class RepoChecksView(BaseAppView):
33 class RepoChecksView(BaseAppView):
36 def load_default_context(self):
34 def load_default_context(self):
37 c = self._get_local_tmpl_context()
35 c = self._get_local_tmpl_context()
38 return c
36 return c
39
37
40 @NotAnonymous()
38 @NotAnonymous()
41 def repo_creating(self):
39 def repo_creating(self):
42 c = self.load_default_context()
40 c = self.load_default_context()
43 repo_name = self.request.matchdict['repo_name']
41 repo_name = self.request.matchdict['repo_name']
44 repo_name = RepoNameType().deserialize(None, repo_name)
42 repo_name = RepoNameType().deserialize(None, repo_name)
45 db_repo = Repository.get_by_repo_name(repo_name)
43 db_repo = Repository.get_by_repo_name(repo_name)
46
44
47 # check if maybe repo is already created
45 # check if maybe repo is already created
48 if db_repo and db_repo.repo_state in [Repository.STATE_CREATED]:
46 if db_repo and db_repo.repo_state in [Repository.STATE_CREATED]:
49 self.flush_permissions_on_creation(db_repo)
47 self.flush_permissions_on_creation(db_repo)
50
48
51 # re-check permissions before redirecting to prevent resource
49 # re-check permissions before redirecting to prevent resource
52 # discovery by checking the 302 code
50 # discovery by checking the 302 code
53 perm_set = ['repository.read', 'repository.write', 'repository.admin']
51 perm_set = ['repository.read', 'repository.write', 'repository.admin']
54 has_perm = HasRepoPermissionAny(*perm_set)(
52 has_perm = HasRepoPermissionAny(*perm_set)(
55 db_repo.repo_name, 'Repo Creating check')
53 db_repo.repo_name, 'Repo Creating check')
56 if not has_perm:
54 if not has_perm:
57 raise HTTPNotFound()
55 raise HTTPNotFound()
58
56
59 raise HTTPFound(h.route_path(
57 raise HTTPFound(h.route_path(
60 'repo_summary', repo_name=db_repo.repo_name))
58 'repo_summary', repo_name=db_repo.repo_name))
61
59
62 c.task_id = self.request.GET.get('task_id')
60 c.task_id = self.request.GET.get('task_id')
63 c.repo_name = repo_name
61 c.repo_name = repo_name
64
62
65 return self._get_template_context(c)
63 return self._get_template_context(c)
66
64
67 @NotAnonymous()
65 @NotAnonymous()
68 def repo_creating_check(self):
66 def repo_creating_check(self):
69 _ = self.request.translate
67 _ = self.request.translate
70 task_id = self.request.GET.get('task_id')
68 task_id = self.request.GET.get('task_id')
71 self.load_default_context()
69 self.load_default_context()
72
70
73 repo_name = self.request.matchdict['repo_name']
71 repo_name = self.request.matchdict['repo_name']
74
72
75 if task_id and task_id not in ['None']:
73 if task_id and task_id not in ['None']:
76 import rhodecode
74 import rhodecode
77 from rhodecode.lib.celerylib.loader import celery_app, exceptions
75 from rhodecode.lib.celerylib.loader import celery_app, exceptions
78 if rhodecode.CELERY_ENABLED:
76 if rhodecode.CELERY_ENABLED:
79 log.debug('celery: checking result for task:%s', task_id)
77 log.debug('celery: checking result for task:%s', task_id)
80 task = celery_app.AsyncResult(task_id)
78 task = celery_app.AsyncResult(task_id)
81 try:
79 try:
82 task.get(timeout=10)
80 task.get(timeout=10)
83 except exceptions.TimeoutError:
81 except exceptions.TimeoutError:
84 task = None
82 task = None
85 if task and task.failed():
83 if task and task.failed():
86 msg = self._log_creation_exception(task.result, repo_name)
84 msg = self._log_creation_exception(task.result, repo_name)
87 h.flash(msg, category='error')
85 h.flash(msg, category='error')
88 raise HTTPFound(h.route_path('home'), code=501)
86 raise HTTPFound(h.route_path('home'), code=501)
89
87
90 db_repo = Repository.get_by_repo_name(repo_name)
88 db_repo = Repository.get_by_repo_name(repo_name)
91 if db_repo and db_repo.repo_state == Repository.STATE_CREATED:
89 if db_repo and db_repo.repo_state == Repository.STATE_CREATED:
92 if db_repo.clone_uri:
90 if db_repo.clone_uri:
93 clone_uri = db_repo.clone_uri_hidden
91 clone_uri = db_repo.clone_uri_hidden
94 h.flash(_('Created repository %s from %s')
92 h.flash(_('Created repository %s from %s')
95 % (db_repo.repo_name, clone_uri), category='success')
93 % (db_repo.repo_name, clone_uri), category='success')
96 else:
94 else:
97 repo_url = h.link_to(
95 repo_url = h.link_to(
98 db_repo.repo_name,
96 db_repo.repo_name,
99 h.route_path('repo_summary', repo_name=db_repo.repo_name))
97 h.route_path('repo_summary', repo_name=db_repo.repo_name))
100 fork = db_repo.fork
98 fork = db_repo.fork
101 if fork:
99 if fork:
102 fork_name = fork.repo_name
100 fork_name = fork.repo_name
103 h.flash(h.literal(_('Forked repository %s as %s')
101 h.flash(h.literal(_('Forked repository %s as %s')
104 % (fork_name, repo_url)), category='success')
102 % (fork_name, repo_url)), category='success')
105 else:
103 else:
106 h.flash(h.literal(_('Created repository %s') % repo_url),
104 h.flash(h.literal(_('Created repository %s') % repo_url),
107 category='success')
105 category='success')
108 self.flush_permissions_on_creation(db_repo)
106 self.flush_permissions_on_creation(db_repo)
109
107
110 return {'result': True}
108 return {'result': True}
111 return {'result': False}
109 return {'result': False}
112
110
113 def flush_permissions_on_creation(self, db_repo):
111 def flush_permissions_on_creation(self, db_repo):
114 # repo is finished and created, we flush the permissions now
112 # repo is finished and created, we flush the permissions now
115 user_group_perms = db_repo.permissions(expand_from_user_groups=True)
113 user_group_perms = db_repo.permissions(expand_from_user_groups=True)
116 affected_user_ids = [perm['user_id'] for perm in user_group_perms]
114 affected_user_ids = [perm['user_id'] for perm in user_group_perms]
117 PermissionModel().trigger_permission_flush(affected_user_ids)
115 PermissionModel().trigger_permission_flush(affected_user_ids)
@@ -1,831 +1,830 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import logging
19 import logging
21 import collections
20 import collections
22
21
23 from pyramid.httpexceptions import (
22 from pyramid.httpexceptions import (
24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
23 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 from pyramid.renderers import render
24 from pyramid.renderers import render
26 from pyramid.response import Response
25 from pyramid.response import Response
27
26
28 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps.file_store import utils as store_utils
28 from rhodecode.apps.file_store import utils as store_utils
30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31
30
32 from rhodecode.lib import diffs, codeblocks, channelstream
31 from rhodecode.lib import diffs, codeblocks, channelstream
33 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 from rhodecode.lib import ext_json
34 from rhodecode.lib import ext_json
36 from collections import OrderedDict
35 from collections import OrderedDict
37 from rhodecode.lib.diffs import (
36 from rhodecode.lib.diffs import (
38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
37 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 get_diff_whitespace_flag)
38 get_diff_whitespace_flag)
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
39 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
41 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
42 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
45 RepositoryError, CommitDoesNotExistError)
44 RepositoryError, CommitDoesNotExistError)
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
45 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 ChangesetCommentHistory
46 ChangesetCommentHistory
48 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
51 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
52
51
53 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
54
53
55
54
56 def _update_with_GET(params, request):
55 def _update_with_GET(params, request):
57 for k in ['diff1', 'diff2', 'diff']:
56 for k in ['diff1', 'diff2', 'diff']:
58 params[k] += request.GET.getall(k)
57 params[k] += request.GET.getall(k)
59
58
60
59
61 class RepoCommitsView(RepoAppView):
60 class RepoCommitsView(RepoAppView):
62 def load_default_context(self):
61 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.rhodecode_repo = self.rhodecode_vcs_repo
63 c.rhodecode_repo = self.rhodecode_vcs_repo
65
64
66 return c
65 return c
67
66
68 def _is_diff_cache_enabled(self, target_repo):
67 def _is_diff_cache_enabled(self, target_repo):
69 caching_enabled = self._get_general_setting(
68 caching_enabled = self._get_general_setting(
70 target_repo, 'rhodecode_diff_cache')
69 target_repo, 'rhodecode_diff_cache')
71 log.debug('Diff caching enabled: %s', caching_enabled)
70 log.debug('Diff caching enabled: %s', caching_enabled)
72 return caching_enabled
71 return caching_enabled
73
72
74 def _commit(self, commit_id_range, method):
73 def _commit(self, commit_id_range, method):
75 _ = self.request.translate
74 _ = self.request.translate
76 c = self.load_default_context()
75 c = self.load_default_context()
77 c.fulldiff = self.request.GET.get('fulldiff')
76 c.fulldiff = self.request.GET.get('fulldiff')
78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
77 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79
78
80 # fetch global flags of ignore ws or context lines
79 # fetch global flags of ignore ws or context lines
81 diff_context = get_diff_context(self.request)
80 diff_context = get_diff_context(self.request)
82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
81 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83
82
84 # diff_limit will cut off the whole diff if the limit is applied
83 # diff_limit will cut off the whole diff if the limit is applied
85 # otherwise it will just hide the big files from the front-end
84 # otherwise it will just hide the big files from the front-end
86 diff_limit = c.visual.cut_off_limit_diff
85 diff_limit = c.visual.cut_off_limit_diff
87 file_limit = c.visual.cut_off_limit_file
86 file_limit = c.visual.cut_off_limit_file
88
87
89 # get ranges of commit ids if preset
88 # get ranges of commit ids if preset
90 commit_range = commit_id_range.split('...')[:2]
89 commit_range = commit_id_range.split('...')[:2]
91
90
92 try:
91 try:
93 pre_load = ['affected_files', 'author', 'branch', 'date',
92 pre_load = ['affected_files', 'author', 'branch', 'date',
94 'message', 'parents']
93 'message', 'parents']
95 if self.rhodecode_vcs_repo.alias == 'hg':
94 if self.rhodecode_vcs_repo.alias == 'hg':
96 pre_load += ['hidden', 'obsolete', 'phase']
95 pre_load += ['hidden', 'obsolete', 'phase']
97
96
98 if len(commit_range) == 2:
97 if len(commit_range) == 2:
99 commits = self.rhodecode_vcs_repo.get_commits(
98 commits = self.rhodecode_vcs_repo.get_commits(
100 start_id=commit_range[0], end_id=commit_range[1],
99 start_id=commit_range[0], end_id=commit_range[1],
101 pre_load=pre_load, translate_tags=False)
100 pre_load=pre_load, translate_tags=False)
102 commits = list(commits)
101 commits = list(commits)
103 else:
102 else:
104 commits = [self.rhodecode_vcs_repo.get_commit(
103 commits = [self.rhodecode_vcs_repo.get_commit(
105 commit_id=commit_id_range, pre_load=pre_load)]
104 commit_id=commit_id_range, pre_load=pre_load)]
106
105
107 c.commit_ranges = commits
106 c.commit_ranges = commits
108 if not c.commit_ranges:
107 if not c.commit_ranges:
109 raise RepositoryError('The commit range returned an empty result')
108 raise RepositoryError('The commit range returned an empty result')
110 except CommitDoesNotExistError as e:
109 except CommitDoesNotExistError as e:
111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
110 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 h.flash(msg, category='error')
111 h.flash(msg, category='error')
113 raise HTTPNotFound()
112 raise HTTPNotFound()
114 except Exception:
113 except Exception:
115 log.exception("General failure")
114 log.exception("General failure")
116 raise HTTPNotFound()
115 raise HTTPNotFound()
117 single_commit = len(c.commit_ranges) == 1
116 single_commit = len(c.commit_ranges) == 1
118
117
119 if redirect_to_combined and not single_commit:
118 if redirect_to_combined and not single_commit:
120 source_ref = getattr(c.commit_ranges[0].parents[0]
119 source_ref = getattr(c.commit_ranges[0].parents[0]
121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
120 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 target_ref = c.commit_ranges[-1].raw_id
121 target_ref = c.commit_ranges[-1].raw_id
123 next_url = h.route_path(
122 next_url = h.route_path(
124 'repo_compare',
123 'repo_compare',
125 repo_name=c.repo_name,
124 repo_name=c.repo_name,
126 source_ref_type='rev',
125 source_ref_type='rev',
127 source_ref=source_ref,
126 source_ref=source_ref,
128 target_ref_type='rev',
127 target_ref_type='rev',
129 target_ref=target_ref)
128 target_ref=target_ref)
130 raise HTTPFound(next_url)
129 raise HTTPFound(next_url)
131
130
132 c.changes = OrderedDict()
131 c.changes = OrderedDict()
133 c.lines_added = 0
132 c.lines_added = 0
134 c.lines_deleted = 0
133 c.lines_deleted = 0
135
134
136 # auto collapse if we have more than limit
135 # auto collapse if we have more than limit
137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
136 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
137 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139
138
140 c.commit_statuses = ChangesetStatus.STATUSES
139 c.commit_statuses = ChangesetStatus.STATUSES
141 c.inline_comments = []
140 c.inline_comments = []
142 c.files = []
141 c.files = []
143
142
144 c.comments = []
143 c.comments = []
145 c.unresolved_comments = []
144 c.unresolved_comments = []
146 c.resolved_comments = []
145 c.resolved_comments = []
147
146
148 # Single commit
147 # Single commit
149 if single_commit:
148 if single_commit:
150 commit = c.commit_ranges[0]
149 commit = c.commit_ranges[0]
151 c.comments = CommentsModel().get_comments(
150 c.comments = CommentsModel().get_comments(
152 self.db_repo.repo_id,
151 self.db_repo.repo_id,
153 revision=commit.raw_id)
152 revision=commit.raw_id)
154
153
155 # comments from PR
154 # comments from PR
156 statuses = ChangesetStatusModel().get_statuses(
155 statuses = ChangesetStatusModel().get_statuses(
157 self.db_repo.repo_id, commit.raw_id,
156 self.db_repo.repo_id, commit.raw_id,
158 with_revisions=True)
157 with_revisions=True)
159
158
160 prs = set()
159 prs = set()
161 reviewers = list()
160 reviewers = list()
162 reviewers_duplicates = set() # to not have duplicates from multiple votes
161 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 for c_status in statuses:
162 for c_status in statuses:
164
163
165 # extract associated pull-requests from votes
164 # extract associated pull-requests from votes
166 if c_status.pull_request:
165 if c_status.pull_request:
167 prs.add(c_status.pull_request)
166 prs.add(c_status.pull_request)
168
167
169 # extract reviewers
168 # extract reviewers
170 _user_id = c_status.author.user_id
169 _user_id = c_status.author.user_id
171 if _user_id not in reviewers_duplicates:
170 if _user_id not in reviewers_duplicates:
172 reviewers.append(
171 reviewers.append(
173 StrictAttributeDict({
172 StrictAttributeDict({
174 'user': c_status.author,
173 'user': c_status.author,
175
174
176 # fake attributed for commit, page that we don't have
175 # fake attributed for commit, page that we don't have
177 # but we share the display with PR page
176 # but we share the display with PR page
178 'mandatory': False,
177 'mandatory': False,
179 'reasons': [],
178 'reasons': [],
180 'rule_user_group_data': lambda: None
179 'rule_user_group_data': lambda: None
181 })
180 })
182 )
181 )
183 reviewers_duplicates.add(_user_id)
182 reviewers_duplicates.add(_user_id)
184
183
185 c.reviewers_count = len(reviewers)
184 c.reviewers_count = len(reviewers)
186 c.observers_count = 0
185 c.observers_count = 0
187
186
188 # from associated statuses, check the pull requests, and
187 # from associated statuses, check the pull requests, and
189 # show comments from them
188 # show comments from them
190 for pr in prs:
189 for pr in prs:
191 c.comments.extend(pr.comments)
190 c.comments.extend(pr.comments)
192
191
193 c.unresolved_comments = CommentsModel()\
192 c.unresolved_comments = CommentsModel()\
194 .get_commit_unresolved_todos(commit.raw_id)
193 .get_commit_unresolved_todos(commit.raw_id)
195 c.resolved_comments = CommentsModel()\
194 c.resolved_comments = CommentsModel()\
196 .get_commit_resolved_todos(commit.raw_id)
195 .get_commit_resolved_todos(commit.raw_id)
197
196
198 c.inline_comments_flat = CommentsModel()\
197 c.inline_comments_flat = CommentsModel()\
199 .get_commit_inline_comments(commit.raw_id)
198 .get_commit_inline_comments(commit.raw_id)
200
199
201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
200 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 statuses, reviewers)
201 statuses, reviewers)
203
202
204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
203 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205
204
206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
205 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207
206
208 for review_obj, member, reasons, mandatory, status in review_statuses:
207 for review_obj, member, reasons, mandatory, status in review_statuses:
209 member_reviewer = h.reviewer_as_json(
208 member_reviewer = h.reviewer_as_json(
210 member, reasons=reasons, mandatory=mandatory, role=None,
209 member, reasons=reasons, mandatory=mandatory, role=None,
211 user_group=None
210 user_group=None
212 )
211 )
213
212
214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
213 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 member_reviewer['review_status'] = current_review_status
214 member_reviewer['review_status'] = current_review_status
216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
215 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 member_reviewer['allowed_to_update'] = False
216 member_reviewer['allowed_to_update'] = False
218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
217 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219
218
220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
219 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
221
220
222 # NOTE(marcink): this uses the same voting logic as in pull-requests
221 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
222 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
223 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225
224
226 diff = None
225 diff = None
227 # Iterate over ranges (default commit view is always one commit)
226 # Iterate over ranges (default commit view is always one commit)
228 for commit in c.commit_ranges:
227 for commit in c.commit_ranges:
229 c.changes[commit.raw_id] = []
228 c.changes[commit.raw_id] = []
230
229
231 commit2 = commit
230 commit2 = commit
232 commit1 = commit.first_parent
231 commit1 = commit.first_parent
233
232
234 if method == 'show':
233 if method == 'show':
235 inline_comments = CommentsModel().get_inline_comments(
234 inline_comments = CommentsModel().get_inline_comments(
236 self.db_repo.repo_id, revision=commit.raw_id)
235 self.db_repo.repo_id, revision=commit.raw_id)
237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
236 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 inline_comments))
237 inline_comments))
239 c.inline_comments = inline_comments
238 c.inline_comments = inline_comments
240
239
241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
240 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 self.db_repo)
241 self.db_repo)
243 cache_file_path = diff_cache_exist(
242 cache_file_path = diff_cache_exist(
244 cache_path, 'diff', commit.raw_id,
243 cache_path, 'diff', commit.raw_id,
245 hide_whitespace_changes, diff_context, c.fulldiff)
244 hide_whitespace_changes, diff_context, c.fulldiff)
246
245
247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
246 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 force_recache = str2bool(self.request.GET.get('force_recache'))
247 force_recache = str2bool(self.request.GET.get('force_recache'))
249
248
250 cached_diff = None
249 cached_diff = None
251 if caching_enabled:
250 if caching_enabled:
252 cached_diff = load_cached_diff(cache_file_path)
251 cached_diff = load_cached_diff(cache_file_path)
253
252
254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
253 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 if not force_recache and has_proper_diff_cache:
254 if not force_recache and has_proper_diff_cache:
256 diffset = cached_diff['diff']
255 diffset = cached_diff['diff']
257 else:
256 else:
258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
257 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 commit1, commit2,
258 commit1, commit2,
260 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
261 context=diff_context)
260 context=diff_context)
262
261
263 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
262 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
264 diff_limit=diff_limit,
263 diff_limit=diff_limit,
265 file_limit=file_limit,
264 file_limit=file_limit,
266 show_full_diff=c.fulldiff)
265 show_full_diff=c.fulldiff)
267
266
268 _parsed = diff_processor.prepare()
267 _parsed = diff_processor.prepare()
269
268
270 diffset = codeblocks.DiffSet(
269 diffset = codeblocks.DiffSet(
271 repo_name=self.db_repo_name,
270 repo_name=self.db_repo_name,
272 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 source_node_getter=codeblocks.diffset_node_getter(commit1),
273 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 target_node_getter=codeblocks.diffset_node_getter(commit2))
274
273
275 diffset = self.path_filter.render_patchset_filtered(
274 diffset = self.path_filter.render_patchset_filtered(
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277
276
278 # save cached diff
277 # save cached diff
279 if caching_enabled:
278 if caching_enabled:
280 cache_diff(cache_file_path, diffset, None)
279 cache_diff(cache_file_path, diffset, None)
281
280
282 c.limited_diff = diffset.limited_diff
281 c.limited_diff = diffset.limited_diff
283 c.changes[commit.raw_id] = diffset
282 c.changes[commit.raw_id] = diffset
284 else:
283 else:
285 # TODO(marcink): no cache usage here...
284 # TODO(marcink): no cache usage here...
286 _diff = self.rhodecode_vcs_repo.get_diff(
285 _diff = self.rhodecode_vcs_repo.get_diff(
287 commit1, commit2,
286 commit1, commit2,
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
289 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
288 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
290 diff_limit=diff_limit,
289 diff_limit=diff_limit,
291 file_limit=file_limit, show_full_diff=c.fulldiff)
290 file_limit=file_limit, show_full_diff=c.fulldiff)
292 # downloads/raw we only need RAW diff nothing else
291 # downloads/raw we only need RAW diff nothing else
293 diff = self.path_filter.get_raw_patch(diff_processor)
292 diff = self.path_filter.get_raw_patch(diff_processor)
294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
295
294
296 # sort comments by how they were generated
295 # sort comments by how they were generated
297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
298 c.at_version_num = None
297 c.at_version_num = None
299
298
300 if len(c.commit_ranges) == 1:
299 if len(c.commit_ranges) == 1:
301 c.commit = c.commit_ranges[0]
300 c.commit = c.commit_ranges[0]
302 c.parent_tmpl = ''.join(
301 c.parent_tmpl = ''.join(
303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
304
303
305 if method == 'download':
304 if method == 'download':
306 response = Response(diff)
305 response = Response(diff)
307 response.content_type = 'text/plain'
306 response.content_type = 'text/plain'
308 response.content_disposition = (
307 response.content_disposition = (
309 'attachment; filename=%s.diff' % commit_id_range[:12])
308 'attachment; filename=%s.diff' % commit_id_range[:12])
310 return response
309 return response
311 elif method == 'patch':
310 elif method == 'patch':
312
311
313 c.diff = safe_str(diff)
312 c.diff = safe_str(diff)
314 patch = render(
313 patch = render(
315 'rhodecode:templates/changeset/patch_changeset.mako',
314 'rhodecode:templates/changeset/patch_changeset.mako',
316 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
317 response = Response(patch)
316 response = Response(patch)
318 response.content_type = 'text/plain'
317 response.content_type = 'text/plain'
319 return response
318 return response
320 elif method == 'raw':
319 elif method == 'raw':
321 response = Response(diff)
320 response = Response(diff)
322 response.content_type = 'text/plain'
321 response.content_type = 'text/plain'
323 return response
322 return response
324 elif method == 'show':
323 elif method == 'show':
325 if len(c.commit_ranges) == 1:
324 if len(c.commit_ranges) == 1:
326 html = render(
325 html = render(
327 'rhodecode:templates/changeset/changeset.mako',
326 'rhodecode:templates/changeset/changeset.mako',
328 self._get_template_context(c), self.request)
327 self._get_template_context(c), self.request)
329 return Response(html)
328 return Response(html)
330 else:
329 else:
331 c.ancestor = None
330 c.ancestor = None
332 c.target_repo = self.db_repo
331 c.target_repo = self.db_repo
333 html = render(
332 html = render(
334 'rhodecode:templates/changeset/changeset_range.mako',
333 'rhodecode:templates/changeset/changeset_range.mako',
335 self._get_template_context(c), self.request)
334 self._get_template_context(c), self.request)
336 return Response(html)
335 return Response(html)
337
336
338 raise HTTPBadRequest()
337 raise HTTPBadRequest()
339
338
340 @LoginRequired()
339 @LoginRequired()
341 @HasRepoPermissionAnyDecorator(
340 @HasRepoPermissionAnyDecorator(
342 'repository.read', 'repository.write', 'repository.admin')
341 'repository.read', 'repository.write', 'repository.admin')
343 def repo_commit_show(self):
342 def repo_commit_show(self):
344 commit_id = self.request.matchdict['commit_id']
343 commit_id = self.request.matchdict['commit_id']
345 return self._commit(commit_id, method='show')
344 return self._commit(commit_id, method='show')
346
345
347 @LoginRequired()
346 @LoginRequired()
348 @HasRepoPermissionAnyDecorator(
347 @HasRepoPermissionAnyDecorator(
349 'repository.read', 'repository.write', 'repository.admin')
348 'repository.read', 'repository.write', 'repository.admin')
350 def repo_commit_raw(self):
349 def repo_commit_raw(self):
351 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
352 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
353
352
354 @LoginRequired()
353 @LoginRequired()
355 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
356 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
357 def repo_commit_patch(self):
356 def repo_commit_patch(self):
358 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
359 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
360
359
361 @LoginRequired()
360 @LoginRequired()
362 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
363 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
364 def repo_commit_download(self):
363 def repo_commit_download(self):
365 commit_id = self.request.matchdict['commit_id']
364 commit_id = self.request.matchdict['commit_id']
366 return self._commit(commit_id, method='download')
365 return self._commit(commit_id, method='download')
367
366
368 def _commit_comments_create(self, commit_id, comments):
367 def _commit_comments_create(self, commit_id, comments):
369 _ = self.request.translate
368 _ = self.request.translate
370 data = {}
369 data = {}
371 if not comments:
370 if not comments:
372 return
371 return
373
372
374 commit = self.db_repo.get_commit(commit_id)
373 commit = self.db_repo.get_commit(commit_id)
375
374
376 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
377 for entry in comments:
376 for entry in comments:
378 c = self.load_default_context()
377 c = self.load_default_context()
379 comment_type = entry['comment_type']
378 comment_type = entry['comment_type']
380 text = entry['text']
379 text = entry['text']
381 status = entry['status']
380 status = entry['status']
382 is_draft = str2bool(entry['is_draft'])
381 is_draft = str2bool(entry['is_draft'])
383 resolves_comment_id = entry['resolves_comment_id']
382 resolves_comment_id = entry['resolves_comment_id']
384 f_path = entry['f_path']
383 f_path = entry['f_path']
385 line_no = entry['line']
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 if status:
387 if status:
389 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
390 % {'transition_icon': '>',
389 % {'transition_icon': '>',
391 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
392
391
393 comment = CommentsModel().create(
392 comment = CommentsModel().create(
394 text=text,
393 text=text,
395 repo=self.db_repo.repo_id,
394 repo=self.db_repo.repo_id,
396 user=self._rhodecode_db_user.user_id,
395 user=self._rhodecode_db_user.user_id,
397 commit_id=commit_id,
396 commit_id=commit_id,
398 f_path=f_path,
397 f_path=f_path,
399 line_no=line_no,
398 line_no=line_no,
400 status_change=(ChangesetStatus.get_status_lbl(status)
399 status_change=(ChangesetStatus.get_status_lbl(status)
401 if status else None),
400 if status else None),
402 status_change_type=status,
401 status_change_type=status,
403 comment_type=comment_type,
402 comment_type=comment_type,
404 is_draft=is_draft,
403 is_draft=is_draft,
405 resolves_comment_id=resolves_comment_id,
404 resolves_comment_id=resolves_comment_id,
406 auth_user=self._rhodecode_user,
405 auth_user=self._rhodecode_user,
407 send_email=not is_draft, # skip notification for draft comments
406 send_email=not is_draft, # skip notification for draft comments
408 )
407 )
409 is_inline = comment.is_inline
408 is_inline = comment.is_inline
410
409
411 # get status if set !
410 # get status if set !
412 if status:
411 if status:
413 # `dont_allow_on_closed_pull_request = True` means
412 # `dont_allow_on_closed_pull_request = True` means
414 # if latest status was from pull request and it's closed
413 # if latest status was from pull request and it's closed
415 # disallow changing status !
414 # disallow changing status !
416
415
417 try:
416 try:
418 ChangesetStatusModel().set_status(
417 ChangesetStatusModel().set_status(
419 self.db_repo.repo_id,
418 self.db_repo.repo_id,
420 status,
419 status,
421 self._rhodecode_db_user.user_id,
420 self._rhodecode_db_user.user_id,
422 comment,
421 comment,
423 revision=commit_id,
422 revision=commit_id,
424 dont_allow_on_closed_pull_request=True
423 dont_allow_on_closed_pull_request=True
425 )
424 )
426 except StatusChangeOnClosedPullRequestError:
425 except StatusChangeOnClosedPullRequestError:
427 msg = _('Changing the status of a commit associated with '
426 msg = _('Changing the status of a commit associated with '
428 'a closed pull request is not allowed')
427 'a closed pull request is not allowed')
429 log.exception(msg)
428 log.exception(msg)
430 h.flash(msg, category='warning')
429 h.flash(msg, category='warning')
431 raise HTTPFound(h.route_path(
430 raise HTTPFound(h.route_path(
432 'repo_commit', repo_name=self.db_repo_name,
431 'repo_commit', repo_name=self.db_repo_name,
433 commit_id=commit_id))
432 commit_id=commit_id))
434
433
435 Session().flush()
434 Session().flush()
436 # this is somehow required to get access to some relationship
435 # this is somehow required to get access to some relationship
437 # loaded on comment
436 # loaded on comment
438 Session().refresh(comment)
437 Session().refresh(comment)
439
438
440 # skip notifications for drafts
439 # skip notifications for drafts
441 if not is_draft:
440 if not is_draft:
442 CommentsModel().trigger_commit_comment_hook(
441 CommentsModel().trigger_commit_comment_hook(
443 self.db_repo, self._rhodecode_user, 'create',
442 self.db_repo, self._rhodecode_user, 'create',
444 data={'comment': comment, 'commit': commit})
443 data={'comment': comment, 'commit': commit})
445
444
446 comment_id = comment.comment_id
445 comment_id = comment.comment_id
447 data[comment_id] = {
446 data[comment_id] = {
448 'target_id': target_elem_id
447 'target_id': target_elem_id
449 }
448 }
450 Session().flush()
449 Session().flush()
451
450
452 c.co = comment
451 c.co = comment
453 c.at_version_num = 0
452 c.at_version_num = 0
454 c.is_new = True
453 c.is_new = True
455 rendered_comment = render(
454 rendered_comment = render(
456 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
457 self._get_template_context(c), self.request)
456 self._get_template_context(c), self.request)
458
457
459 data[comment_id].update(comment.get_dict())
458 data[comment_id].update(comment.get_dict())
460 data[comment_id].update({'rendered_text': rendered_comment})
459 data[comment_id].update({'rendered_text': rendered_comment})
461
460
462 # finalize, commit and redirect
461 # finalize, commit and redirect
463 Session().commit()
462 Session().commit()
464
463
465 # skip channelstream for draft comments
464 # skip channelstream for draft comments
466 if not all_drafts:
465 if not all_drafts:
467 comment_broadcast_channel = channelstream.comment_channel(
466 comment_broadcast_channel = channelstream.comment_channel(
468 self.db_repo_name, commit_obj=commit)
467 self.db_repo_name, commit_obj=commit)
469
468
470 comment_data = data
469 comment_data = data
471 posted_comment_type = 'inline' if is_inline else 'general'
470 posted_comment_type = 'inline' if is_inline else 'general'
472 if len(data) == 1:
471 if len(data) == 1:
473 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
474 else:
473 else:
475 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
476
475
477 channelstream.comment_channelstream_push(
476 channelstream.comment_channelstream_push(
478 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
479 comment_data=comment_data)
478 comment_data=comment_data)
480
479
481 return data
480 return data
482
481
483 @LoginRequired()
482 @LoginRequired()
484 @NotAnonymous()
483 @NotAnonymous()
485 @HasRepoPermissionAnyDecorator(
484 @HasRepoPermissionAnyDecorator(
486 'repository.read', 'repository.write', 'repository.admin')
485 'repository.read', 'repository.write', 'repository.admin')
487 @CSRFRequired()
486 @CSRFRequired()
488 def repo_commit_comment_create(self):
487 def repo_commit_comment_create(self):
489 _ = self.request.translate
488 _ = self.request.translate
490 commit_id = self.request.matchdict['commit_id']
489 commit_id = self.request.matchdict['commit_id']
491
490
492 multi_commit_ids = []
491 multi_commit_ids = []
493 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
494 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
495 if _commit_id not in multi_commit_ids:
494 if _commit_id not in multi_commit_ids:
496 multi_commit_ids.append(_commit_id)
495 multi_commit_ids.append(_commit_id)
497
496
498 commit_ids = multi_commit_ids or [commit_id]
497 commit_ids = multi_commit_ids or [commit_id]
499
498
500 data = []
499 data = []
501 # Multiple comments for each passed commit id
500 # Multiple comments for each passed commit id
502 for current_id in filter(None, commit_ids):
501 for current_id in filter(None, commit_ids):
503 comment_data = {
502 comment_data = {
504 'comment_type': self.request.POST.get('comment_type'),
503 'comment_type': self.request.POST.get('comment_type'),
505 'text': self.request.POST.get('text'),
504 'text': self.request.POST.get('text'),
506 'status': self.request.POST.get('changeset_status', None),
505 'status': self.request.POST.get('changeset_status', None),
507 'is_draft': self.request.POST.get('draft'),
506 'is_draft': self.request.POST.get('draft'),
508 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
509 'close_pull_request': self.request.POST.get('close_pull_request'),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
510 'f_path': self.request.POST.get('f_path'),
509 'f_path': self.request.POST.get('f_path'),
511 'line': self.request.POST.get('line'),
510 'line': self.request.POST.get('line'),
512 }
511 }
513 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
514 data.append(comment)
513 data.append(comment)
515
514
516 return data if len(data) > 1 else data[0]
515 return data if len(data) > 1 else data[0]
517
516
518 @LoginRequired()
517 @LoginRequired()
519 @NotAnonymous()
518 @NotAnonymous()
520 @HasRepoPermissionAnyDecorator(
519 @HasRepoPermissionAnyDecorator(
521 'repository.read', 'repository.write', 'repository.admin')
520 'repository.read', 'repository.write', 'repository.admin')
522 @CSRFRequired()
521 @CSRFRequired()
523 def repo_commit_comment_preview(self):
522 def repo_commit_comment_preview(self):
524 # Technically a CSRF token is not needed as no state changes with this
523 # Technically a CSRF token is not needed as no state changes with this
525 # call. However, as this is a POST is better to have it, so automated
524 # call. However, as this is a POST is better to have it, so automated
526 # tools don't flag it as potential CSRF.
525 # tools don't flag it as potential CSRF.
527 # Post is required because the payload could be bigger than the maximum
526 # Post is required because the payload could be bigger than the maximum
528 # allowed by GET.
527 # allowed by GET.
529
528
530 text = self.request.POST.get('text')
529 text = self.request.POST.get('text')
531 renderer = self.request.POST.get('renderer') or 'rst'
530 renderer = self.request.POST.get('renderer') or 'rst'
532 if text:
531 if text:
533 return h.render(text, renderer=renderer, mentions=True,
532 return h.render(text, renderer=renderer, mentions=True,
534 repo_name=self.db_repo_name)
533 repo_name=self.db_repo_name)
535 return ''
534 return ''
536
535
537 @LoginRequired()
536 @LoginRequired()
538 @HasRepoPermissionAnyDecorator(
537 @HasRepoPermissionAnyDecorator(
539 'repository.read', 'repository.write', 'repository.admin')
538 'repository.read', 'repository.write', 'repository.admin')
540 @CSRFRequired()
539 @CSRFRequired()
541 def repo_commit_comment_history_view(self):
540 def repo_commit_comment_history_view(self):
542 c = self.load_default_context()
541 c = self.load_default_context()
543 comment_id = self.request.matchdict['comment_id']
542 comment_id = self.request.matchdict['comment_id']
544 comment_history_id = self.request.matchdict['comment_history_id']
543 comment_history_id = self.request.matchdict['comment_history_id']
545
544
546 comment = ChangesetComment.get_or_404(comment_id)
545 comment = ChangesetComment.get_or_404(comment_id)
547 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
548 if comment.draft and not comment_owner:
547 if comment.draft and not comment_owner:
549 # if we see draft comments history, we only allow this for owner
548 # if we see draft comments history, we only allow this for owner
550 raise HTTPNotFound()
549 raise HTTPNotFound()
551
550
552 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
553 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
554
553
555 if is_repo_comment:
554 if is_repo_comment:
556 c.comment_history = comment_history
555 c.comment_history = comment_history
557
556
558 rendered_comment = render(
557 rendered_comment = render(
559 'rhodecode:templates/changeset/comment_history.mako',
558 'rhodecode:templates/changeset/comment_history.mako',
560 self._get_template_context(c), self.request)
559 self._get_template_context(c), self.request)
561 return rendered_comment
560 return rendered_comment
562 else:
561 else:
563 log.warning('No permissions for user %s to show comment_history_id: %s',
562 log.warning('No permissions for user %s to show comment_history_id: %s',
564 self._rhodecode_db_user, comment_history_id)
563 self._rhodecode_db_user, comment_history_id)
565 raise HTTPNotFound()
564 raise HTTPNotFound()
566
565
567 @LoginRequired()
566 @LoginRequired()
568 @NotAnonymous()
567 @NotAnonymous()
569 @HasRepoPermissionAnyDecorator(
568 @HasRepoPermissionAnyDecorator(
570 'repository.read', 'repository.write', 'repository.admin')
569 'repository.read', 'repository.write', 'repository.admin')
571 @CSRFRequired()
570 @CSRFRequired()
572 def repo_commit_comment_attachment_upload(self):
571 def repo_commit_comment_attachment_upload(self):
573 c = self.load_default_context()
572 c = self.load_default_context()
574 upload_key = 'attachment'
573 upload_key = 'attachment'
575
574
576 file_obj = self.request.POST.get(upload_key)
575 file_obj = self.request.POST.get(upload_key)
577
576
578 if file_obj is None:
577 if file_obj is None:
579 self.request.response.status = 400
578 self.request.response.status = 400
580 return {'store_fid': None,
579 return {'store_fid': None,
581 'access_path': None,
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 if not hasattr(file_obj, 'filename'):
583 if not hasattr(file_obj, 'filename'):
585 self.request.response.status = 400
584 self.request.response.status = 400
586 return {'store_fid': None,
585 return {'store_fid': None,
587 'access_path': None,
586 'access_path': None,
588 'error': 'filename cannot be read from the data field'}
587 'error': 'filename cannot be read from the data field'}
589
588
590 filename = file_obj.filename
589 filename = file_obj.filename
591 file_display_name = filename
590 file_display_name = filename
592
591
593 metadata = {
592 metadata = {
594 'user_uploaded': {'username': self._rhodecode_user.username,
593 'user_uploaded': {'username': self._rhodecode_user.username,
595 'user_id': self._rhodecode_user.user_id,
594 'user_id': self._rhodecode_user.user_id,
596 'ip': self._rhodecode_user.ip_addr}}
595 'ip': self._rhodecode_user.ip_addr}}
597
596
598 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
599 allowed_extensions = [
598 allowed_extensions = [
600 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
601 '.pptx', '.txt', '.xlsx', '.zip']
600 '.pptx', '.txt', '.xlsx', '.zip']
602 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
603
602
604 try:
603 try:
605 storage = store_utils.get_file_storage(self.request.registry.settings)
604 storage = store_utils.get_file_storage(self.request.registry.settings)
606 store_uid, metadata = storage.save_file(
605 store_uid, metadata = storage.save_file(
607 file_obj.file, filename, extra_metadata=metadata,
606 file_obj.file, filename, extra_metadata=metadata,
608 extensions=allowed_extensions, max_filesize=max_file_size)
607 extensions=allowed_extensions, max_filesize=max_file_size)
609 except FileNotAllowedException:
608 except FileNotAllowedException:
610 self.request.response.status = 400
609 self.request.response.status = 400
611 permitted_extensions = ', '.join(allowed_extensions)
610 permitted_extensions = ', '.join(allowed_extensions)
612 error_msg = 'File `{}` is not allowed. ' \
611 error_msg = 'File `{}` is not allowed. ' \
613 'Only following extensions are permitted: {}'.format(
612 'Only following extensions are permitted: {}'.format(
614 filename, permitted_extensions)
613 filename, permitted_extensions)
615 return {'store_fid': None,
614 return {'store_fid': None,
616 'access_path': None,
615 'access_path': None,
617 'error': error_msg}
616 'error': error_msg}
618 except FileOverSizeException:
617 except FileOverSizeException:
619 self.request.response.status = 400
618 self.request.response.status = 400
620 limit_mb = h.format_byte_size_binary(max_file_size)
619 limit_mb = h.format_byte_size_binary(max_file_size)
621 return {'store_fid': None,
620 return {'store_fid': None,
622 'access_path': None,
621 'access_path': None,
623 'error': 'File {} is exceeding allowed limit of {}.'.format(
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
624 filename, limit_mb)}
623 filename, limit_mb)}
625
624
626 try:
625 try:
627 entry = FileStore.create(
626 entry = FileStore.create(
628 file_uid=store_uid, filename=metadata["filename"],
627 file_uid=store_uid, filename=metadata["filename"],
629 file_hash=metadata["sha256"], file_size=metadata["size"],
628 file_hash=metadata["sha256"], file_size=metadata["size"],
630 file_display_name=file_display_name,
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 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
633 scope_repo_id=self.db_repo.repo_id
632 scope_repo_id=self.db_repo.repo_id
634 )
633 )
635 Session().add(entry)
634 Session().add(entry)
636 Session().commit()
635 Session().commit()
637 log.debug('Stored upload in DB as %s', entry)
636 log.debug('Stored upload in DB as %s', entry)
638 except Exception:
637 except Exception:
639 log.exception('Failed to store file %s', filename)
638 log.exception('Failed to store file %s', filename)
640 self.request.response.status = 400
639 self.request.response.status = 400
641 return {'store_fid': None,
640 return {'store_fid': None,
642 'access_path': None,
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 Session().commit()
644 Session().commit()
646
645
647 data = {
646 data = {
648 'store_fid': store_uid,
647 'store_fid': store_uid,
649 'access_path': h.route_path(
648 'access_path': h.route_path(
650 'download_file', fid=store_uid),
649 'download_file', fid=store_uid),
651 'fqn_access_path': h.route_url(
650 'fqn_access_path': h.route_url(
652 'download_file', fid=store_uid),
651 'download_file', fid=store_uid),
653 # for EE those are replaced by FQN links on repo-only like
652 # for EE those are replaced by FQN links on repo-only like
654 'repo_access_path': h.route_url(
653 'repo_access_path': h.route_url(
655 'download_file', fid=store_uid),
654 'download_file', fid=store_uid),
656 'repo_fqn_access_path': h.route_url(
655 'repo_fqn_access_path': h.route_url(
657 'download_file', fid=store_uid),
656 'download_file', fid=store_uid),
658 }
657 }
659 # this data is a part of CE/EE additional code
658 # this data is a part of CE/EE additional code
660 if c.rhodecode_edition_id == 'EE':
659 if c.rhodecode_edition_id == 'EE':
661 data.update({
660 data.update({
662 'repo_access_path': h.route_path(
661 'repo_access_path': h.route_path(
663 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
662 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
664 'repo_fqn_access_path': h.route_url(
663 'repo_fqn_access_path': h.route_url(
665 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
664 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
666 })
665 })
667
666
668 return data
667 return data
669
668
670 @LoginRequired()
669 @LoginRequired()
671 @NotAnonymous()
670 @NotAnonymous()
672 @HasRepoPermissionAnyDecorator(
671 @HasRepoPermissionAnyDecorator(
673 'repository.read', 'repository.write', 'repository.admin')
672 'repository.read', 'repository.write', 'repository.admin')
674 @CSRFRequired()
673 @CSRFRequired()
675 def repo_commit_comment_delete(self):
674 def repo_commit_comment_delete(self):
676 commit_id = self.request.matchdict['commit_id']
675 commit_id = self.request.matchdict['commit_id']
677 comment_id = self.request.matchdict['comment_id']
676 comment_id = self.request.matchdict['comment_id']
678
677
679 comment = ChangesetComment.get_or_404(comment_id)
678 comment = ChangesetComment.get_or_404(comment_id)
680 if not comment:
679 if not comment:
681 log.debug('Comment with id:%s not found, skipping', comment_id)
680 log.debug('Comment with id:%s not found, skipping', comment_id)
682 # comment already deleted in another call probably
681 # comment already deleted in another call probably
683 return True
682 return True
684
683
685 if comment.immutable:
684 if comment.immutable:
686 # don't allow deleting comments that are immutable
685 # don't allow deleting comments that are immutable
687 raise HTTPForbidden()
686 raise HTTPForbidden()
688
687
689 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
688 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
690 super_admin = h.HasPermissionAny('hg.admin')()
689 super_admin = h.HasPermissionAny('hg.admin')()
691 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
690 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
692 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
691 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
693 comment_repo_admin = is_repo_admin and is_repo_comment
692 comment_repo_admin = is_repo_admin and is_repo_comment
694
693
695 if comment.draft and not comment_owner:
694 if comment.draft and not comment_owner:
696 # We never allow to delete draft comments for other than owners
695 # We never allow to delete draft comments for other than owners
697 raise HTTPNotFound()
696 raise HTTPNotFound()
698
697
699 if super_admin or comment_owner or comment_repo_admin:
698 if super_admin or comment_owner or comment_repo_admin:
700 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
699 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
701 Session().commit()
700 Session().commit()
702 return True
701 return True
703 else:
702 else:
704 log.warning('No permissions for user %s to delete comment_id: %s',
703 log.warning('No permissions for user %s to delete comment_id: %s',
705 self._rhodecode_db_user, comment_id)
704 self._rhodecode_db_user, comment_id)
706 raise HTTPNotFound()
705 raise HTTPNotFound()
707
706
708 @LoginRequired()
707 @LoginRequired()
709 @NotAnonymous()
708 @NotAnonymous()
710 @HasRepoPermissionAnyDecorator(
709 @HasRepoPermissionAnyDecorator(
711 'repository.read', 'repository.write', 'repository.admin')
710 'repository.read', 'repository.write', 'repository.admin')
712 @CSRFRequired()
711 @CSRFRequired()
713 def repo_commit_comment_edit(self):
712 def repo_commit_comment_edit(self):
714 self.load_default_context()
713 self.load_default_context()
715
714
716 commit_id = self.request.matchdict['commit_id']
715 commit_id = self.request.matchdict['commit_id']
717 comment_id = self.request.matchdict['comment_id']
716 comment_id = self.request.matchdict['comment_id']
718 comment = ChangesetComment.get_or_404(comment_id)
717 comment = ChangesetComment.get_or_404(comment_id)
719
718
720 if comment.immutable:
719 if comment.immutable:
721 # don't allow deleting comments that are immutable
720 # don't allow deleting comments that are immutable
722 raise HTTPForbidden()
721 raise HTTPForbidden()
723
722
724 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
723 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
725 super_admin = h.HasPermissionAny('hg.admin')()
724 super_admin = h.HasPermissionAny('hg.admin')()
726 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
725 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
727 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
726 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
728 comment_repo_admin = is_repo_admin and is_repo_comment
727 comment_repo_admin = is_repo_admin and is_repo_comment
729
728
730 if super_admin or comment_owner or comment_repo_admin:
729 if super_admin or comment_owner or comment_repo_admin:
731 text = self.request.POST.get('text')
730 text = self.request.POST.get('text')
732 version = self.request.POST.get('version')
731 version = self.request.POST.get('version')
733 if text == comment.text:
732 if text == comment.text:
734 log.warning(
733 log.warning(
735 'Comment(repo): '
734 'Comment(repo): '
736 'Trying to create new version '
735 'Trying to create new version '
737 'with the same comment body {}'.format(
736 'with the same comment body {}'.format(
738 comment_id,
737 comment_id,
739 )
738 )
740 )
739 )
741 raise HTTPNotFound()
740 raise HTTPNotFound()
742
741
743 if version.isdigit():
742 if version.isdigit():
744 version = int(version)
743 version = int(version)
745 else:
744 else:
746 log.warning(
745 log.warning(
747 'Comment(repo): Wrong version type {} {} '
746 'Comment(repo): Wrong version type {} {} '
748 'for comment {}'.format(
747 'for comment {}'.format(
749 version,
748 version,
750 type(version),
749 type(version),
751 comment_id,
750 comment_id,
752 )
751 )
753 )
752 )
754 raise HTTPNotFound()
753 raise HTTPNotFound()
755
754
756 try:
755 try:
757 comment_history = CommentsModel().edit(
756 comment_history = CommentsModel().edit(
758 comment_id=comment_id,
757 comment_id=comment_id,
759 text=text,
758 text=text,
760 auth_user=self._rhodecode_user,
759 auth_user=self._rhodecode_user,
761 version=version,
760 version=version,
762 )
761 )
763 except CommentVersionMismatch:
762 except CommentVersionMismatch:
764 raise HTTPConflict()
763 raise HTTPConflict()
765
764
766 if not comment_history:
765 if not comment_history:
767 raise HTTPNotFound()
766 raise HTTPNotFound()
768
767
769 if not comment.draft:
768 if not comment.draft:
770 commit = self.db_repo.get_commit(commit_id)
769 commit = self.db_repo.get_commit(commit_id)
771 CommentsModel().trigger_commit_comment_hook(
770 CommentsModel().trigger_commit_comment_hook(
772 self.db_repo, self._rhodecode_user, 'edit',
771 self.db_repo, self._rhodecode_user, 'edit',
773 data={'comment': comment, 'commit': commit})
772 data={'comment': comment, 'commit': commit})
774
773
775 Session().commit()
774 Session().commit()
776 return {
775 return {
777 'comment_history_id': comment_history.comment_history_id,
776 'comment_history_id': comment_history.comment_history_id,
778 'comment_id': comment.comment_id,
777 'comment_id': comment.comment_id,
779 'comment_version': comment_history.version,
778 'comment_version': comment_history.version,
780 'comment_author_username': comment_history.author.username,
779 'comment_author_username': comment_history.author.username,
781 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
780 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
782 'comment_created_on': h.age_component(comment_history.created_on,
781 'comment_created_on': h.age_component(comment_history.created_on,
783 time_is_local=True),
782 time_is_local=True),
784 }
783 }
785 else:
784 else:
786 log.warning('No permissions for user %s to edit comment_id: %s',
785 log.warning('No permissions for user %s to edit comment_id: %s',
787 self._rhodecode_db_user, comment_id)
786 self._rhodecode_db_user, comment_id)
788 raise HTTPNotFound()
787 raise HTTPNotFound()
789
788
790 @LoginRequired()
789 @LoginRequired()
791 @HasRepoPermissionAnyDecorator(
790 @HasRepoPermissionAnyDecorator(
792 'repository.read', 'repository.write', 'repository.admin')
791 'repository.read', 'repository.write', 'repository.admin')
793 def repo_commit_data(self):
792 def repo_commit_data(self):
794 commit_id = self.request.matchdict['commit_id']
793 commit_id = self.request.matchdict['commit_id']
795 self.load_default_context()
794 self.load_default_context()
796
795
797 try:
796 try:
798 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
797 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
799 except CommitDoesNotExistError as e:
798 except CommitDoesNotExistError as e:
800 return EmptyCommit(message=str(e))
799 return EmptyCommit(message=str(e))
801
800
802 @LoginRequired()
801 @LoginRequired()
803 @HasRepoPermissionAnyDecorator(
802 @HasRepoPermissionAnyDecorator(
804 'repository.read', 'repository.write', 'repository.admin')
803 'repository.read', 'repository.write', 'repository.admin')
805 def repo_commit_children(self):
804 def repo_commit_children(self):
806 commit_id = self.request.matchdict['commit_id']
805 commit_id = self.request.matchdict['commit_id']
807 self.load_default_context()
806 self.load_default_context()
808
807
809 try:
808 try:
810 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
809 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
811 children = commit.children
810 children = commit.children
812 except CommitDoesNotExistError:
811 except CommitDoesNotExistError:
813 children = []
812 children = []
814
813
815 result = {"results": children}
814 result = {"results": children}
816 return result
815 return result
817
816
818 @LoginRequired()
817 @LoginRequired()
819 @HasRepoPermissionAnyDecorator(
818 @HasRepoPermissionAnyDecorator(
820 'repository.read', 'repository.write', 'repository.admin')
819 'repository.read', 'repository.write', 'repository.admin')
821 def repo_commit_parents(self):
820 def repo_commit_parents(self):
822 commit_id = self.request.matchdict['commit_id']
821 commit_id = self.request.matchdict['commit_id']
823 self.load_default_context()
822 self.load_default_context()
824
823
825 try:
824 try:
826 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
825 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
827 parents = commit.parents
826 parents = commit.parents
828 except CommitDoesNotExistError:
827 except CommitDoesNotExistError:
829 parents = []
828 parents = []
830 result = {"results": parents}
829 result = {"results": parents}
831 return result
830 return result
@@ -1,307 +1,305 b''
1
2
3 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 import logging
20 import logging
23
21
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
22 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25
23
26 from pyramid.renderers import render
24 from pyramid.renderers import render
27 from pyramid.response import Response
25 from pyramid.response import Response
28
26
29 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
30
28
31 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
30 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
32 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.lib.utils2 import str2bool
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
34 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 from rhodecode.lib.vcs.exceptions import (
35 from rhodecode.lib.vcs.exceptions import (
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
36 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 NodeDoesNotExistError)
37 NodeDoesNotExistError)
40 from rhodecode.model.db import Repository, ChangesetStatus
38 from rhodecode.model.db import Repository, ChangesetStatus
41
39
42 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
43
41
44
42
45 class RepoCompareView(RepoAppView):
43 class RepoCompareView(RepoAppView):
46 def load_default_context(self):
44 def load_default_context(self):
47 c = self._get_local_tmpl_context(include_app_defaults=True)
45 c = self._get_local_tmpl_context(include_app_defaults=True)
48 c.rhodecode_repo = self.rhodecode_vcs_repo
46 c.rhodecode_repo = self.rhodecode_vcs_repo
49 return c
47 return c
50
48
51 def _get_commit_or_redirect(
49 def _get_commit_or_redirect(
52 self, ref, ref_type, repo, redirect_after=True, partial=False):
50 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 """
51 """
54 This is a safe way to get a commit. If an error occurs it
52 This is a safe way to get a commit. If an error occurs it
55 redirects to a commit with a proper message. If partial is set
53 redirects to a commit with a proper message. If partial is set
56 then it does not do redirect raise and throws an exception instead.
54 then it does not do redirect raise and throws an exception instead.
57 """
55 """
58 _ = self.request.translate
56 _ = self.request.translate
59 try:
57 try:
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
58 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 except EmptyRepositoryError:
59 except EmptyRepositoryError:
62 if not redirect_after:
60 if not redirect_after:
63 return repo.scm_instance().EMPTY_COMMIT
61 return repo.scm_instance().EMPTY_COMMIT
64 h.flash(h.literal(_('There are no commits yet')),
62 h.flash(h.literal(_('There are no commits yet')),
65 category='warning')
63 category='warning')
66 if not partial:
64 if not partial:
67 raise HTTPFound(
65 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=repo.repo_name))
66 h.route_path('repo_summary', repo_name=repo.repo_name))
69 raise HTTPBadRequest()
67 raise HTTPBadRequest()
70
68
71 except RepositoryError as e:
69 except RepositoryError as e:
72 log.exception(safe_str(e))
70 log.exception(safe_str(e))
73 h.flash(h.escape(safe_str(e)), category='warning')
71 h.flash(h.escape(safe_str(e)), category='warning')
74 if not partial:
72 if not partial:
75 raise HTTPFound(
73 raise HTTPFound(
76 h.route_path('repo_summary', repo_name=repo.repo_name))
74 h.route_path('repo_summary', repo_name=repo.repo_name))
77 raise HTTPBadRequest()
75 raise HTTPBadRequest()
78
76
79 @LoginRequired()
77 @LoginRequired()
80 @HasRepoPermissionAnyDecorator(
78 @HasRepoPermissionAnyDecorator(
81 'repository.read', 'repository.write', 'repository.admin')
79 'repository.read', 'repository.write', 'repository.admin')
82 def compare_select(self):
80 def compare_select(self):
83 _ = self.request.translate
81 _ = self.request.translate
84 c = self.load_default_context()
82 c = self.load_default_context()
85
83
86 source_repo = self.db_repo_name
84 source_repo = self.db_repo_name
87 target_repo = self.request.GET.get('target_repo', source_repo)
85 target_repo = self.request.GET.get('target_repo', source_repo)
88 c.source_repo = Repository.get_by_repo_name(source_repo)
86 c.source_repo = Repository.get_by_repo_name(source_repo)
89 c.target_repo = Repository.get_by_repo_name(target_repo)
87 c.target_repo = Repository.get_by_repo_name(target_repo)
90
88
91 if c.source_repo is None or c.target_repo is None:
89 if c.source_repo is None or c.target_repo is None:
92 raise HTTPNotFound()
90 raise HTTPNotFound()
93
91
94 c.compare_home = True
92 c.compare_home = True
95 c.commit_ranges = []
93 c.commit_ranges = []
96 c.collapse_all_commits = False
94 c.collapse_all_commits = False
97 c.diffset = None
95 c.diffset = None
98 c.limited_diff = False
96 c.limited_diff = False
99 c.source_ref = c.target_ref = _('Select commit')
97 c.source_ref = c.target_ref = _('Select commit')
100 c.source_ref_type = ""
98 c.source_ref_type = ""
101 c.target_ref_type = ""
99 c.target_ref_type = ""
102 c.commit_statuses = ChangesetStatus.STATUSES
100 c.commit_statuses = ChangesetStatus.STATUSES
103 c.preview_mode = False
101 c.preview_mode = False
104 c.file_path = None
102 c.file_path = None
105
103
106 return self._get_template_context(c)
104 return self._get_template_context(c)
107
105
108 @LoginRequired()
106 @LoginRequired()
109 @HasRepoPermissionAnyDecorator(
107 @HasRepoPermissionAnyDecorator(
110 'repository.read', 'repository.write', 'repository.admin')
108 'repository.read', 'repository.write', 'repository.admin')
111 def compare(self):
109 def compare(self):
112 _ = self.request.translate
110 _ = self.request.translate
113 c = self.load_default_context()
111 c = self.load_default_context()
114
112
115 source_ref_type = self.request.matchdict['source_ref_type']
113 source_ref_type = self.request.matchdict['source_ref_type']
116 source_ref = self.request.matchdict['source_ref']
114 source_ref = self.request.matchdict['source_ref']
117 target_ref_type = self.request.matchdict['target_ref_type']
115 target_ref_type = self.request.matchdict['target_ref_type']
118 target_ref = self.request.matchdict['target_ref']
116 target_ref = self.request.matchdict['target_ref']
119
117
120 # source_ref will be evaluated in source_repo
118 # source_ref will be evaluated in source_repo
121 source_repo_name = self.db_repo_name
119 source_repo_name = self.db_repo_name
122 source_path, source_id = parse_path_ref(source_ref)
120 source_path, source_id = parse_path_ref(source_ref)
123
121
124 # target_ref will be evaluated in target_repo
122 # target_ref will be evaluated in target_repo
125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
123 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 target_path, target_id = parse_path_ref(
124 target_path, target_id = parse_path_ref(
127 target_ref, default_path=self.request.GET.get('f_path', ''))
125 target_ref, default_path=self.request.GET.get('f_path', ''))
128
126
129 # if merge is True
127 # if merge is True
130 # Show what changes since the shared ancestor commit of target/source
128 # Show what changes since the shared ancestor commit of target/source
131 # the source would get if it was merged with target. Only commits
129 # the source would get if it was merged with target. Only commits
132 # which are in target but not in source will be shown.
130 # which are in target but not in source will be shown.
133 merge = str2bool(self.request.GET.get('merge'))
131 merge = str2bool(self.request.GET.get('merge'))
134 # if merge is False
132 # if merge is False
135 # Show a raw diff of source/target refs even if no ancestor exists
133 # Show a raw diff of source/target refs even if no ancestor exists
136
134
137 # c.fulldiff disables cut_off_limit
135 # c.fulldiff disables cut_off_limit
138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
136 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139
137
140 # fetch global flags of ignore ws or context lines
138 # fetch global flags of ignore ws or context lines
141 diff_context = diffs.get_diff_context(self.request)
139 diff_context = diffs.get_diff_context(self.request)
142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
140 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143
141
144 c.file_path = target_path
142 c.file_path = target_path
145 c.commit_statuses = ChangesetStatus.STATUSES
143 c.commit_statuses = ChangesetStatus.STATUSES
146
144
147 # if partial, returns just compare_commits.html (commits log)
145 # if partial, returns just compare_commits.html (commits log)
148 partial = self.request.is_xhr
146 partial = self.request.is_xhr
149
147
150 # swap url for compare_diff page
148 # swap url for compare_diff page
151 c.swap_url = h.route_path(
149 c.swap_url = h.route_path(
152 'repo_compare',
150 'repo_compare',
153 repo_name=target_repo_name,
151 repo_name=target_repo_name,
154 source_ref_type=target_ref_type,
152 source_ref_type=target_ref_type,
155 source_ref=target_ref,
153 source_ref=target_ref,
156 target_repo=source_repo_name,
154 target_repo=source_repo_name,
157 target_ref_type=source_ref_type,
155 target_ref_type=source_ref_type,
158 target_ref=source_ref,
156 target_ref=source_ref,
159 _query=dict(merge=merge and '1' or '', f_path=target_path))
157 _query=dict(merge=merge and '1' or '', f_path=target_path))
160
158
161 source_repo = Repository.get_by_repo_name(source_repo_name)
159 source_repo = Repository.get_by_repo_name(source_repo_name)
162 target_repo = Repository.get_by_repo_name(target_repo_name)
160 target_repo = Repository.get_by_repo_name(target_repo_name)
163
161
164 if source_repo is None:
162 if source_repo is None:
165 log.error('Could not find the source repo: {}'
163 log.error('Could not find the source repo: {}'
166 .format(source_repo_name))
164 .format(source_repo_name))
167 h.flash(_('Could not find the source repo: `{}`')
165 h.flash(_('Could not find the source repo: `{}`')
168 .format(h.escape(source_repo_name)), category='error')
166 .format(h.escape(source_repo_name)), category='error')
169 raise HTTPFound(
167 raise HTTPFound(
170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
168 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171
169
172 if target_repo is None:
170 if target_repo is None:
173 log.error('Could not find the target repo: {}'
171 log.error('Could not find the target repo: {}'
174 .format(source_repo_name))
172 .format(source_repo_name))
175 h.flash(_('Could not find the target repo: `{}`')
173 h.flash(_('Could not find the target repo: `{}`')
176 .format(h.escape(target_repo_name)), category='error')
174 .format(h.escape(target_repo_name)), category='error')
177 raise HTTPFound(
175 raise HTTPFound(
178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179
177
180 source_scm = source_repo.scm_instance()
178 source_scm = source_repo.scm_instance()
181 target_scm = target_repo.scm_instance()
179 target_scm = target_repo.scm_instance()
182
180
183 source_alias = source_scm.alias
181 source_alias = source_scm.alias
184 target_alias = target_scm.alias
182 target_alias = target_scm.alias
185 if source_alias != target_alias:
183 if source_alias != target_alias:
186 msg = _('The comparison of two different kinds of remote repos '
184 msg = _('The comparison of two different kinds of remote repos '
187 'is not available')
185 'is not available')
188 log.error(msg)
186 log.error(msg)
189 h.flash(msg, category='error')
187 h.flash(msg, category='error')
190 raise HTTPFound(
188 raise HTTPFound(
191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
189 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192
190
193 source_commit = self._get_commit_or_redirect(
191 source_commit = self._get_commit_or_redirect(
194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
192 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 partial=partial)
193 partial=partial)
196 target_commit = self._get_commit_or_redirect(
194 target_commit = self._get_commit_or_redirect(
197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
195 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 partial=partial)
196 partial=partial)
199
197
200 c.compare_home = False
198 c.compare_home = False
201 c.source_repo = source_repo
199 c.source_repo = source_repo
202 c.target_repo = target_repo
200 c.target_repo = target_repo
203 c.source_ref = source_ref
201 c.source_ref = source_ref
204 c.target_ref = target_ref
202 c.target_ref = target_ref
205 c.source_ref_type = source_ref_type
203 c.source_ref_type = source_ref_type
206 c.target_ref_type = target_ref_type
204 c.target_ref_type = target_ref_type
207
205
208 pre_load = ["author", "date", "message", "branch"]
206 pre_load = ["author", "date", "message", "branch"]
209 c.ancestor = None
207 c.ancestor = None
210
208
211 try:
209 try:
212 c.commit_ranges = source_scm.compare(
210 c.commit_ranges = source_scm.compare(
213 source_commit.raw_id, target_commit.raw_id,
211 source_commit.raw_id, target_commit.raw_id,
214 target_scm, merge, pre_load=pre_load) or []
212 target_scm, merge, pre_load=pre_load) or []
215 if merge:
213 if merge:
216 c.ancestor = source_scm.get_common_ancestor(
214 c.ancestor = source_scm.get_common_ancestor(
217 source_commit.raw_id, target_commit.raw_id, target_scm)
215 source_commit.raw_id, target_commit.raw_id, target_scm)
218 except RepositoryRequirementError:
216 except RepositoryRequirementError:
219 msg = _('Could not compare repos with different '
217 msg = _('Could not compare repos with different '
220 'large file settings')
218 'large file settings')
221 log.error(msg)
219 log.error(msg)
222 if partial:
220 if partial:
223 return Response(msg)
221 return Response(msg)
224 h.flash(msg, category='error')
222 h.flash(msg, category='error')
225 raise HTTPFound(
223 raise HTTPFound(
226 h.route_path('repo_compare_select',
224 h.route_path('repo_compare_select',
227 repo_name=self.db_repo_name))
225 repo_name=self.db_repo_name))
228
226
229 c.statuses = self.db_repo.statuses(
227 c.statuses = self.db_repo.statuses(
230 [x.raw_id for x in c.commit_ranges])
228 [x.raw_id for x in c.commit_ranges])
231
229
232 # auto collapse if we have more than limit
230 # auto collapse if we have more than limit
233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
231 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
232 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235
233
236 if partial: # for PR ajax commits loader
234 if partial: # for PR ajax commits loader
237 if not c.ancestor:
235 if not c.ancestor:
238 return Response('') # cannot merge if there is no ancestor
236 return Response('') # cannot merge if there is no ancestor
239
237
240 html = render(
238 html = render(
241 'rhodecode:templates/compare/compare_commits.mako',
239 'rhodecode:templates/compare/compare_commits.mako',
242 self._get_template_context(c), self.request)
240 self._get_template_context(c), self.request)
243 return Response(html)
241 return Response(html)
244
242
245 if c.ancestor:
243 if c.ancestor:
246 # case we want a simple diff without incoming commits,
244 # case we want a simple diff without incoming commits,
247 # previewing what will be merged.
245 # previewing what will be merged.
248 # Make the diff on target repo (which is known to have target_ref)
246 # Make the diff on target repo (which is known to have target_ref)
249 log.debug('Using ancestor %s as source_ref instead of %s',
247 log.debug('Using ancestor %s as source_ref instead of %s',
250 c.ancestor, source_ref)
248 c.ancestor, source_ref)
251 source_repo = target_repo
249 source_repo = target_repo
252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
250 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253
251
254 # diff_limit will cut off the whole diff if the limit is applied
252 # diff_limit will cut off the whole diff if the limit is applied
255 # otherwise it will just hide the big files from the front-end
253 # otherwise it will just hide the big files from the front-end
256 diff_limit = c.visual.cut_off_limit_diff
254 diff_limit = c.visual.cut_off_limit_diff
257 file_limit = c.visual.cut_off_limit_file
255 file_limit = c.visual.cut_off_limit_file
258
256
259 log.debug('calculating diff between '
257 log.debug('calculating diff between '
260 'source_ref:%s and target_ref:%s for repo `%s`',
258 'source_ref:%s and target_ref:%s for repo `%s`',
261 source_commit, target_commit,
259 source_commit, target_commit,
262 safe_str(source_repo.scm_instance().path))
260 safe_str(source_repo.scm_instance().path))
263
261
264 if source_commit.repository != target_commit.repository:
262 if source_commit.repository != target_commit.repository:
265
263
266 msg = _(
264 msg = _(
267 "Repositories unrelated. "
265 "Repositories unrelated. "
268 "Cannot compare commit %(commit1)s from repository %(repo1)s "
266 "Cannot compare commit %(commit1)s from repository %(repo1)s "
269 "with commit %(commit2)s from repository %(repo2)s.") % {
267 "with commit %(commit2)s from repository %(repo2)s.") % {
270 'commit1': h.show_id(source_commit),
268 'commit1': h.show_id(source_commit),
271 'repo1': source_repo.repo_name,
269 'repo1': source_repo.repo_name,
272 'commit2': h.show_id(target_commit),
270 'commit2': h.show_id(target_commit),
273 'repo2': target_repo.repo_name,
271 'repo2': target_repo.repo_name,
274 }
272 }
275 h.flash(msg, category='error')
273 h.flash(msg, category='error')
276 raise HTTPFound(
274 raise HTTPFound(
277 h.route_path('repo_compare_select',
275 h.route_path('repo_compare_select',
278 repo_name=self.db_repo_name))
276 repo_name=self.db_repo_name))
279
277
280 txt_diff = source_repo.scm_instance().get_diff(
278 txt_diff = source_repo.scm_instance().get_diff(
281 commit1=source_commit, commit2=target_commit,
279 commit1=source_commit, commit2=target_commit,
282 path=target_path, path1=source_path,
280 path=target_path, path1=source_path,
283 ignore_whitespace=hide_whitespace_changes, context=diff_context)
281 ignore_whitespace=hide_whitespace_changes, context=diff_context)
284
282
285 diff_processor = diffs.DiffProcessor(txt_diff, diff_format='newdiff',
283 diff_processor = diffs.DiffProcessor(txt_diff, diff_format='newdiff',
286 diff_limit=diff_limit,
284 diff_limit=diff_limit,
287 file_limit=file_limit,
285 file_limit=file_limit,
288 show_full_diff=c.fulldiff)
286 show_full_diff=c.fulldiff)
289 _parsed = diff_processor.prepare()
287 _parsed = diff_processor.prepare()
290
288
291 diffset = codeblocks.DiffSet(
289 diffset = codeblocks.DiffSet(
292 repo_name=source_repo.repo_name,
290 repo_name=source_repo.repo_name,
293 source_node_getter=codeblocks.diffset_node_getter(source_commit),
291 source_node_getter=codeblocks.diffset_node_getter(source_commit),
294 target_repo_name=self.db_repo_name,
292 target_repo_name=self.db_repo_name,
295 target_node_getter=codeblocks.diffset_node_getter(target_commit),
293 target_node_getter=codeblocks.diffset_node_getter(target_commit),
296 )
294 )
297 c.diffset = self.path_filter.render_patchset_filtered(
295 c.diffset = self.path_filter.render_patchset_filtered(
298 diffset, _parsed, source_ref, target_ref)
296 diffset, _parsed, source_ref, target_ref)
299
297
300 c.preview_mode = merge
298 c.preview_mode = merge
301 c.source_commit = source_commit
299 c.source_commit = source_commit
302 c.target_commit = target_commit
300 c.target_commit = target_commit
303
301
304 html = render(
302 html = render(
305 'rhodecode:templates/compare/compare_diff.mako',
303 'rhodecode:templates/compare/compare_diff.mako',
306 self._get_template_context(c), self.request)
304 self._get_template_context(c), self.request)
307 return Response(html) No newline at end of file
305 return Response(html)
@@ -1,215 +1,213 b''
1
2
3 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import datetime
20 import datetime
23
21
24 from pyramid.response import Response
22 from pyramid.response import Response
25
23
26 from rhodecode.apps._base import RepoAppView
24 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
25 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
28 from rhodecode.lib import audit_logger
26 from rhodecode.lib import audit_logger
29 from rhodecode.lib import rc_cache
27 from rhodecode.lib import rc_cache
30 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
30 LoginRequired, HasRepoPermissionAnyDecorator)
33 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
31 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
32 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
35 from rhodecode.model.db import UserApiKeys, CacheKey
33 from rhodecode.model.db import UserApiKeys, CacheKey
36
34
37 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
38
36
39
37
40 class RepoFeedView(RepoAppView):
38 class RepoFeedView(RepoAppView):
41 def load_default_context(self):
39 def load_default_context(self):
42 c = self._get_local_tmpl_context()
40 c = self._get_local_tmpl_context()
43 self._load_defaults()
41 self._load_defaults()
44 return c
42 return c
45
43
46 def _get_config(self):
44 def _get_config(self):
47 import rhodecode
45 import rhodecode
48 config = rhodecode.CONFIG
46 config = rhodecode.CONFIG
49
47
50 return {
48 return {
51 'language': 'en-us',
49 'language': 'en-us',
52 'feed_ttl': '5', # TTL of feed,
50 'feed_ttl': '5', # TTL of feed,
53 'feed_include_diff':
51 'feed_include_diff':
54 str2bool(config.get('rss_include_diff', False)),
52 str2bool(config.get('rss_include_diff', False)),
55 'feed_items_per_page':
53 'feed_items_per_page':
56 safe_int(config.get('rss_items_per_page', 20)),
54 safe_int(config.get('rss_items_per_page', 20)),
57 'feed_diff_limit':
55 'feed_diff_limit':
58 # we need to protect from parsing huge diffs here other way
56 # we need to protect from parsing huge diffs here other way
59 # we can kill the server
57 # we can kill the server
60 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
58 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
61 }
59 }
62
60
63 def _load_defaults(self):
61 def _load_defaults(self):
64 _ = self.request.translate
62 _ = self.request.translate
65 config = self._get_config()
63 config = self._get_config()
66 # common values for feeds
64 # common values for feeds
67 self.description = _('Changes on %s repository')
65 self.description = _('Changes on %s repository')
68 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
66 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
69 self.language = config["language"]
67 self.language = config["language"]
70 self.ttl = config["feed_ttl"]
68 self.ttl = config["feed_ttl"]
71 self.feed_include_diff = config['feed_include_diff']
69 self.feed_include_diff = config['feed_include_diff']
72 self.feed_diff_limit = config['feed_diff_limit']
70 self.feed_diff_limit = config['feed_diff_limit']
73 self.feed_items_per_page = config['feed_items_per_page']
71 self.feed_items_per_page = config['feed_items_per_page']
74
72
75 def _changes(self, commit):
73 def _changes(self, commit):
76 diff = commit.diff()
74 diff = commit.diff()
77 diff_processor = DiffProcessor(diff, diff_format='newdiff',
75 diff_processor = DiffProcessor(diff, diff_format='newdiff',
78 diff_limit=self.feed_diff_limit)
76 diff_limit=self.feed_diff_limit)
79 _parsed = diff_processor.prepare(inline_diff=False)
77 _parsed = diff_processor.prepare(inline_diff=False)
80 limited_diff = isinstance(_parsed, LimitedDiffContainer)
78 limited_diff = isinstance(_parsed, LimitedDiffContainer)
81
79
82 return diff_processor, _parsed, limited_diff
80 return diff_processor, _parsed, limited_diff
83
81
84 def _get_title(self, commit):
82 def _get_title(self, commit):
85 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
83 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
86
84
87 def _get_description(self, commit):
85 def _get_description(self, commit):
88 _renderer = self.request.get_partial_renderer(
86 _renderer = self.request.get_partial_renderer(
89 'rhodecode:templates/feed/atom_feed_entry.mako')
87 'rhodecode:templates/feed/atom_feed_entry.mako')
90 diff_processor, parsed_diff, limited_diff = self._changes(commit)
88 diff_processor, parsed_diff, limited_diff = self._changes(commit)
91 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
89 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
92 return _renderer(
90 return _renderer(
93 'body',
91 'body',
94 commit=commit,
92 commit=commit,
95 parsed_diff=filtered_parsed_diff,
93 parsed_diff=filtered_parsed_diff,
96 limited_diff=limited_diff,
94 limited_diff=limited_diff,
97 feed_include_diff=self.feed_include_diff,
95 feed_include_diff=self.feed_include_diff,
98 diff_processor=diff_processor,
96 diff_processor=diff_processor,
99 has_hidden_changes=has_hidden_changes
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 if not getattr(date, "tzinfo", None):
101 if not getattr(date, "tzinfo", None):
104 date.replace(tzinfo=tzinfo)
102 date.replace(tzinfo=tzinfo)
105 return date
103 return date
106
104
107 def _get_commits(self):
105 def _get_commits(self):
108 pre_load = ['author', 'branch', 'date', 'message', 'parents']
106 pre_load = ['author', 'branch', 'date', 'message', 'parents']
109 if self.rhodecode_vcs_repo.is_empty():
107 if self.rhodecode_vcs_repo.is_empty():
110 return []
108 return []
111
109
112 collection = self.rhodecode_vcs_repo.get_commits(
110 collection = self.rhodecode_vcs_repo.get_commits(
113 branch_name=None, show_hidden=False, pre_load=pre_load,
111 branch_name=None, show_hidden=False, pre_load=pre_load,
114 translate_tags=False)
112 translate_tags=False)
115
113
116 return list(collection[-self.feed_items_per_page:])
114 return list(collection[-self.feed_items_per_page:])
117
115
118 def uid(self, repo_id, commit_id):
116 def uid(self, repo_id, commit_id):
119 return '{}:{}'.format(
117 return '{}:{}'.format(
120 md5_safe(repo_id, return_type='str'),
118 md5_safe(repo_id, return_type='str'),
121 md5_safe(commit_id, return_type='str')
119 md5_safe(commit_id, return_type='str')
122 )
120 )
123
121
124 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
122 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
125 @HasRepoPermissionAnyDecorator(
123 @HasRepoPermissionAnyDecorator(
126 'repository.read', 'repository.write', 'repository.admin')
124 'repository.read', 'repository.write', 'repository.admin')
127 def atom(self):
125 def atom(self):
128 """
126 """
129 Produce an atom-1.0 feed via feedgenerator module
127 Produce an atom-1.0 feed via feedgenerator module
130 """
128 """
131 self.load_default_context()
129 self.load_default_context()
132 force_recache = self.get_recache_flag()
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 condition = not (self.path_filter.is_enabled or force_recache)
133 condition = not (self.path_filter.is_enabled or force_recache)
136 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
134 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
137
135
138 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
136 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
139 condition=condition)
137 condition=condition)
140 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
138 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
141 feed = Atom1Feed(
139 feed = Atom1Feed(
142 title=self.title % 'atom',
140 title=self.title % 'atom',
143 link=h.route_url('repo_summary', repo_name=_repo_name),
141 link=h.route_url('repo_summary', repo_name=_repo_name),
144 description=self.description % _repo_name,
142 description=self.description % _repo_name,
145 language=self.language,
143 language=self.language,
146 ttl=self.ttl
144 ttl=self.ttl
147 )
145 )
148
146
149 for commit in reversed(self._get_commits()):
147 for commit in reversed(self._get_commits()):
150 date = self._set_timezone(commit.date)
148 date = self._set_timezone(commit.date)
151 feed.add_item(
149 feed.add_item(
152 unique_id=self.uid(str(repo_id), commit.raw_id),
150 unique_id=self.uid(str(repo_id), commit.raw_id),
153 title=self._get_title(commit),
151 title=self._get_title(commit),
154 author_name=commit.author,
152 author_name=commit.author,
155 description=self._get_description(commit),
153 description=self._get_description(commit),
156 link=h.route_url(
154 link=h.route_url(
157 'repo_commit', repo_name=_repo_name,
155 'repo_commit', repo_name=_repo_name,
158 commit_id=commit.raw_id),
156 commit_id=commit.raw_id),
159 pubdate=date,)
157 pubdate=date,)
160
158
161 return feed.content_type, feed.writeString('utf-8')
159 return feed.content_type, feed.writeString('utf-8')
162
160
163 commit_id = self.db_repo.changeset_cache.get('raw_id')
161 commit_id = self.db_repo.changeset_cache.get('raw_id')
164 content_type, feed = generate_atom_feed(
162 content_type, feed = generate_atom_feed(
165 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
163 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
166
164
167 response = Response(feed)
165 response = Response(feed)
168 response.content_type = content_type
166 response.content_type = content_type
169 return response
167 return response
170
168
171 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
169 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
172 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
174 def rss(self):
172 def rss(self):
175 """
173 """
176 Produce an rss2 feed via feedgenerator module
174 Produce an rss2 feed via feedgenerator module
177 """
175 """
178 self.load_default_context()
176 self.load_default_context()
179 force_recache = self.get_recache_flag()
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 condition = not (self.path_filter.is_enabled or force_recache)
180 condition = not (self.path_filter.is_enabled or force_recache)
183 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
181 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
184
182
185 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
183 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
186 condition=condition)
184 condition=condition)
187 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
185 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
188 feed = Rss201rev2Feed(
186 feed = Rss201rev2Feed(
189 title=self.title % 'rss',
187 title=self.title % 'rss',
190 link=h.route_url('repo_summary', repo_name=_repo_name),
188 link=h.route_url('repo_summary', repo_name=_repo_name),
191 description=self.description % _repo_name,
189 description=self.description % _repo_name,
192 language=self.language,
190 language=self.language,
193 ttl=self.ttl
191 ttl=self.ttl
194 )
192 )
195
193
196 for commit in reversed(self._get_commits()):
194 for commit in reversed(self._get_commits()):
197 date = self._set_timezone(commit.date)
195 date = self._set_timezone(commit.date)
198 feed.add_item(
196 feed.add_item(
199 unique_id=self.uid(str(repo_id), commit.raw_id),
197 unique_id=self.uid(str(repo_id), commit.raw_id),
200 title=self._get_title(commit),
198 title=self._get_title(commit),
201 author_name=commit.author,
199 author_name=commit.author,
202 description=self._get_description(commit),
200 description=self._get_description(commit),
203 link=h.route_url(
201 link=h.route_url(
204 'repo_commit', repo_name=_repo_name,
202 'repo_commit', repo_name=_repo_name,
205 commit_id=commit.raw_id),
203 commit_id=commit.raw_id),
206 pubdate=date,)
204 pubdate=date,)
207 return feed.content_type, feed.writeString('utf-8')
205 return feed.content_type, feed.writeString('utf-8')
208
206
209 commit_id = self.db_repo.changeset_cache.get('raw_id')
207 commit_id = self.db_repo.changeset_cache.get('raw_id')
210 content_type, feed = generate_rss_feed(
208 content_type, feed = generate_rss_feed(
211 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
209 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
212
210
213 response = Response(feed)
211 response = Response(feed)
214 response.content_type = content_type
212 response.content_type = content_type
215 return response
213 return response
@@ -1,1587 +1,1585 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import itertools
19 import itertools
22 import logging
20 import logging
23 import os
21 import os
24 import collections
22 import collections
25 import urllib.request
23 import urllib.request
26 import urllib.parse
24 import urllib.parse
27 import urllib.error
25 import urllib.error
28 import pathlib
26 import pathlib
29
27
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31
29
32 from pyramid.renderers import render
30 from pyramid.renderers import render
33 from pyramid.response import Response
31 from pyramid.response import Response
34
32
35 import rhodecode
33 import rhodecode
36 from rhodecode.apps._base import RepoAppView
34 from rhodecode.apps._base import RepoAppView
37
35
38
36
39 from rhodecode.lib import diffs, helpers as h, rc_cache
37 from rhodecode.lib import diffs, helpers as h, rc_cache
40 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
41 from rhodecode.lib.hash_utils import sha1_safe
39 from rhodecode.lib.hash_utils import sha1_safe
42 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
43 from rhodecode.lib.str_utils import safe_bytes
41 from rhodecode.lib.str_utils import safe_bytes
44 from rhodecode.lib.view_utils import parse_path_ref
42 from rhodecode.lib.view_utils import parse_path_ref
45 from rhodecode.lib.exceptions import NonRelativePathError
43 from rhodecode.lib.exceptions import NonRelativePathError
46 from rhodecode.lib.codeblocks import (
44 from rhodecode.lib.codeblocks import (
47 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
48 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
46 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
49 from rhodecode.lib.type_utils import str2bool
47 from rhodecode.lib.type_utils import str2bool
50 from rhodecode.lib.str_utils import safe_str, safe_int
48 from rhodecode.lib.str_utils import safe_str, safe_int
51 from rhodecode.lib.auth import (
49 from rhodecode.lib.auth import (
52 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
50 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
53 from rhodecode.lib.vcs import path as vcspath
51 from rhodecode.lib.vcs import path as vcspath
54 from rhodecode.lib.vcs.backends.base import EmptyCommit
52 from rhodecode.lib.vcs.backends.base import EmptyCommit
55 from rhodecode.lib.vcs.conf import settings
53 from rhodecode.lib.vcs.conf import settings
56 from rhodecode.lib.vcs.nodes import FileNode
54 from rhodecode.lib.vcs.nodes import FileNode
57 from rhodecode.lib.vcs.exceptions import (
55 from rhodecode.lib.vcs.exceptions import (
58 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
56 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
59 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
57 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
60 NodeDoesNotExistError, CommitError, NodeError)
58 NodeDoesNotExistError, CommitError, NodeError)
61
59
62 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.db import Repository
61 from rhodecode.model.db import Repository
64
62
65 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
66
64
67
65
68 def get_archive_name(db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
66 def get_archive_name(db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
69 # original backward compat name of archive
67 # original backward compat name of archive
70 clean_name = safe_str(db_repo_name.replace('/', '_'))
68 clean_name = safe_str(db_repo_name.replace('/', '_'))
71
69
72 # e.g vcsserver-sub-1-abcfdef-archive-all.zip
70 # e.g vcsserver-sub-1-abcfdef-archive-all.zip
73 # vcsserver-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
71 # vcsserver-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
74
72
75 sub_repo = 'sub-1' if subrepos else 'sub-0'
73 sub_repo = 'sub-1' if subrepos else 'sub-0'
76 commit = commit_sha if with_hash else 'archive'
74 commit = commit_sha if with_hash else 'archive'
77 path_marker = (path_sha if with_hash else '') or 'all'
75 path_marker = (path_sha if with_hash else '') or 'all'
78 archive_name = f'{clean_name}-{sub_repo}-{commit}-{path_marker}{ext}'
76 archive_name = f'{clean_name}-{sub_repo}-{commit}-{path_marker}{ext}'
79
77
80 return archive_name
78 return archive_name
81
79
82
80
83 def get_path_sha(at_path):
81 def get_path_sha(at_path):
84 return safe_str(sha1_safe(at_path)[:8])
82 return safe_str(sha1_safe(at_path)[:8])
85
83
86
84
87 def _get_archive_spec(fname):
85 def _get_archive_spec(fname):
88 log.debug('Detecting archive spec for: `%s`', fname)
86 log.debug('Detecting archive spec for: `%s`', fname)
89
87
90 fileformat = None
88 fileformat = None
91 ext = None
89 ext = None
92 content_type = None
90 content_type = None
93 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
91 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
94
92
95 if fname.endswith(extension):
93 if fname.endswith(extension):
96 fileformat = a_type
94 fileformat = a_type
97 log.debug('archive is of type: %s', fileformat)
95 log.debug('archive is of type: %s', fileformat)
98 ext = extension
96 ext = extension
99 break
97 break
100
98
101 if not fileformat:
99 if not fileformat:
102 raise ValueError()
100 raise ValueError()
103
101
104 # left over part of whole fname is the commit
102 # left over part of whole fname is the commit
105 commit_id = fname[:-len(ext)]
103 commit_id = fname[:-len(ext)]
106
104
107 return commit_id, ext, fileformat, content_type
105 return commit_id, ext, fileformat, content_type
108
106
109
107
110 class RepoFilesView(RepoAppView):
108 class RepoFilesView(RepoAppView):
111
109
112 @staticmethod
110 @staticmethod
113 def adjust_file_path_for_svn(f_path, repo):
111 def adjust_file_path_for_svn(f_path, repo):
114 """
112 """
115 Computes the relative path of `f_path`.
113 Computes the relative path of `f_path`.
116
114
117 This is mainly based on prefix matching of the recognized tags and
115 This is mainly based on prefix matching of the recognized tags and
118 branches in the underlying repository.
116 branches in the underlying repository.
119 """
117 """
120 tags_and_branches = itertools.chain(
118 tags_and_branches = itertools.chain(
121 repo.branches.keys(),
119 repo.branches.keys(),
122 repo.tags.keys())
120 repo.tags.keys())
123 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
121 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
124
122
125 for name in tags_and_branches:
123 for name in tags_and_branches:
126 if f_path.startswith(f'{name}/'):
124 if f_path.startswith(f'{name}/'):
127 f_path = vcspath.relpath(f_path, name)
125 f_path = vcspath.relpath(f_path, name)
128 break
126 break
129 return f_path
127 return f_path
130
128
131 def load_default_context(self):
129 def load_default_context(self):
132 c = self._get_local_tmpl_context(include_app_defaults=True)
130 c = self._get_local_tmpl_context(include_app_defaults=True)
133 c.rhodecode_repo = self.rhodecode_vcs_repo
131 c.rhodecode_repo = self.rhodecode_vcs_repo
134 c.enable_downloads = self.db_repo.enable_downloads
132 c.enable_downloads = self.db_repo.enable_downloads
135 return c
133 return c
136
134
137 def _ensure_not_locked(self, commit_id='tip'):
135 def _ensure_not_locked(self, commit_id='tip'):
138 _ = self.request.translate
136 _ = self.request.translate
139
137
140 repo = self.db_repo
138 repo = self.db_repo
141 if repo.enable_locking and repo.locked[0]:
139 if repo.enable_locking and repo.locked[0]:
142 h.flash(_('This repository has been locked by %s on %s')
140 h.flash(_('This repository has been locked by %s on %s')
143 % (h.person_by_id(repo.locked[0]),
141 % (h.person_by_id(repo.locked[0]),
144 h.format_date(h.time_to_datetime(repo.locked[1]))),
142 h.format_date(h.time_to_datetime(repo.locked[1]))),
145 'warning')
143 'warning')
146 files_url = h.route_path(
144 files_url = h.route_path(
147 'repo_files:default_path',
145 'repo_files:default_path',
148 repo_name=self.db_repo_name, commit_id=commit_id)
146 repo_name=self.db_repo_name, commit_id=commit_id)
149 raise HTTPFound(files_url)
147 raise HTTPFound(files_url)
150
148
151 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
149 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
152 _ = self.request.translate
150 _ = self.request.translate
153
151
154 if not is_head:
152 if not is_head:
155 message = _('Cannot modify file. '
153 message = _('Cannot modify file. '
156 'Given commit `{}` is not head of a branch.').format(commit_id)
154 'Given commit `{}` is not head of a branch.').format(commit_id)
157 h.flash(message, category='warning')
155 h.flash(message, category='warning')
158
156
159 if json_mode:
157 if json_mode:
160 return message
158 return message
161
159
162 files_url = h.route_path(
160 files_url = h.route_path(
163 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
161 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
164 f_path=f_path)
162 f_path=f_path)
165 raise HTTPFound(files_url)
163 raise HTTPFound(files_url)
166
164
167 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
165 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
168 _ = self.request.translate
166 _ = self.request.translate
169
167
170 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
168 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
171 self.db_repo_name, branch_name)
169 self.db_repo_name, branch_name)
172 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
170 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
173 message = _('Branch `{}` changes forbidden by rule {}.').format(
171 message = _('Branch `{}` changes forbidden by rule {}.').format(
174 h.escape(branch_name), h.escape(rule))
172 h.escape(branch_name), h.escape(rule))
175 h.flash(message, 'warning')
173 h.flash(message, 'warning')
176
174
177 if json_mode:
175 if json_mode:
178 return message
176 return message
179
177
180 files_url = h.route_path(
178 files_url = h.route_path(
181 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
179 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
182
180
183 raise HTTPFound(files_url)
181 raise HTTPFound(files_url)
184
182
185 def _get_commit_and_path(self):
183 def _get_commit_and_path(self):
186 default_commit_id = self.db_repo.landing_ref_name
184 default_commit_id = self.db_repo.landing_ref_name
187 default_f_path = '/'
185 default_f_path = '/'
188
186
189 commit_id = self.request.matchdict.get(
187 commit_id = self.request.matchdict.get(
190 'commit_id', default_commit_id)
188 'commit_id', default_commit_id)
191 f_path = self._get_f_path(self.request.matchdict, default_f_path)
189 f_path = self._get_f_path(self.request.matchdict, default_f_path)
192 return commit_id, f_path
190 return commit_id, f_path
193
191
194 def _get_default_encoding(self, c):
192 def _get_default_encoding(self, c):
195 enc_list = getattr(c, 'default_encodings', [])
193 enc_list = getattr(c, 'default_encodings', [])
196 return enc_list[0] if enc_list else 'UTF-8'
194 return enc_list[0] if enc_list else 'UTF-8'
197
195
198 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
196 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
199 """
197 """
200 This is a safe way to get commit. If an error occurs it redirects to
198 This is a safe way to get commit. If an error occurs it redirects to
201 tip with proper message
199 tip with proper message
202
200
203 :param commit_id: id of commit to fetch
201 :param commit_id: id of commit to fetch
204 :param redirect_after: toggle redirection
202 :param redirect_after: toggle redirection
205 """
203 """
206 _ = self.request.translate
204 _ = self.request.translate
207
205
208 try:
206 try:
209 return self.rhodecode_vcs_repo.get_commit(commit_id)
207 return self.rhodecode_vcs_repo.get_commit(commit_id)
210 except EmptyRepositoryError:
208 except EmptyRepositoryError:
211 if not redirect_after:
209 if not redirect_after:
212 return None
210 return None
213
211
214 add_new = upload_new = ""
212 add_new = upload_new = ""
215 if h.HasRepoPermissionAny(
213 if h.HasRepoPermissionAny(
216 'repository.write', 'repository.admin')(self.db_repo_name):
214 'repository.write', 'repository.admin')(self.db_repo_name):
217 _url = h.route_path(
215 _url = h.route_path(
218 'repo_files_add_file',
216 'repo_files_add_file',
219 repo_name=self.db_repo_name, commit_id=0, f_path='')
217 repo_name=self.db_repo_name, commit_id=0, f_path='')
220 add_new = h.link_to(
218 add_new = h.link_to(
221 _('add a new file'), _url, class_="alert-link")
219 _('add a new file'), _url, class_="alert-link")
222
220
223 _url_upld = h.route_path(
221 _url_upld = h.route_path(
224 'repo_files_upload_file',
222 'repo_files_upload_file',
225 repo_name=self.db_repo_name, commit_id=0, f_path='')
223 repo_name=self.db_repo_name, commit_id=0, f_path='')
226 upload_new = h.link_to(
224 upload_new = h.link_to(
227 _('upload a new file'), _url_upld, class_="alert-link")
225 _('upload a new file'), _url_upld, class_="alert-link")
228
226
229 h.flash(h.literal(
227 h.flash(h.literal(
230 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
228 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
231 raise HTTPFound(
229 raise HTTPFound(
232 h.route_path('repo_summary', repo_name=self.db_repo_name))
230 h.route_path('repo_summary', repo_name=self.db_repo_name))
233
231
234 except (CommitDoesNotExistError, LookupError) as e:
232 except (CommitDoesNotExistError, LookupError) as e:
235 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
233 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
236 h.flash(msg, category='error')
234 h.flash(msg, category='error')
237 raise HTTPNotFound()
235 raise HTTPNotFound()
238 except RepositoryError as e:
236 except RepositoryError as e:
239 h.flash(h.escape(safe_str(e)), category='error')
237 h.flash(h.escape(safe_str(e)), category='error')
240 raise HTTPNotFound()
238 raise HTTPNotFound()
241
239
242 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
240 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
243 """
241 """
244 Returns file_node, if error occurs or given path is directory,
242 Returns file_node, if error occurs or given path is directory,
245 it'll redirect to top level path
243 it'll redirect to top level path
246 """
244 """
247 _ = self.request.translate
245 _ = self.request.translate
248
246
249 try:
247 try:
250 file_node = commit_obj.get_node(path, pre_load=pre_load)
248 file_node = commit_obj.get_node(path, pre_load=pre_load)
251 if file_node.is_dir():
249 if file_node.is_dir():
252 raise RepositoryError('The given path is a directory')
250 raise RepositoryError('The given path is a directory')
253 except CommitDoesNotExistError:
251 except CommitDoesNotExistError:
254 log.exception('No such commit exists for this repository')
252 log.exception('No such commit exists for this repository')
255 h.flash(_('No such commit exists for this repository'), category='error')
253 h.flash(_('No such commit exists for this repository'), category='error')
256 raise HTTPNotFound()
254 raise HTTPNotFound()
257 except RepositoryError as e:
255 except RepositoryError as e:
258 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
256 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
259 h.flash(h.escape(safe_str(e)), category='error')
257 h.flash(h.escape(safe_str(e)), category='error')
260 raise HTTPNotFound()
258 raise HTTPNotFound()
261
259
262 return file_node
260 return file_node
263
261
264 def _is_valid_head(self, commit_id, repo, landing_ref):
262 def _is_valid_head(self, commit_id, repo, landing_ref):
265 branch_name = sha_commit_id = ''
263 branch_name = sha_commit_id = ''
266 is_head = False
264 is_head = False
267 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
265 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
268
266
269 for _branch_name, branch_commit_id in repo.branches.items():
267 for _branch_name, branch_commit_id in repo.branches.items():
270 # simple case we pass in branch name, it's a HEAD
268 # simple case we pass in branch name, it's a HEAD
271 if commit_id == _branch_name:
269 if commit_id == _branch_name:
272 is_head = True
270 is_head = True
273 branch_name = _branch_name
271 branch_name = _branch_name
274 sha_commit_id = branch_commit_id
272 sha_commit_id = branch_commit_id
275 break
273 break
276 # case when we pass in full sha commit_id, which is a head
274 # case when we pass in full sha commit_id, which is a head
277 elif commit_id == branch_commit_id:
275 elif commit_id == branch_commit_id:
278 is_head = True
276 is_head = True
279 branch_name = _branch_name
277 branch_name = _branch_name
280 sha_commit_id = branch_commit_id
278 sha_commit_id = branch_commit_id
281 break
279 break
282
280
283 if h.is_svn(repo) and not repo.is_empty():
281 if h.is_svn(repo) and not repo.is_empty():
284 # Note: Subversion only has one head.
282 # Note: Subversion only has one head.
285 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
283 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
286 is_head = True
284 is_head = True
287 return branch_name, sha_commit_id, is_head
285 return branch_name, sha_commit_id, is_head
288
286
289 # checked branches, means we only need to try to get the branch/commit_sha
287 # checked branches, means we only need to try to get the branch/commit_sha
290 if repo.is_empty():
288 if repo.is_empty():
291 is_head = True
289 is_head = True
292 branch_name = landing_ref
290 branch_name = landing_ref
293 sha_commit_id = EmptyCommit().raw_id
291 sha_commit_id = EmptyCommit().raw_id
294 else:
292 else:
295 commit = repo.get_commit(commit_id=commit_id)
293 commit = repo.get_commit(commit_id=commit_id)
296 if commit:
294 if commit:
297 branch_name = commit.branch
295 branch_name = commit.branch
298 sha_commit_id = commit.raw_id
296 sha_commit_id = commit.raw_id
299
297
300 return branch_name, sha_commit_id, is_head
298 return branch_name, sha_commit_id, is_head
301
299
302 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
300 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
303
301
304 repo_id = self.db_repo.repo_id
302 repo_id = self.db_repo.repo_id
305 force_recache = self.get_recache_flag()
303 force_recache = self.get_recache_flag()
306
304
307 cache_seconds = safe_int(
305 cache_seconds = safe_int(
308 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
306 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
309 cache_on = not force_recache and cache_seconds > 0
307 cache_on = not force_recache and cache_seconds > 0
310 log.debug(
308 log.debug(
311 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
309 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
312 'with caching: %s[TTL: %ss]' % (
310 'with caching: %s[TTL: %ss]' % (
313 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
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 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
314 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
317
315
318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
319 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
317 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
320 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
318 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
321 ver, _repo_id, _commit_id, _f_path)
319 ver, _repo_id, _commit_id, _f_path)
322
320
323 c.full_load = _full_load
321 c.full_load = _full_load
324 return render(
322 return render(
325 'rhodecode:templates/files/files_browser_tree.mako',
323 'rhodecode:templates/files/files_browser_tree.mako',
326 self._get_template_context(c), self.request, _at_rev)
324 self._get_template_context(c), self.request, _at_rev)
327
325
328 return compute_file_tree(
326 return compute_file_tree(
329 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
327 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
330 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
328 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
331
329
332 def create_pure_path(self, *parts):
330 def create_pure_path(self, *parts):
333 # Split paths and sanitize them, removing any ../ etc
331 # Split paths and sanitize them, removing any ../ etc
334 sanitized_path = [
332 sanitized_path = [
335 x for x in pathlib.PurePath(*parts).parts
333 x for x in pathlib.PurePath(*parts).parts
336 if x not in ['.', '..']]
334 if x not in ['.', '..']]
337
335
338 pure_path = pathlib.PurePath(*sanitized_path)
336 pure_path = pathlib.PurePath(*sanitized_path)
339 return pure_path
337 return pure_path
340
338
341 def _is_lf_enabled(self, target_repo):
339 def _is_lf_enabled(self, target_repo):
342 lf_enabled = False
340 lf_enabled = False
343
341
344 lf_key_for_vcs_map = {
342 lf_key_for_vcs_map = {
345 'hg': 'extensions_largefiles',
343 'hg': 'extensions_largefiles',
346 'git': 'vcs_git_lfs_enabled'
344 'git': 'vcs_git_lfs_enabled'
347 }
345 }
348
346
349 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
347 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
350
348
351 if lf_key_for_vcs:
349 if lf_key_for_vcs:
352 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
350 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
353
351
354 return lf_enabled
352 return lf_enabled
355
353
356 @LoginRequired()
354 @LoginRequired()
357 @HasRepoPermissionAnyDecorator(
355 @HasRepoPermissionAnyDecorator(
358 'repository.read', 'repository.write', 'repository.admin')
356 'repository.read', 'repository.write', 'repository.admin')
359 def repo_archivefile(self):
357 def repo_archivefile(self):
360 # archive cache config
358 # archive cache config
361 from rhodecode import CONFIG
359 from rhodecode import CONFIG
362 _ = self.request.translate
360 _ = self.request.translate
363 self.load_default_context()
361 self.load_default_context()
364 default_at_path = '/'
362 default_at_path = '/'
365 fname = self.request.matchdict['fname']
363 fname = self.request.matchdict['fname']
366 subrepos = self.request.GET.get('subrepos') == 'true'
364 subrepos = self.request.GET.get('subrepos') == 'true'
367 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
365 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
368 at_path = self.request.GET.get('at_path') or default_at_path
366 at_path = self.request.GET.get('at_path') or default_at_path
369
367
370 if not self.db_repo.enable_downloads:
368 if not self.db_repo.enable_downloads:
371 return Response(_('Downloads disabled'))
369 return Response(_('Downloads disabled'))
372
370
373 try:
371 try:
374 commit_id, ext, fileformat, content_type = \
372 commit_id, ext, fileformat, content_type = \
375 _get_archive_spec(fname)
373 _get_archive_spec(fname)
376 except ValueError:
374 except ValueError:
377 return Response(_('Unknown archive type for: `{}`').format(
375 return Response(_('Unknown archive type for: `{}`').format(
378 h.escape(fname)))
376 h.escape(fname)))
379
377
380 try:
378 try:
381 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
379 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
382 except CommitDoesNotExistError:
380 except CommitDoesNotExistError:
383 return Response(_('Unknown commit_id {}').format(
381 return Response(_('Unknown commit_id {}').format(
384 h.escape(commit_id)))
382 h.escape(commit_id)))
385 except EmptyRepositoryError:
383 except EmptyRepositoryError:
386 return Response(_('Empty repository'))
384 return Response(_('Empty repository'))
387
385
388 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
386 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
389 if commit_id != commit.raw_id:
387 if commit_id != commit.raw_id:
390 fname='{}{}'.format(commit.raw_id, ext)
388 fname=f'{commit.raw_id}{ext}'
391 raise HTTPFound(self.request.current_route_path(fname=fname))
389 raise HTTPFound(self.request.current_route_path(fname=fname))
392
390
393 try:
391 try:
394 at_path = commit.get_node(at_path).path or default_at_path
392 at_path = commit.get_node(at_path).path or default_at_path
395 except Exception:
393 except Exception:
396 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
394 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
397
395
398 path_sha = get_path_sha(at_path)
396 path_sha = get_path_sha(at_path)
399
397
400 # used for cache etc, consistent unique archive name
398 # used for cache etc, consistent unique archive name
401 archive_name_key = get_archive_name(
399 archive_name_key = get_archive_name(
402 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
400 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 path_sha=path_sha, with_hash=True)
401 path_sha=path_sha, with_hash=True)
404
402
405 if not with_hash:
403 if not with_hash:
406 path_sha = ''
404 path_sha = ''
407
405
408 # what end client gets served
406 # what end client gets served
409 response_archive_name = get_archive_name(
407 response_archive_name = get_archive_name(
410 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
408 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
411 path_sha=path_sha, with_hash=with_hash)
409 path_sha=path_sha, with_hash=with_hash)
412
410
413 # remove extension from our archive directory name
411 # remove extension from our archive directory name
414 archive_dir_name = response_archive_name[:-len(ext)]
412 archive_dir_name = response_archive_name[:-len(ext)]
415
413
416 archive_cache_disable = self.request.GET.get('no_cache')
414 archive_cache_disable = self.request.GET.get('no_cache')
417
415
418 d_cache = get_archival_cache_store(config=CONFIG)
416 d_cache = get_archival_cache_store(config=CONFIG)
419 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
417 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
420 d_cache_conf = get_archival_config(config=CONFIG)
418 d_cache_conf = get_archival_config(config=CONFIG)
421
419
422 reentrant_lock_key = archive_name_key + '.lock'
420 reentrant_lock_key = archive_name_key + '.lock'
423 with ReentrantLock(d_cache, reentrant_lock_key):
421 with ReentrantLock(d_cache, reentrant_lock_key):
424 # This is also a cache key
422 # This is also a cache key
425 use_cached_archive = False
423 use_cached_archive = False
426 if archive_name_key in d_cache and not archive_cache_disable:
424 if archive_name_key in d_cache and not archive_cache_disable:
427 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
425 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
428 use_cached_archive = True
426 use_cached_archive = True
429 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
427 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
430 archive_name_key, tag, reader.name)
428 archive_name_key, tag, reader.name)
431 else:
429 else:
432 reader = None
430 reader = None
433 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
431 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
434
432
435 # generate new archive, as previous was not found in the cache
433 # generate new archive, as previous was not found in the cache
436 if not reader:
434 if not reader:
437 # first remove expired items, before generating a new one :)
435 # first remove expired items, before generating a new one :)
438 # we di this manually because automatic eviction is disabled
436 # we di this manually because automatic eviction is disabled
439 d_cache.cull(retry=True)
437 d_cache.cull(retry=True)
440
438
441 try:
439 try:
442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
440 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 kind=fileformat, subrepos=subrepos,
441 kind=fileformat, subrepos=subrepos,
444 archive_at_path=at_path, cache_config=d_cache_conf)
442 archive_at_path=at_path, cache_config=d_cache_conf)
445 except ImproperArchiveTypeError:
443 except ImproperArchiveTypeError:
446 return _('Unknown archive type')
444 return _('Unknown archive type')
447
445
448 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
446 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
449
447
450 if not reader:
448 if not reader:
451 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
449 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
452
450
453 def archive_iterator(_reader):
451 def archive_iterator(_reader):
454 while 1:
452 while 1:
455 data = _reader.read(1024)
453 data = _reader.read(1024)
456 if not data:
454 if not data:
457 break
455 break
458 yield data
456 yield data
459
457
460 response = Response(app_iter=archive_iterator(reader))
458 response = Response(app_iter=archive_iterator(reader))
461 response.content_disposition = f'attachment; filename={response_archive_name}'
459 response.content_disposition = f'attachment; filename={response_archive_name}'
462 response.content_type = str(content_type)
460 response.content_type = str(content_type)
463
461
464 try:
462 try:
465 return response
463 return response
466 finally:
464 finally:
467 # store download action
465 # store download action
468 audit_logger.store_web(
466 audit_logger.store_web(
469 'repo.archive.download', action_data={
467 'repo.archive.download', action_data={
470 'user_agent': self.request.user_agent,
468 'user_agent': self.request.user_agent,
471 'archive_name': archive_name_key,
469 'archive_name': archive_name_key,
472 'archive_spec': fname,
470 'archive_spec': fname,
473 'archive_cached': use_cached_archive},
471 'archive_cached': use_cached_archive},
474 user=self._rhodecode_user,
472 user=self._rhodecode_user,
475 repo=self.db_repo,
473 repo=self.db_repo,
476 commit=True
474 commit=True
477 )
475 )
478
476
479 def _get_file_node(self, commit_id, f_path):
477 def _get_file_node(self, commit_id, f_path):
480 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
478 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
481 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
479 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
482 try:
480 try:
483 node = commit.get_node(f_path)
481 node = commit.get_node(f_path)
484 if node.is_dir():
482 if node.is_dir():
485 raise NodeError(f'{node} path is a {type(node)} not a file')
483 raise NodeError(f'{node} path is a {type(node)} not a file')
486 except NodeDoesNotExistError:
484 except NodeDoesNotExistError:
487 commit = EmptyCommit(
485 commit = EmptyCommit(
488 commit_id=commit_id,
486 commit_id=commit_id,
489 idx=commit.idx,
487 idx=commit.idx,
490 repo=commit.repository,
488 repo=commit.repository,
491 alias=commit.repository.alias,
489 alias=commit.repository.alias,
492 message=commit.message,
490 message=commit.message,
493 author=commit.author,
491 author=commit.author,
494 date=commit.date)
492 date=commit.date)
495 node = FileNode(safe_bytes(f_path), b'', commit=commit)
493 node = FileNode(safe_bytes(f_path), b'', commit=commit)
496 else:
494 else:
497 commit = EmptyCommit(
495 commit = EmptyCommit(
498 repo=self.rhodecode_vcs_repo,
496 repo=self.rhodecode_vcs_repo,
499 alias=self.rhodecode_vcs_repo.alias)
497 alias=self.rhodecode_vcs_repo.alias)
500 node = FileNode(safe_bytes(f_path), b'', commit=commit)
498 node = FileNode(safe_bytes(f_path), b'', commit=commit)
501 return node
499 return node
502
500
503 @LoginRequired()
501 @LoginRequired()
504 @HasRepoPermissionAnyDecorator(
502 @HasRepoPermissionAnyDecorator(
505 'repository.read', 'repository.write', 'repository.admin')
503 'repository.read', 'repository.write', 'repository.admin')
506 def repo_files_diff(self):
504 def repo_files_diff(self):
507 c = self.load_default_context()
505 c = self.load_default_context()
508 f_path = self._get_f_path(self.request.matchdict)
506 f_path = self._get_f_path(self.request.matchdict)
509 diff1 = self.request.GET.get('diff1', '')
507 diff1 = self.request.GET.get('diff1', '')
510 diff2 = self.request.GET.get('diff2', '')
508 diff2 = self.request.GET.get('diff2', '')
511
509
512 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
510 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
513
511
514 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
512 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
515 line_context = self.request.GET.get('context', 3)
513 line_context = self.request.GET.get('context', 3)
516
514
517 if not any((diff1, diff2)):
515 if not any((diff1, diff2)):
518 h.flash(
516 h.flash(
519 'Need query parameter "diff1" or "diff2" to generate a diff.',
517 'Need query parameter "diff1" or "diff2" to generate a diff.',
520 category='error')
518 category='error')
521 raise HTTPBadRequest()
519 raise HTTPBadRequest()
522
520
523 c.action = self.request.GET.get('diff')
521 c.action = self.request.GET.get('diff')
524 if c.action not in ['download', 'raw']:
522 if c.action not in ['download', 'raw']:
525 compare_url = h.route_path(
523 compare_url = h.route_path(
526 'repo_compare',
524 'repo_compare',
527 repo_name=self.db_repo_name,
525 repo_name=self.db_repo_name,
528 source_ref_type='rev',
526 source_ref_type='rev',
529 source_ref=diff1,
527 source_ref=diff1,
530 target_repo=self.db_repo_name,
528 target_repo=self.db_repo_name,
531 target_ref_type='rev',
529 target_ref_type='rev',
532 target_ref=diff2,
530 target_ref=diff2,
533 _query=dict(f_path=f_path))
531 _query=dict(f_path=f_path))
534 # redirect to new view if we render diff
532 # redirect to new view if we render diff
535 raise HTTPFound(compare_url)
533 raise HTTPFound(compare_url)
536
534
537 try:
535 try:
538 node1 = self._get_file_node(diff1, path1)
536 node1 = self._get_file_node(diff1, path1)
539 node2 = self._get_file_node(diff2, f_path)
537 node2 = self._get_file_node(diff2, f_path)
540 except (RepositoryError, NodeError):
538 except (RepositoryError, NodeError):
541 log.exception("Exception while trying to get node from repository")
539 log.exception("Exception while trying to get node from repository")
542 raise HTTPFound(
540 raise HTTPFound(
543 h.route_path('repo_files', repo_name=self.db_repo_name,
541 h.route_path('repo_files', repo_name=self.db_repo_name,
544 commit_id='tip', f_path=f_path))
542 commit_id='tip', f_path=f_path))
545
543
546 if all(isinstance(node.commit, EmptyCommit)
544 if all(isinstance(node.commit, EmptyCommit)
547 for node in (node1, node2)):
545 for node in (node1, node2)):
548 raise HTTPNotFound()
546 raise HTTPNotFound()
549
547
550 c.commit_1 = node1.commit
548 c.commit_1 = node1.commit
551 c.commit_2 = node2.commit
549 c.commit_2 = node2.commit
552
550
553 if c.action == 'download':
551 if c.action == 'download':
554 _diff = diffs.get_gitdiff(node1, node2,
552 _diff = diffs.get_gitdiff(node1, node2,
555 ignore_whitespace=ignore_whitespace,
553 ignore_whitespace=ignore_whitespace,
556 context=line_context)
554 context=line_context)
557 # NOTE: this was using diff_format='gitdiff'
555 # NOTE: this was using diff_format='gitdiff'
558 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
556 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
559
557
560 response = Response(self.path_filter.get_raw_patch(diff))
558 response = Response(self.path_filter.get_raw_patch(diff))
561 response.content_type = 'text/plain'
559 response.content_type = 'text/plain'
562 response.content_disposition = (
560 response.content_disposition = (
563 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
561 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
564 )
562 )
565 charset = self._get_default_encoding(c)
563 charset = self._get_default_encoding(c)
566 if charset:
564 if charset:
567 response.charset = charset
565 response.charset = charset
568 return response
566 return response
569
567
570 elif c.action == 'raw':
568 elif c.action == 'raw':
571 _diff = diffs.get_gitdiff(node1, node2,
569 _diff = diffs.get_gitdiff(node1, node2,
572 ignore_whitespace=ignore_whitespace,
570 ignore_whitespace=ignore_whitespace,
573 context=line_context)
571 context=line_context)
574 # NOTE: this was using diff_format='gitdiff'
572 # NOTE: this was using diff_format='gitdiff'
575 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
573 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
576
574
577 response = Response(self.path_filter.get_raw_patch(diff))
575 response = Response(self.path_filter.get_raw_patch(diff))
578 response.content_type = 'text/plain'
576 response.content_type = 'text/plain'
579 charset = self._get_default_encoding(c)
577 charset = self._get_default_encoding(c)
580 if charset:
578 if charset:
581 response.charset = charset
579 response.charset = charset
582 return response
580 return response
583
581
584 # in case we ever end up here
582 # in case we ever end up here
585 raise HTTPNotFound()
583 raise HTTPNotFound()
586
584
587 @LoginRequired()
585 @LoginRequired()
588 @HasRepoPermissionAnyDecorator(
586 @HasRepoPermissionAnyDecorator(
589 'repository.read', 'repository.write', 'repository.admin')
587 'repository.read', 'repository.write', 'repository.admin')
590 def repo_files_diff_2way_redirect(self):
588 def repo_files_diff_2way_redirect(self):
591 """
589 """
592 Kept only to make OLD links work
590 Kept only to make OLD links work
593 """
591 """
594 f_path = self._get_f_path_unchecked(self.request.matchdict)
592 f_path = self._get_f_path_unchecked(self.request.matchdict)
595 diff1 = self.request.GET.get('diff1', '')
593 diff1 = self.request.GET.get('diff1', '')
596 diff2 = self.request.GET.get('diff2', '')
594 diff2 = self.request.GET.get('diff2', '')
597
595
598 if not any((diff1, diff2)):
596 if not any((diff1, diff2)):
599 h.flash(
597 h.flash(
600 'Need query parameter "diff1" or "diff2" to generate a diff.',
598 'Need query parameter "diff1" or "diff2" to generate a diff.',
601 category='error')
599 category='error')
602 raise HTTPBadRequest()
600 raise HTTPBadRequest()
603
601
604 compare_url = h.route_path(
602 compare_url = h.route_path(
605 'repo_compare',
603 'repo_compare',
606 repo_name=self.db_repo_name,
604 repo_name=self.db_repo_name,
607 source_ref_type='rev',
605 source_ref_type='rev',
608 source_ref=diff1,
606 source_ref=diff1,
609 target_ref_type='rev',
607 target_ref_type='rev',
610 target_ref=diff2,
608 target_ref=diff2,
611 _query=dict(f_path=f_path, diffmode='sideside',
609 _query=dict(f_path=f_path, diffmode='sideside',
612 target_repo=self.db_repo_name,))
610 target_repo=self.db_repo_name,))
613 raise HTTPFound(compare_url)
611 raise HTTPFound(compare_url)
614
612
615 @LoginRequired()
613 @LoginRequired()
616 def repo_files_default_commit_redirect(self):
614 def repo_files_default_commit_redirect(self):
617 """
615 """
618 Special page that redirects to the landing page of files based on the default
616 Special page that redirects to the landing page of files based on the default
619 commit for repository
617 commit for repository
620 """
618 """
621 c = self.load_default_context()
619 c = self.load_default_context()
622 ref_name = c.rhodecode_db_repo.landing_ref_name
620 ref_name = c.rhodecode_db_repo.landing_ref_name
623 landing_url = h.repo_files_by_ref_url(
621 landing_url = h.repo_files_by_ref_url(
624 c.rhodecode_db_repo.repo_name,
622 c.rhodecode_db_repo.repo_name,
625 c.rhodecode_db_repo.repo_type,
623 c.rhodecode_db_repo.repo_type,
626 f_path='',
624 f_path='',
627 ref_name=ref_name,
625 ref_name=ref_name,
628 commit_id='tip',
626 commit_id='tip',
629 query=dict(at=ref_name)
627 query=dict(at=ref_name)
630 )
628 )
631
629
632 raise HTTPFound(landing_url)
630 raise HTTPFound(landing_url)
633
631
634 @LoginRequired()
632 @LoginRequired()
635 @HasRepoPermissionAnyDecorator(
633 @HasRepoPermissionAnyDecorator(
636 'repository.read', 'repository.write', 'repository.admin')
634 'repository.read', 'repository.write', 'repository.admin')
637 def repo_files(self):
635 def repo_files(self):
638 c = self.load_default_context()
636 c = self.load_default_context()
639
637
640 view_name = getattr(self.request.matched_route, 'name', None)
638 view_name = getattr(self.request.matched_route, 'name', None)
641
639
642 c.annotate = view_name == 'repo_files:annotated'
640 c.annotate = view_name == 'repo_files:annotated'
643 # default is false, but .rst/.md files later are auto rendered, we can
641 # default is false, but .rst/.md files later are auto rendered, we can
644 # overwrite auto rendering by setting this GET flag
642 # overwrite auto rendering by setting this GET flag
645 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
643 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
646
644
647 commit_id, f_path = self._get_commit_and_path()
645 commit_id, f_path = self._get_commit_and_path()
648
646
649 c.commit = self._get_commit_or_redirect(commit_id)
647 c.commit = self._get_commit_or_redirect(commit_id)
650 c.branch = self.request.GET.get('branch', None)
648 c.branch = self.request.GET.get('branch', None)
651 c.f_path = f_path
649 c.f_path = f_path
652 at_rev = self.request.GET.get('at')
650 at_rev = self.request.GET.get('at')
653
651
654 # prev link
652 # prev link
655 try:
653 try:
656 prev_commit = c.commit.prev(c.branch)
654 prev_commit = c.commit.prev(c.branch)
657 c.prev_commit = prev_commit
655 c.prev_commit = prev_commit
658 c.url_prev = h.route_path(
656 c.url_prev = h.route_path(
659 'repo_files', repo_name=self.db_repo_name,
657 'repo_files', repo_name=self.db_repo_name,
660 commit_id=prev_commit.raw_id, f_path=f_path)
658 commit_id=prev_commit.raw_id, f_path=f_path)
661 if c.branch:
659 if c.branch:
662 c.url_prev += '?branch=%s' % c.branch
660 c.url_prev += '?branch=%s' % c.branch
663 except (CommitDoesNotExistError, VCSError):
661 except (CommitDoesNotExistError, VCSError):
664 c.url_prev = '#'
662 c.url_prev = '#'
665 c.prev_commit = EmptyCommit()
663 c.prev_commit = EmptyCommit()
666
664
667 # next link
665 # next link
668 try:
666 try:
669 next_commit = c.commit.next(c.branch)
667 next_commit = c.commit.next(c.branch)
670 c.next_commit = next_commit
668 c.next_commit = next_commit
671 c.url_next = h.route_path(
669 c.url_next = h.route_path(
672 'repo_files', repo_name=self.db_repo_name,
670 'repo_files', repo_name=self.db_repo_name,
673 commit_id=next_commit.raw_id, f_path=f_path)
671 commit_id=next_commit.raw_id, f_path=f_path)
674 if c.branch:
672 if c.branch:
675 c.url_next += '?branch=%s' % c.branch
673 c.url_next += '?branch=%s' % c.branch
676 except (CommitDoesNotExistError, VCSError):
674 except (CommitDoesNotExistError, VCSError):
677 c.url_next = '#'
675 c.url_next = '#'
678 c.next_commit = EmptyCommit()
676 c.next_commit = EmptyCommit()
679
677
680 # files or dirs
678 # files or dirs
681 try:
679 try:
682 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
680 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
683
681
684 c.file_author = True
682 c.file_author = True
685 c.file_tree = ''
683 c.file_tree = ''
686
684
687 # load file content
685 # load file content
688 if c.file.is_file():
686 if c.file.is_file():
689 c.lf_node = {}
687 c.lf_node = {}
690
688
691 has_lf_enabled = self._is_lf_enabled(self.db_repo)
689 has_lf_enabled = self._is_lf_enabled(self.db_repo)
692 if has_lf_enabled:
690 if has_lf_enabled:
693 c.lf_node = c.file.get_largefile_node()
691 c.lf_node = c.file.get_largefile_node()
694
692
695 c.file_source_page = 'true'
693 c.file_source_page = 'true'
696 c.file_last_commit = c.file.last_commit
694 c.file_last_commit = c.file.last_commit
697
695
698 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
696 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
699
697
700 if not (c.file_size_too_big or c.file.is_binary):
698 if not (c.file_size_too_big or c.file.is_binary):
701 if c.annotate: # annotation has precedence over renderer
699 if c.annotate: # annotation has precedence over renderer
702 c.annotated_lines = filenode_as_annotated_lines_tokens(
700 c.annotated_lines = filenode_as_annotated_lines_tokens(
703 c.file
701 c.file
704 )
702 )
705 else:
703 else:
706 c.renderer = (
704 c.renderer = (
707 c.renderer and h.renderer_from_filename(c.file.path)
705 c.renderer and h.renderer_from_filename(c.file.path)
708 )
706 )
709 if not c.renderer:
707 if not c.renderer:
710 c.lines = filenode_as_lines_tokens(c.file)
708 c.lines = filenode_as_lines_tokens(c.file)
711
709
712 _branch_name, _sha_commit_id, is_head = \
710 _branch_name, _sha_commit_id, is_head = \
713 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
711 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
714 landing_ref=self.db_repo.landing_ref_name)
712 landing_ref=self.db_repo.landing_ref_name)
715 c.on_branch_head = is_head
713 c.on_branch_head = is_head
716
714
717 branch = c.commit.branch if (
715 branch = c.commit.branch if (
718 c.commit.branch and '/' not in c.commit.branch) else None
716 c.commit.branch and '/' not in c.commit.branch) else None
719 c.branch_or_raw_id = branch or c.commit.raw_id
717 c.branch_or_raw_id = branch or c.commit.raw_id
720 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
718 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
721
719
722 author = c.file_last_commit.author
720 author = c.file_last_commit.author
723 c.authors = [[
721 c.authors = [[
724 h.email(author),
722 h.email(author),
725 h.person(author, 'username_or_name_or_email'),
723 h.person(author, 'username_or_name_or_email'),
726 1
724 1
727 ]]
725 ]]
728
726
729 else: # load tree content at path
727 else: # load tree content at path
730 c.file_source_page = 'false'
728 c.file_source_page = 'false'
731 c.authors = []
729 c.authors = []
732 # this loads a simple tree without metadata to speed things up
730 # this loads a simple tree without metadata to speed things up
733 # later via ajax we call repo_nodetree_full and fetch whole
731 # later via ajax we call repo_nodetree_full and fetch whole
734 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
732 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
735
733
736 c.readme_data, c.readme_file = \
734 c.readme_data, c.readme_file = \
737 self._get_readme_data(self.db_repo, c.visual.default_renderer,
735 self._get_readme_data(self.db_repo, c.visual.default_renderer,
738 c.commit.raw_id, f_path)
736 c.commit.raw_id, f_path)
739
737
740 except RepositoryError as e:
738 except RepositoryError as e:
741 h.flash(h.escape(safe_str(e)), category='error')
739 h.flash(h.escape(safe_str(e)), category='error')
742 raise HTTPNotFound()
740 raise HTTPNotFound()
743
741
744 if self.request.environ.get('HTTP_X_PJAX'):
742 if self.request.environ.get('HTTP_X_PJAX'):
745 html = render('rhodecode:templates/files/files_pjax.mako',
743 html = render('rhodecode:templates/files/files_pjax.mako',
746 self._get_template_context(c), self.request)
744 self._get_template_context(c), self.request)
747 else:
745 else:
748 html = render('rhodecode:templates/files/files.mako',
746 html = render('rhodecode:templates/files/files.mako',
749 self._get_template_context(c), self.request)
747 self._get_template_context(c), self.request)
750 return Response(html)
748 return Response(html)
751
749
752 @HasRepoPermissionAnyDecorator(
750 @HasRepoPermissionAnyDecorator(
753 'repository.read', 'repository.write', 'repository.admin')
751 'repository.read', 'repository.write', 'repository.admin')
754 def repo_files_annotated_previous(self):
752 def repo_files_annotated_previous(self):
755 self.load_default_context()
753 self.load_default_context()
756
754
757 commit_id, f_path = self._get_commit_and_path()
755 commit_id, f_path = self._get_commit_and_path()
758 commit = self._get_commit_or_redirect(commit_id)
756 commit = self._get_commit_or_redirect(commit_id)
759 prev_commit_id = commit.raw_id
757 prev_commit_id = commit.raw_id
760 line_anchor = self.request.GET.get('line_anchor')
758 line_anchor = self.request.GET.get('line_anchor')
761 is_file = False
759 is_file = False
762 try:
760 try:
763 _file = commit.get_node(f_path)
761 _file = commit.get_node(f_path)
764 is_file = _file.is_file()
762 is_file = _file.is_file()
765 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
763 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
766 pass
764 pass
767
765
768 if is_file:
766 if is_file:
769 history = commit.get_path_history(f_path)
767 history = commit.get_path_history(f_path)
770 prev_commit_id = history[1].raw_id \
768 prev_commit_id = history[1].raw_id \
771 if len(history) > 1 else prev_commit_id
769 if len(history) > 1 else prev_commit_id
772 prev_url = h.route_path(
770 prev_url = h.route_path(
773 'repo_files:annotated', repo_name=self.db_repo_name,
771 'repo_files:annotated', repo_name=self.db_repo_name,
774 commit_id=prev_commit_id, f_path=f_path,
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 raise HTTPFound(prev_url)
775 raise HTTPFound(prev_url)
778
776
779 @LoginRequired()
777 @LoginRequired()
780 @HasRepoPermissionAnyDecorator(
778 @HasRepoPermissionAnyDecorator(
781 'repository.read', 'repository.write', 'repository.admin')
779 'repository.read', 'repository.write', 'repository.admin')
782 def repo_nodetree_full(self):
780 def repo_nodetree_full(self):
783 """
781 """
784 Returns rendered html of file tree that contains commit date,
782 Returns rendered html of file tree that contains commit date,
785 author, commit_id for the specified combination of
783 author, commit_id for the specified combination of
786 repo, commit_id and file path
784 repo, commit_id and file path
787 """
785 """
788 c = self.load_default_context()
786 c = self.load_default_context()
789
787
790 commit_id, f_path = self._get_commit_and_path()
788 commit_id, f_path = self._get_commit_and_path()
791 commit = self._get_commit_or_redirect(commit_id)
789 commit = self._get_commit_or_redirect(commit_id)
792 try:
790 try:
793 dir_node = commit.get_node(f_path)
791 dir_node = commit.get_node(f_path)
794 except RepositoryError as e:
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 if dir_node.is_file():
795 if dir_node.is_file():
798 return Response('')
796 return Response('')
799
797
800 c.file = dir_node
798 c.file = dir_node
801 c.commit = commit
799 c.commit = commit
802 at_rev = self.request.GET.get('at')
800 at_rev = self.request.GET.get('at')
803
801
804 html = self._get_tree_at_commit(
802 html = self._get_tree_at_commit(
805 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
803 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
806
804
807 return Response(html)
805 return Response(html)
808
806
809 def _get_attachement_headers(self, f_path):
807 def _get_attachement_headers(self, f_path):
810 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
808 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
811 safe_path = f_name.replace('"', '\\"')
809 safe_path = f_name.replace('"', '\\"')
812 encoded_path = urllib.parse.quote(f_name)
810 encoded_path = urllib.parse.quote(f_name)
813
811
814 return "attachment; " \
812 return "attachment; " \
815 "filename=\"{}\"; " \
813 "filename=\"{}\"; " \
816 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
814 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
817
815
818 @LoginRequired()
816 @LoginRequired()
819 @HasRepoPermissionAnyDecorator(
817 @HasRepoPermissionAnyDecorator(
820 'repository.read', 'repository.write', 'repository.admin')
818 'repository.read', 'repository.write', 'repository.admin')
821 def repo_file_raw(self):
819 def repo_file_raw(self):
822 """
820 """
823 Action for show as raw, some mimetypes are "rendered",
821 Action for show as raw, some mimetypes are "rendered",
824 those include images, icons.
822 those include images, icons.
825 """
823 """
826 c = self.load_default_context()
824 c = self.load_default_context()
827
825
828 commit_id, f_path = self._get_commit_and_path()
826 commit_id, f_path = self._get_commit_and_path()
829 commit = self._get_commit_or_redirect(commit_id)
827 commit = self._get_commit_or_redirect(commit_id)
830 file_node = self._get_filenode_or_redirect(commit, f_path)
828 file_node = self._get_filenode_or_redirect(commit, f_path)
831
829
832 raw_mimetype_mapping = {
830 raw_mimetype_mapping = {
833 # map original mimetype to a mimetype used for "show as raw"
831 # map original mimetype to a mimetype used for "show as raw"
834 # you can also provide a content-disposition to override the
832 # you can also provide a content-disposition to override the
835 # default "attachment" disposition.
833 # default "attachment" disposition.
836 # orig_type: (new_type, new_dispo)
834 # orig_type: (new_type, new_dispo)
837
835
838 # show images inline:
836 # show images inline:
839 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
837 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
840 # for example render an SVG with javascript inside or even render
838 # for example render an SVG with javascript inside or even render
841 # HTML.
839 # HTML.
842 'image/x-icon': ('image/x-icon', 'inline'),
840 'image/x-icon': ('image/x-icon', 'inline'),
843 'image/png': ('image/png', 'inline'),
841 'image/png': ('image/png', 'inline'),
844 'image/gif': ('image/gif', 'inline'),
842 'image/gif': ('image/gif', 'inline'),
845 'image/jpeg': ('image/jpeg', 'inline'),
843 'image/jpeg': ('image/jpeg', 'inline'),
846 'application/pdf': ('application/pdf', 'inline'),
844 'application/pdf': ('application/pdf', 'inline'),
847 }
845 }
848
846
849 mimetype = file_node.mimetype
847 mimetype = file_node.mimetype
850 try:
848 try:
851 mimetype, disposition = raw_mimetype_mapping[mimetype]
849 mimetype, disposition = raw_mimetype_mapping[mimetype]
852 except KeyError:
850 except KeyError:
853 # we don't know anything special about this, handle it safely
851 # we don't know anything special about this, handle it safely
854 if file_node.is_binary:
852 if file_node.is_binary:
855 # do same as download raw for binary files
853 # do same as download raw for binary files
856 mimetype, disposition = 'application/octet-stream', 'attachment'
854 mimetype, disposition = 'application/octet-stream', 'attachment'
857 else:
855 else:
858 # do not just use the original mimetype, but force text/plain,
856 # do not just use the original mimetype, but force text/plain,
859 # otherwise it would serve text/html and that might be unsafe.
857 # otherwise it would serve text/html and that might be unsafe.
860 # Note: underlying vcs library fakes text/plain mimetype if the
858 # Note: underlying vcs library fakes text/plain mimetype if the
861 # mimetype can not be determined and it thinks it is not
859 # mimetype can not be determined and it thinks it is not
862 # binary.This might lead to erroneous text display in some
860 # binary.This might lead to erroneous text display in some
863 # cases, but helps in other cases, like with text files
861 # cases, but helps in other cases, like with text files
864 # without extension.
862 # without extension.
865 mimetype, disposition = 'text/plain', 'inline'
863 mimetype, disposition = 'text/plain', 'inline'
866
864
867 if disposition == 'attachment':
865 if disposition == 'attachment':
868 disposition = self._get_attachement_headers(f_path)
866 disposition = self._get_attachement_headers(f_path)
869
867
870 stream_content = file_node.stream_bytes()
868 stream_content = file_node.stream_bytes()
871
869
872 response = Response(app_iter=stream_content)
870 response = Response(app_iter=stream_content)
873 response.content_disposition = disposition
871 response.content_disposition = disposition
874 response.content_type = mimetype
872 response.content_type = mimetype
875
873
876 charset = self._get_default_encoding(c)
874 charset = self._get_default_encoding(c)
877 if charset:
875 if charset:
878 response.charset = charset
876 response.charset = charset
879
877
880 return response
878 return response
881
879
882 @LoginRequired()
880 @LoginRequired()
883 @HasRepoPermissionAnyDecorator(
881 @HasRepoPermissionAnyDecorator(
884 'repository.read', 'repository.write', 'repository.admin')
882 'repository.read', 'repository.write', 'repository.admin')
885 def repo_file_download(self):
883 def repo_file_download(self):
886 c = self.load_default_context()
884 c = self.load_default_context()
887
885
888 commit_id, f_path = self._get_commit_and_path()
886 commit_id, f_path = self._get_commit_and_path()
889 commit = self._get_commit_or_redirect(commit_id)
887 commit = self._get_commit_or_redirect(commit_id)
890 file_node = self._get_filenode_or_redirect(commit, f_path)
888 file_node = self._get_filenode_or_redirect(commit, f_path)
891
889
892 if self.request.GET.get('lf'):
890 if self.request.GET.get('lf'):
893 # only if lf get flag is passed, we download this file
891 # only if lf get flag is passed, we download this file
894 # as LFS/Largefile
892 # as LFS/Largefile
895 lf_node = file_node.get_largefile_node()
893 lf_node = file_node.get_largefile_node()
896 if lf_node:
894 if lf_node:
897 # overwrite our pointer with the REAL large-file
895 # overwrite our pointer with the REAL large-file
898 file_node = lf_node
896 file_node = lf_node
899
897
900 disposition = self._get_attachement_headers(f_path)
898 disposition = self._get_attachement_headers(f_path)
901
899
902 stream_content = file_node.stream_bytes()
900 stream_content = file_node.stream_bytes()
903
901
904 response = Response(app_iter=stream_content)
902 response = Response(app_iter=stream_content)
905 response.content_disposition = disposition
903 response.content_disposition = disposition
906 response.content_type = file_node.mimetype
904 response.content_type = file_node.mimetype
907
905
908 charset = self._get_default_encoding(c)
906 charset = self._get_default_encoding(c)
909 if charset:
907 if charset:
910 response.charset = charset
908 response.charset = charset
911
909
912 return response
910 return response
913
911
914 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
912 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
915
913
916 cache_seconds = safe_int(
914 cache_seconds = safe_int(
917 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
915 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
918 cache_on = cache_seconds > 0
916 cache_on = cache_seconds > 0
919 log.debug(
917 log.debug(
920 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
918 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
921 'with caching: %s[TTL: %ss]' % (
919 'with caching: %s[TTL: %ss]' % (
922 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
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 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
923 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
926
924
927 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
925 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
928 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
926 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
929 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
927 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
930 _repo_id, commit_id, f_path)
928 _repo_id, commit_id, f_path)
931 try:
929 try:
932 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
930 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
933 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
931 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
934 log.exception(safe_str(e))
932 log.exception(safe_str(e))
935 h.flash(h.escape(safe_str(e)), category='error')
933 h.flash(h.escape(safe_str(e)), category='error')
936 raise HTTPFound(h.route_path(
934 raise HTTPFound(h.route_path(
937 'repo_files', repo_name=self.db_repo_name,
935 'repo_files', repo_name=self.db_repo_name,
938 commit_id='tip', f_path='/'))
936 commit_id='tip', f_path='/'))
939
937
940 return _d + _f
938 return _d + _f
941
939
942 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
940 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
943 commit_id, f_path)
941 commit_id, f_path)
944 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
942 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
945
943
946 @LoginRequired()
944 @LoginRequired()
947 @HasRepoPermissionAnyDecorator(
945 @HasRepoPermissionAnyDecorator(
948 'repository.read', 'repository.write', 'repository.admin')
946 'repository.read', 'repository.write', 'repository.admin')
949 def repo_nodelist(self):
947 def repo_nodelist(self):
950 self.load_default_context()
948 self.load_default_context()
951
949
952 commit_id, f_path = self._get_commit_and_path()
950 commit_id, f_path = self._get_commit_and_path()
953 commit = self._get_commit_or_redirect(commit_id)
951 commit = self._get_commit_or_redirect(commit_id)
954
952
955 metadata = self._get_nodelist_at_commit(
953 metadata = self._get_nodelist_at_commit(
956 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
954 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
957 return {'nodes': [x for x in metadata]}
955 return {'nodes': [x for x in metadata]}
958
956
959 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
957 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
960 items = []
958 items = []
961 for name, commit_id in branches_or_tags.items():
959 for name, commit_id in branches_or_tags.items():
962 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
960 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
963 items.append((sym_ref, name, ref_type))
961 items.append((sym_ref, name, ref_type))
964 return items
962 return items
965
963
966 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
964 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
967 return commit_id
965 return commit_id
968
966
969 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
967 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
970 return commit_id
968 return commit_id
971
969
972 # NOTE(dan): old code we used in "diff" mode compare
970 # NOTE(dan): old code we used in "diff" mode compare
973 new_f_path = vcspath.join(name, f_path)
971 new_f_path = vcspath.join(name, f_path)
974 return f'{new_f_path}@{commit_id}'
972 return f'{new_f_path}@{commit_id}'
975
973
976 def _get_node_history(self, commit_obj, f_path, commits=None):
974 def _get_node_history(self, commit_obj, f_path, commits=None):
977 """
975 """
978 get commit history for given node
976 get commit history for given node
979
977
980 :param commit_obj: commit to calculate history
978 :param commit_obj: commit to calculate history
981 :param f_path: path for node to calculate history for
979 :param f_path: path for node to calculate history for
982 :param commits: if passed don't calculate history and take
980 :param commits: if passed don't calculate history and take
983 commits defined in this list
981 commits defined in this list
984 """
982 """
985 _ = self.request.translate
983 _ = self.request.translate
986
984
987 # calculate history based on tip
985 # calculate history based on tip
988 tip = self.rhodecode_vcs_repo.get_commit()
986 tip = self.rhodecode_vcs_repo.get_commit()
989 if commits is None:
987 if commits is None:
990 pre_load = ["author", "branch"]
988 pre_load = ["author", "branch"]
991 try:
989 try:
992 commits = tip.get_path_history(f_path, pre_load=pre_load)
990 commits = tip.get_path_history(f_path, pre_load=pre_load)
993 except (NodeDoesNotExistError, CommitError):
991 except (NodeDoesNotExistError, CommitError):
994 # this node is not present at tip!
992 # this node is not present at tip!
995 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
993 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
996
994
997 history = []
995 history = []
998 commits_group = ([], _("Changesets"))
996 commits_group = ([], _("Changesets"))
999 for commit in commits:
997 for commit in commits:
1000 branch = ' (%s)' % commit.branch if commit.branch else ''
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 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1000 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1003 history.append(commits_group)
1001 history.append(commits_group)
1004
1002
1005 symbolic_reference = self._symbolic_reference
1003 symbolic_reference = self._symbolic_reference
1006
1004
1007 if self.rhodecode_vcs_repo.alias == 'svn':
1005 if self.rhodecode_vcs_repo.alias == 'svn':
1008 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1006 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1009 f_path, self.rhodecode_vcs_repo)
1007 f_path, self.rhodecode_vcs_repo)
1010 if adjusted_f_path != f_path:
1008 if adjusted_f_path != f_path:
1011 log.debug(
1009 log.debug(
1012 'Recognized svn tag or branch in file "%s", using svn '
1010 'Recognized svn tag or branch in file "%s", using svn '
1013 'specific symbolic references', f_path)
1011 'specific symbolic references', f_path)
1014 f_path = adjusted_f_path
1012 f_path = adjusted_f_path
1015 symbolic_reference = self._symbolic_reference_svn
1013 symbolic_reference = self._symbolic_reference_svn
1016
1014
1017 branches = self._create_references(
1015 branches = self._create_references(
1018 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1016 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1019 branches_group = (branches, _("Branches"))
1017 branches_group = (branches, _("Branches"))
1020
1018
1021 tags = self._create_references(
1019 tags = self._create_references(
1022 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1020 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1023 tags_group = (tags, _("Tags"))
1021 tags_group = (tags, _("Tags"))
1024
1022
1025 history.append(branches_group)
1023 history.append(branches_group)
1026 history.append(tags_group)
1024 history.append(tags_group)
1027
1025
1028 return history, commits
1026 return history, commits
1029
1027
1030 @LoginRequired()
1028 @LoginRequired()
1031 @HasRepoPermissionAnyDecorator(
1029 @HasRepoPermissionAnyDecorator(
1032 'repository.read', 'repository.write', 'repository.admin')
1030 'repository.read', 'repository.write', 'repository.admin')
1033 def repo_file_history(self):
1031 def repo_file_history(self):
1034 self.load_default_context()
1032 self.load_default_context()
1035
1033
1036 commit_id, f_path = self._get_commit_and_path()
1034 commit_id, f_path = self._get_commit_and_path()
1037 commit = self._get_commit_or_redirect(commit_id)
1035 commit = self._get_commit_or_redirect(commit_id)
1038 file_node = self._get_filenode_or_redirect(commit, f_path)
1036 file_node = self._get_filenode_or_redirect(commit, f_path)
1039
1037
1040 if file_node.is_file():
1038 if file_node.is_file():
1041 file_history, _hist = self._get_node_history(commit, f_path)
1039 file_history, _hist = self._get_node_history(commit, f_path)
1042
1040
1043 res = []
1041 res = []
1044 for section_items, section in file_history:
1042 for section_items, section in file_history:
1045 items = []
1043 items = []
1046 for obj_id, obj_text, obj_type in section_items:
1044 for obj_id, obj_text, obj_type in section_items:
1047 at_rev = ''
1045 at_rev = ''
1048 if obj_type in ['branch', 'bookmark', 'tag']:
1046 if obj_type in ['branch', 'bookmark', 'tag']:
1049 at_rev = obj_text
1047 at_rev = obj_text
1050 entry = {
1048 entry = {
1051 'id': obj_id,
1049 'id': obj_id,
1052 'text': obj_text,
1050 'text': obj_text,
1053 'type': obj_type,
1051 'type': obj_type,
1054 'at_rev': at_rev
1052 'at_rev': at_rev
1055 }
1053 }
1056
1054
1057 items.append(entry)
1055 items.append(entry)
1058
1056
1059 res.append({
1057 res.append({
1060 'text': section,
1058 'text': section,
1061 'children': items
1059 'children': items
1062 })
1060 })
1063
1061
1064 data = {
1062 data = {
1065 'more': False,
1063 'more': False,
1066 'results': res
1064 'results': res
1067 }
1065 }
1068 return data
1066 return data
1069
1067
1070 log.warning('Cannot fetch history for directory')
1068 log.warning('Cannot fetch history for directory')
1071 raise HTTPBadRequest()
1069 raise HTTPBadRequest()
1072
1070
1073 @LoginRequired()
1071 @LoginRequired()
1074 @HasRepoPermissionAnyDecorator(
1072 @HasRepoPermissionAnyDecorator(
1075 'repository.read', 'repository.write', 'repository.admin')
1073 'repository.read', 'repository.write', 'repository.admin')
1076 def repo_file_authors(self):
1074 def repo_file_authors(self):
1077 c = self.load_default_context()
1075 c = self.load_default_context()
1078
1076
1079 commit_id, f_path = self._get_commit_and_path()
1077 commit_id, f_path = self._get_commit_and_path()
1080 commit = self._get_commit_or_redirect(commit_id)
1078 commit = self._get_commit_or_redirect(commit_id)
1081 file_node = self._get_filenode_or_redirect(commit, f_path)
1079 file_node = self._get_filenode_or_redirect(commit, f_path)
1082
1080
1083 if not file_node.is_file():
1081 if not file_node.is_file():
1084 raise HTTPBadRequest()
1082 raise HTTPBadRequest()
1085
1083
1086 c.file_last_commit = file_node.last_commit
1084 c.file_last_commit = file_node.last_commit
1087 if self.request.GET.get('annotate') == '1':
1085 if self.request.GET.get('annotate') == '1':
1088 # use _hist from annotation if annotation mode is on
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 _hist = (
1088 _hist = (
1091 self.rhodecode_vcs_repo.get_commit(commit_id)
1089 self.rhodecode_vcs_repo.get_commit(commit_id)
1092 for commit_id in commit_ids)
1090 for commit_id in commit_ids)
1093 else:
1091 else:
1094 _f_history, _hist = self._get_node_history(commit, f_path)
1092 _f_history, _hist = self._get_node_history(commit, f_path)
1095 c.file_author = False
1093 c.file_author = False
1096
1094
1097 unique = collections.OrderedDict()
1095 unique = collections.OrderedDict()
1098 for commit in _hist:
1096 for commit in _hist:
1099 author = commit.author
1097 author = commit.author
1100 if author not in unique:
1098 if author not in unique:
1101 unique[commit.author] = [
1099 unique[commit.author] = [
1102 h.email(author),
1100 h.email(author),
1103 h.person(author, 'username_or_name_or_email'),
1101 h.person(author, 'username_or_name_or_email'),
1104 1 # counter
1102 1 # counter
1105 ]
1103 ]
1106
1104
1107 else:
1105 else:
1108 # increase counter
1106 # increase counter
1109 unique[commit.author][2] += 1
1107 unique[commit.author][2] += 1
1110
1108
1111 c.authors = [val for val in unique.values()]
1109 c.authors = [val for val in unique.values()]
1112
1110
1113 return self._get_template_context(c)
1111 return self._get_template_context(c)
1114
1112
1115 @LoginRequired()
1113 @LoginRequired()
1116 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1117 def repo_files_check_head(self):
1115 def repo_files_check_head(self):
1118 self.load_default_context()
1116 self.load_default_context()
1119
1117
1120 commit_id, f_path = self._get_commit_and_path()
1118 commit_id, f_path = self._get_commit_and_path()
1121 _branch_name, _sha_commit_id, is_head = \
1119 _branch_name, _sha_commit_id, is_head = \
1122 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1120 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1123 landing_ref=self.db_repo.landing_ref_name)
1121 landing_ref=self.db_repo.landing_ref_name)
1124
1122
1125 new_path = self.request.POST.get('path')
1123 new_path = self.request.POST.get('path')
1126 operation = self.request.POST.get('operation')
1124 operation = self.request.POST.get('operation')
1127 path_exist = ''
1125 path_exist = ''
1128
1126
1129 if new_path and operation in ['create', 'upload']:
1127 if new_path and operation in ['create', 'upload']:
1130 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1128 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1131 try:
1129 try:
1132 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1130 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1133 # NOTE(dan): construct whole path without leading /
1131 # NOTE(dan): construct whole path without leading /
1134 file_node = commit_obj.get_node(new_f_path)
1132 file_node = commit_obj.get_node(new_f_path)
1135 if file_node is not None:
1133 if file_node is not None:
1136 path_exist = new_f_path
1134 path_exist = new_f_path
1137 except EmptyRepositoryError:
1135 except EmptyRepositoryError:
1138 pass
1136 pass
1139 except Exception:
1137 except Exception:
1140 pass
1138 pass
1141
1139
1142 return {
1140 return {
1143 'branch': _branch_name,
1141 'branch': _branch_name,
1144 'sha': _sha_commit_id,
1142 'sha': _sha_commit_id,
1145 'is_head': is_head,
1143 'is_head': is_head,
1146 'path_exists': path_exist
1144 'path_exists': path_exist
1147 }
1145 }
1148
1146
1149 @LoginRequired()
1147 @LoginRequired()
1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1148 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 def repo_files_remove_file(self):
1149 def repo_files_remove_file(self):
1152 _ = self.request.translate
1150 _ = self.request.translate
1153 c = self.load_default_context()
1151 c = self.load_default_context()
1154 commit_id, f_path = self._get_commit_and_path()
1152 commit_id, f_path = self._get_commit_and_path()
1155
1153
1156 self._ensure_not_locked()
1154 self._ensure_not_locked()
1157 _branch_name, _sha_commit_id, is_head = \
1155 _branch_name, _sha_commit_id, is_head = \
1158 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1156 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1159 landing_ref=self.db_repo.landing_ref_name)
1157 landing_ref=self.db_repo.landing_ref_name)
1160
1158
1161 self.forbid_non_head(is_head, f_path)
1159 self.forbid_non_head(is_head, f_path)
1162 self.check_branch_permission(_branch_name)
1160 self.check_branch_permission(_branch_name)
1163
1161
1164 c.commit = self._get_commit_or_redirect(commit_id)
1162 c.commit = self._get_commit_or_redirect(commit_id)
1165 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1163 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1166
1164
1167 c.default_message = _(
1165 c.default_message = _(
1168 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1166 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1169 c.f_path = f_path
1167 c.f_path = f_path
1170
1168
1171 return self._get_template_context(c)
1169 return self._get_template_context(c)
1172
1170
1173 @LoginRequired()
1171 @LoginRequired()
1174 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1172 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1175 @CSRFRequired()
1173 @CSRFRequired()
1176 def repo_files_delete_file(self):
1174 def repo_files_delete_file(self):
1177 _ = self.request.translate
1175 _ = self.request.translate
1178
1176
1179 c = self.load_default_context()
1177 c = self.load_default_context()
1180 commit_id, f_path = self._get_commit_and_path()
1178 commit_id, f_path = self._get_commit_and_path()
1181
1179
1182 self._ensure_not_locked()
1180 self._ensure_not_locked()
1183 _branch_name, _sha_commit_id, is_head = \
1181 _branch_name, _sha_commit_id, is_head = \
1184 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1182 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1185 landing_ref=self.db_repo.landing_ref_name)
1183 landing_ref=self.db_repo.landing_ref_name)
1186
1184
1187 self.forbid_non_head(is_head, f_path)
1185 self.forbid_non_head(is_head, f_path)
1188 self.check_branch_permission(_branch_name)
1186 self.check_branch_permission(_branch_name)
1189
1187
1190 c.commit = self._get_commit_or_redirect(commit_id)
1188 c.commit = self._get_commit_or_redirect(commit_id)
1191 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1192
1190
1193 c.default_message = _(
1191 c.default_message = _(
1194 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1192 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1195 c.f_path = f_path
1193 c.f_path = f_path
1196 node_path = f_path
1194 node_path = f_path
1197 author = self._rhodecode_db_user.full_contact
1195 author = self._rhodecode_db_user.full_contact
1198 message = self.request.POST.get('message') or c.default_message
1196 message = self.request.POST.get('message') or c.default_message
1199 try:
1197 try:
1200 nodes = {
1198 nodes = {
1201 safe_bytes(node_path): {
1199 safe_bytes(node_path): {
1202 'content': b''
1200 'content': b''
1203 }
1201 }
1204 }
1202 }
1205 ScmModel().delete_nodes(
1203 ScmModel().delete_nodes(
1206 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1204 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1207 message=message,
1205 message=message,
1208 nodes=nodes,
1206 nodes=nodes,
1209 parent_commit=c.commit,
1207 parent_commit=c.commit,
1210 author=author,
1208 author=author,
1211 )
1209 )
1212
1210
1213 h.flash(
1211 h.flash(
1214 _('Successfully deleted file `{}`').format(
1212 _('Successfully deleted file `{}`').format(
1215 h.escape(f_path)), category='success')
1213 h.escape(f_path)), category='success')
1216 except Exception:
1214 except Exception:
1217 log.exception('Error during commit operation')
1215 log.exception('Error during commit operation')
1218 h.flash(_('Error occurred during commit'), category='error')
1216 h.flash(_('Error occurred during commit'), category='error')
1219 raise HTTPFound(
1217 raise HTTPFound(
1220 h.route_path('repo_commit', repo_name=self.db_repo_name,
1218 h.route_path('repo_commit', repo_name=self.db_repo_name,
1221 commit_id='tip'))
1219 commit_id='tip'))
1222
1220
1223 @LoginRequired()
1221 @LoginRequired()
1224 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1222 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1225 def repo_files_edit_file(self):
1223 def repo_files_edit_file(self):
1226 _ = self.request.translate
1224 _ = self.request.translate
1227 c = self.load_default_context()
1225 c = self.load_default_context()
1228 commit_id, f_path = self._get_commit_and_path()
1226 commit_id, f_path = self._get_commit_and_path()
1229
1227
1230 self._ensure_not_locked()
1228 self._ensure_not_locked()
1231 _branch_name, _sha_commit_id, is_head = \
1229 _branch_name, _sha_commit_id, is_head = \
1232 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1230 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1233 landing_ref=self.db_repo.landing_ref_name)
1231 landing_ref=self.db_repo.landing_ref_name)
1234
1232
1235 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1233 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1236 self.check_branch_permission(_branch_name, commit_id=commit_id)
1234 self.check_branch_permission(_branch_name, commit_id=commit_id)
1237
1235
1238 c.commit = self._get_commit_or_redirect(commit_id)
1236 c.commit = self._get_commit_or_redirect(commit_id)
1239 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1237 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1240
1238
1241 if c.file.is_binary:
1239 if c.file.is_binary:
1242 files_url = h.route_path(
1240 files_url = h.route_path(
1243 'repo_files',
1241 'repo_files',
1244 repo_name=self.db_repo_name,
1242 repo_name=self.db_repo_name,
1245 commit_id=c.commit.raw_id, f_path=f_path)
1243 commit_id=c.commit.raw_id, f_path=f_path)
1246 raise HTTPFound(files_url)
1244 raise HTTPFound(files_url)
1247
1245
1248 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1246 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1249 c.f_path = f_path
1247 c.f_path = f_path
1250
1248
1251 return self._get_template_context(c)
1249 return self._get_template_context(c)
1252
1250
1253 @LoginRequired()
1251 @LoginRequired()
1254 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1252 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1255 @CSRFRequired()
1253 @CSRFRequired()
1256 def repo_files_update_file(self):
1254 def repo_files_update_file(self):
1257 _ = self.request.translate
1255 _ = self.request.translate
1258 c = self.load_default_context()
1256 c = self.load_default_context()
1259 commit_id, f_path = self._get_commit_and_path()
1257 commit_id, f_path = self._get_commit_and_path()
1260
1258
1261 self._ensure_not_locked()
1259 self._ensure_not_locked()
1262
1260
1263 c.commit = self._get_commit_or_redirect(commit_id)
1261 c.commit = self._get_commit_or_redirect(commit_id)
1264 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1262 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1265
1263
1266 if c.file.is_binary:
1264 if c.file.is_binary:
1267 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1265 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1268 commit_id=c.commit.raw_id, f_path=f_path))
1266 commit_id=c.commit.raw_id, f_path=f_path))
1269
1267
1270 _branch_name, _sha_commit_id, is_head = \
1268 _branch_name, _sha_commit_id, is_head = \
1271 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1269 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1272 landing_ref=self.db_repo.landing_ref_name)
1270 landing_ref=self.db_repo.landing_ref_name)
1273
1271
1274 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1272 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1275 self.check_branch_permission(_branch_name, commit_id=commit_id)
1273 self.check_branch_permission(_branch_name, commit_id=commit_id)
1276
1274
1277 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1275 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1278 c.f_path = f_path
1276 c.f_path = f_path
1279
1277
1280 old_content = c.file.str_content
1278 old_content = c.file.str_content
1281 sl = old_content.splitlines(1)
1279 sl = old_content.splitlines(1)
1282 first_line = sl[0] if sl else ''
1280 first_line = sl[0] if sl else ''
1283
1281
1284 r_post = self.request.POST
1282 r_post = self.request.POST
1285 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1283 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1286 line_ending_mode = detect_mode(first_line, 0)
1284 line_ending_mode = detect_mode(first_line, 0)
1287 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1285 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1288
1286
1289 message = r_post.get('message') or c.default_message
1287 message = r_post.get('message') or c.default_message
1290
1288
1291 org_node_path = c.file.str_path
1289 org_node_path = c.file.str_path
1292 filename = r_post['filename']
1290 filename = r_post['filename']
1293
1291
1294 root_path = c.file.dir_path
1292 root_path = c.file.dir_path
1295 pure_path = self.create_pure_path(root_path, filename)
1293 pure_path = self.create_pure_path(root_path, filename)
1296 node_path = pure_path.as_posix()
1294 node_path = pure_path.as_posix()
1297
1295
1298 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1296 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1299 commit_id=commit_id)
1297 commit_id=commit_id)
1300 if content == old_content and node_path == org_node_path:
1298 if content == old_content and node_path == org_node_path:
1301 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1299 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1302 category='warning')
1300 category='warning')
1303 raise HTTPFound(default_redirect_url)
1301 raise HTTPFound(default_redirect_url)
1304
1302
1305 try:
1303 try:
1306 mapping = {
1304 mapping = {
1307 c.file.bytes_path: {
1305 c.file.bytes_path: {
1308 'org_filename': org_node_path,
1306 'org_filename': org_node_path,
1309 'filename': safe_bytes(node_path),
1307 'filename': safe_bytes(node_path),
1310 'content': safe_bytes(content),
1308 'content': safe_bytes(content),
1311 'lexer': '',
1309 'lexer': '',
1312 'op': 'mod',
1310 'op': 'mod',
1313 'mode': c.file.mode
1311 'mode': c.file.mode
1314 }
1312 }
1315 }
1313 }
1316
1314
1317 commit = ScmModel().update_nodes(
1315 commit = ScmModel().update_nodes(
1318 user=self._rhodecode_db_user.user_id,
1316 user=self._rhodecode_db_user.user_id,
1319 repo=self.db_repo,
1317 repo=self.db_repo,
1320 message=message,
1318 message=message,
1321 nodes=mapping,
1319 nodes=mapping,
1322 parent_commit=c.commit,
1320 parent_commit=c.commit,
1323 )
1321 )
1324
1322
1325 h.flash(_('Successfully committed changes to file `{}`').format(
1323 h.flash(_('Successfully committed changes to file `{}`').format(
1326 h.escape(f_path)), category='success')
1324 h.escape(f_path)), category='success')
1327 default_redirect_url = h.route_path(
1325 default_redirect_url = h.route_path(
1328 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1326 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1329
1327
1330 except Exception:
1328 except Exception:
1331 log.exception('Error occurred during commit')
1329 log.exception('Error occurred during commit')
1332 h.flash(_('Error occurred during commit'), category='error')
1330 h.flash(_('Error occurred during commit'), category='error')
1333
1331
1334 raise HTTPFound(default_redirect_url)
1332 raise HTTPFound(default_redirect_url)
1335
1333
1336 @LoginRequired()
1334 @LoginRequired()
1337 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1335 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1338 def repo_files_add_file(self):
1336 def repo_files_add_file(self):
1339 _ = self.request.translate
1337 _ = self.request.translate
1340 c = self.load_default_context()
1338 c = self.load_default_context()
1341 commit_id, f_path = self._get_commit_and_path()
1339 commit_id, f_path = self._get_commit_and_path()
1342
1340
1343 self._ensure_not_locked()
1341 self._ensure_not_locked()
1344
1342
1345 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1343 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1346 if c.commit is None:
1344 if c.commit is None:
1347 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1345 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1348
1346
1349 if self.rhodecode_vcs_repo.is_empty():
1347 if self.rhodecode_vcs_repo.is_empty():
1350 # for empty repository we cannot check for current branch, we rely on
1348 # for empty repository we cannot check for current branch, we rely on
1351 # c.commit.branch instead
1349 # c.commit.branch instead
1352 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1350 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1353 else:
1351 else:
1354 _branch_name, _sha_commit_id, is_head = \
1352 _branch_name, _sha_commit_id, is_head = \
1355 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1353 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1356 landing_ref=self.db_repo.landing_ref_name)
1354 landing_ref=self.db_repo.landing_ref_name)
1357
1355
1358 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1356 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1359 self.check_branch_permission(_branch_name, commit_id=commit_id)
1357 self.check_branch_permission(_branch_name, commit_id=commit_id)
1360
1358
1361 c.default_message = (_('Added file via RhodeCode Enterprise'))
1359 c.default_message = (_('Added file via RhodeCode Enterprise'))
1362 c.f_path = f_path.lstrip('/') # ensure not relative path
1360 c.f_path = f_path.lstrip('/') # ensure not relative path
1363
1361
1364 return self._get_template_context(c)
1362 return self._get_template_context(c)
1365
1363
1366 @LoginRequired()
1364 @LoginRequired()
1367 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1365 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1368 @CSRFRequired()
1366 @CSRFRequired()
1369 def repo_files_create_file(self):
1367 def repo_files_create_file(self):
1370 _ = self.request.translate
1368 _ = self.request.translate
1371 c = self.load_default_context()
1369 c = self.load_default_context()
1372 commit_id, f_path = self._get_commit_and_path()
1370 commit_id, f_path = self._get_commit_and_path()
1373
1371
1374 self._ensure_not_locked()
1372 self._ensure_not_locked()
1375
1373
1376 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1374 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1377 if c.commit is None:
1375 if c.commit is None:
1378 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1376 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1379
1377
1380 # calculate redirect URL
1378 # calculate redirect URL
1381 if self.rhodecode_vcs_repo.is_empty():
1379 if self.rhodecode_vcs_repo.is_empty():
1382 default_redirect_url = h.route_path(
1380 default_redirect_url = h.route_path(
1383 'repo_summary', repo_name=self.db_repo_name)
1381 'repo_summary', repo_name=self.db_repo_name)
1384 else:
1382 else:
1385 default_redirect_url = h.route_path(
1383 default_redirect_url = h.route_path(
1386 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1384 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1387
1385
1388 if self.rhodecode_vcs_repo.is_empty():
1386 if self.rhodecode_vcs_repo.is_empty():
1389 # for empty repository we cannot check for current branch, we rely on
1387 # for empty repository we cannot check for current branch, we rely on
1390 # c.commit.branch instead
1388 # c.commit.branch instead
1391 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1389 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1392 else:
1390 else:
1393 _branch_name, _sha_commit_id, is_head = \
1391 _branch_name, _sha_commit_id, is_head = \
1394 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1392 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1395 landing_ref=self.db_repo.landing_ref_name)
1393 landing_ref=self.db_repo.landing_ref_name)
1396
1394
1397 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1395 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1398 self.check_branch_permission(_branch_name, commit_id=commit_id)
1396 self.check_branch_permission(_branch_name, commit_id=commit_id)
1399
1397
1400 c.default_message = (_('Added file via RhodeCode Enterprise'))
1398 c.default_message = (_('Added file via RhodeCode Enterprise'))
1401 c.f_path = f_path
1399 c.f_path = f_path
1402
1400
1403 r_post = self.request.POST
1401 r_post = self.request.POST
1404 message = r_post.get('message') or c.default_message
1402 message = r_post.get('message') or c.default_message
1405 filename = r_post.get('filename')
1403 filename = r_post.get('filename')
1406 unix_mode = 0
1404 unix_mode = 0
1407
1405
1408 if not filename:
1406 if not filename:
1409 # If there's no commit, redirect to repo summary
1407 # If there's no commit, redirect to repo summary
1410 if type(c.commit) is EmptyCommit:
1408 if type(c.commit) is EmptyCommit:
1411 redirect_url = h.route_path(
1409 redirect_url = h.route_path(
1412 'repo_summary', repo_name=self.db_repo_name)
1410 'repo_summary', repo_name=self.db_repo_name)
1413 else:
1411 else:
1414 redirect_url = default_redirect_url
1412 redirect_url = default_redirect_url
1415 h.flash(_('No filename specified'), category='warning')
1413 h.flash(_('No filename specified'), category='warning')
1416 raise HTTPFound(redirect_url)
1414 raise HTTPFound(redirect_url)
1417
1415
1418 root_path = f_path
1416 root_path = f_path
1419 pure_path = self.create_pure_path(root_path, filename)
1417 pure_path = self.create_pure_path(root_path, filename)
1420 node_path = pure_path.as_posix().lstrip('/')
1418 node_path = pure_path.as_posix().lstrip('/')
1421
1419
1422 author = self._rhodecode_db_user.full_contact
1420 author = self._rhodecode_db_user.full_contact
1423 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1421 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1424 nodes = {
1422 nodes = {
1425 safe_bytes(node_path): {
1423 safe_bytes(node_path): {
1426 'content': safe_bytes(content)
1424 'content': safe_bytes(content)
1427 }
1425 }
1428 }
1426 }
1429
1427
1430 try:
1428 try:
1431
1429
1432 commit = ScmModel().create_nodes(
1430 commit = ScmModel().create_nodes(
1433 user=self._rhodecode_db_user.user_id,
1431 user=self._rhodecode_db_user.user_id,
1434 repo=self.db_repo,
1432 repo=self.db_repo,
1435 message=message,
1433 message=message,
1436 nodes=nodes,
1434 nodes=nodes,
1437 parent_commit=c.commit,
1435 parent_commit=c.commit,
1438 author=author,
1436 author=author,
1439 )
1437 )
1440
1438
1441 h.flash(_('Successfully committed new file `{}`').format(
1439 h.flash(_('Successfully committed new file `{}`').format(
1442 h.escape(node_path)), category='success')
1440 h.escape(node_path)), category='success')
1443
1441
1444 default_redirect_url = h.route_path(
1442 default_redirect_url = h.route_path(
1445 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1443 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1446
1444
1447 except NonRelativePathError:
1445 except NonRelativePathError:
1448 log.exception('Non Relative path found')
1446 log.exception('Non Relative path found')
1449 h.flash(_('The location specified must be a relative path and must not '
1447 h.flash(_('The location specified must be a relative path and must not '
1450 'contain .. in the path'), category='warning')
1448 'contain .. in the path'), category='warning')
1451 raise HTTPFound(default_redirect_url)
1449 raise HTTPFound(default_redirect_url)
1452 except (NodeError, NodeAlreadyExistsError) as e:
1450 except (NodeError, NodeAlreadyExistsError) as e:
1453 h.flash(h.escape(safe_str(e)), category='error')
1451 h.flash(h.escape(safe_str(e)), category='error')
1454 except Exception:
1452 except Exception:
1455 log.exception('Error occurred during commit')
1453 log.exception('Error occurred during commit')
1456 h.flash(_('Error occurred during commit'), category='error')
1454 h.flash(_('Error occurred during commit'), category='error')
1457
1455
1458 raise HTTPFound(default_redirect_url)
1456 raise HTTPFound(default_redirect_url)
1459
1457
1460 @LoginRequired()
1458 @LoginRequired()
1461 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1459 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1462 @CSRFRequired()
1460 @CSRFRequired()
1463 def repo_files_upload_file(self):
1461 def repo_files_upload_file(self):
1464 _ = self.request.translate
1462 _ = self.request.translate
1465 c = self.load_default_context()
1463 c = self.load_default_context()
1466 commit_id, f_path = self._get_commit_and_path()
1464 commit_id, f_path = self._get_commit_and_path()
1467
1465
1468 self._ensure_not_locked()
1466 self._ensure_not_locked()
1469
1467
1470 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1468 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1471 if c.commit is None:
1469 if c.commit is None:
1472 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1470 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1473
1471
1474 # calculate redirect URL
1472 # calculate redirect URL
1475 if self.rhodecode_vcs_repo.is_empty():
1473 if self.rhodecode_vcs_repo.is_empty():
1476 default_redirect_url = h.route_path(
1474 default_redirect_url = h.route_path(
1477 'repo_summary', repo_name=self.db_repo_name)
1475 'repo_summary', repo_name=self.db_repo_name)
1478 else:
1476 else:
1479 default_redirect_url = h.route_path(
1477 default_redirect_url = h.route_path(
1480 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1478 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1481
1479
1482 if self.rhodecode_vcs_repo.is_empty():
1480 if self.rhodecode_vcs_repo.is_empty():
1483 # for empty repository we cannot check for current branch, we rely on
1481 # for empty repository we cannot check for current branch, we rely on
1484 # c.commit.branch instead
1482 # c.commit.branch instead
1485 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1483 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1486 else:
1484 else:
1487 _branch_name, _sha_commit_id, is_head = \
1485 _branch_name, _sha_commit_id, is_head = \
1488 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1486 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1489 landing_ref=self.db_repo.landing_ref_name)
1487 landing_ref=self.db_repo.landing_ref_name)
1490
1488
1491 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1489 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1492 if error:
1490 if error:
1493 return {
1491 return {
1494 'error': error,
1492 'error': error,
1495 'redirect_url': default_redirect_url
1493 'redirect_url': default_redirect_url
1496 }
1494 }
1497 error = self.check_branch_permission(_branch_name, json_mode=True)
1495 error = self.check_branch_permission(_branch_name, json_mode=True)
1498 if error:
1496 if error:
1499 return {
1497 return {
1500 'error': error,
1498 'error': error,
1501 'redirect_url': default_redirect_url
1499 'redirect_url': default_redirect_url
1502 }
1500 }
1503
1501
1504 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1502 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1505 c.f_path = f_path
1503 c.f_path = f_path
1506
1504
1507 r_post = self.request.POST
1505 r_post = self.request.POST
1508
1506
1509 message = c.default_message
1507 message = c.default_message
1510 user_message = r_post.getall('message')
1508 user_message = r_post.getall('message')
1511 if isinstance(user_message, list) and user_message:
1509 if isinstance(user_message, list) and user_message:
1512 # we take the first from duplicated results if it's not empty
1510 # we take the first from duplicated results if it's not empty
1513 message = user_message[0] if user_message[0] else message
1511 message = user_message[0] if user_message[0] else message
1514
1512
1515 nodes = {}
1513 nodes = {}
1516
1514
1517 for file_obj in r_post.getall('files_upload') or []:
1515 for file_obj in r_post.getall('files_upload') or []:
1518 content = file_obj.file
1516 content = file_obj.file
1519 filename = file_obj.filename
1517 filename = file_obj.filename
1520
1518
1521 root_path = f_path
1519 root_path = f_path
1522 pure_path = self.create_pure_path(root_path, filename)
1520 pure_path = self.create_pure_path(root_path, filename)
1523 node_path = pure_path.as_posix().lstrip('/')
1521 node_path = pure_path.as_posix().lstrip('/')
1524
1522
1525 nodes[safe_bytes(node_path)] = {
1523 nodes[safe_bytes(node_path)] = {
1526 'content': content
1524 'content': content
1527 }
1525 }
1528
1526
1529 if not nodes:
1527 if not nodes:
1530 error = 'missing files'
1528 error = 'missing files'
1531 return {
1529 return {
1532 'error': error,
1530 'error': error,
1533 'redirect_url': default_redirect_url
1531 'redirect_url': default_redirect_url
1534 }
1532 }
1535
1533
1536 author = self._rhodecode_db_user.full_contact
1534 author = self._rhodecode_db_user.full_contact
1537
1535
1538 try:
1536 try:
1539 commit = ScmModel().create_nodes(
1537 commit = ScmModel().create_nodes(
1540 user=self._rhodecode_db_user.user_id,
1538 user=self._rhodecode_db_user.user_id,
1541 repo=self.db_repo,
1539 repo=self.db_repo,
1542 message=message,
1540 message=message,
1543 nodes=nodes,
1541 nodes=nodes,
1544 parent_commit=c.commit,
1542 parent_commit=c.commit,
1545 author=author,
1543 author=author,
1546 )
1544 )
1547 if len(nodes) == 1:
1545 if len(nodes) == 1:
1548 flash_message = _('Successfully committed {} new files').format(len(nodes))
1546 flash_message = _('Successfully committed {} new files').format(len(nodes))
1549 else:
1547 else:
1550 flash_message = _('Successfully committed 1 new file')
1548 flash_message = _('Successfully committed 1 new file')
1551
1549
1552 h.flash(flash_message, category='success')
1550 h.flash(flash_message, category='success')
1553
1551
1554 default_redirect_url = h.route_path(
1552 default_redirect_url = h.route_path(
1555 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1553 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1556
1554
1557 except NonRelativePathError:
1555 except NonRelativePathError:
1558 log.exception('Non Relative path found')
1556 log.exception('Non Relative path found')
1559 error = _('The location specified must be a relative path and must not '
1557 error = _('The location specified must be a relative path and must not '
1560 'contain .. in the path')
1558 'contain .. in the path')
1561 h.flash(error, category='warning')
1559 h.flash(error, category='warning')
1562
1560
1563 return {
1561 return {
1564 'error': error,
1562 'error': error,
1565 'redirect_url': default_redirect_url
1563 'redirect_url': default_redirect_url
1566 }
1564 }
1567 except (NodeError, NodeAlreadyExistsError) as e:
1565 except (NodeError, NodeAlreadyExistsError) as e:
1568 error = h.escape(e)
1566 error = h.escape(e)
1569 h.flash(error, category='error')
1567 h.flash(error, category='error')
1570
1568
1571 return {
1569 return {
1572 'error': error,
1570 'error': error,
1573 'redirect_url': default_redirect_url
1571 'redirect_url': default_redirect_url
1574 }
1572 }
1575 except Exception:
1573 except Exception:
1576 log.exception('Error occurred during commit')
1574 log.exception('Error occurred during commit')
1577 error = _('Error occurred during commit')
1575 error = _('Error occurred during commit')
1578 h.flash(error, category='error')
1576 h.flash(error, category='error')
1579 return {
1577 return {
1580 'error': error,
1578 'error': error,
1581 'redirect_url': default_redirect_url
1579 'redirect_url': default_redirect_url
1582 }
1580 }
1583
1581
1584 return {
1582 return {
1585 'error': None,
1583 'error': None,
1586 'redirect_url': default_redirect_url
1584 'redirect_url': default_redirect_url
1587 }
1585 }
@@ -1,254 +1,252 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import datetime
20 import datetime
23 import formencode
21 import formencode
24 import formencode.htmlfill
22 import formencode.htmlfill
25
23
26 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
27
25
28 from pyramid.renderers import render
26 from pyramid.renderers import render
29 from pyramid.response import Response
27 from pyramid.response import Response
30
28
31 from rhodecode import events
29 from rhodecode import events
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
30 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
32 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
33 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 import rhodecode.lib.helpers as h
34 import rhodecode.lib.helpers as h
37 from rhodecode.lib.str_utils import safe_str
35 from rhodecode.lib.str_utils import safe_str
38 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.celerylib.utils import get_task_id
39 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
37 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
40 from rhodecode.model.permission import PermissionModel
38 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.forms import RepoForkForm
40 from rhodecode.model.forms import RepoForkForm
43 from rhodecode.model.scm import ScmModel, RepoGroupList
41 from rhodecode.model.scm import ScmModel, RepoGroupList
44
42
45 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
46
44
47
45
48 class RepoForksView(RepoAppView, DataGridAppView):
46 class RepoForksView(RepoAppView, DataGridAppView):
49
47
50 def load_default_context(self):
48 def load_default_context(self):
51 c = self._get_local_tmpl_context(include_app_defaults=True)
49 c = self._get_local_tmpl_context(include_app_defaults=True)
52 c.rhodecode_repo = self.rhodecode_vcs_repo
50 c.rhodecode_repo = self.rhodecode_vcs_repo
53
51
54 acl_groups = RepoGroupList(
52 acl_groups = RepoGroupList(
55 RepoGroup.query().all(),
53 RepoGroup.query().all(),
56 perm_set=['group.write', 'group.admin'])
54 perm_set=['group.write', 'group.admin'])
57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
55 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
56 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
59
57
60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
58 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61
59
62 return c
60 return c
63
61
64 @LoginRequired()
62 @LoginRequired()
65 @HasRepoPermissionAnyDecorator(
63 @HasRepoPermissionAnyDecorator(
66 'repository.read', 'repository.write', 'repository.admin')
64 'repository.read', 'repository.write', 'repository.admin')
67 def repo_forks_show_all(self):
65 def repo_forks_show_all(self):
68 c = self.load_default_context()
66 c = self.load_default_context()
69 return self._get_template_context(c)
67 return self._get_template_context(c)
70
68
71 @LoginRequired()
69 @LoginRequired()
72 @HasRepoPermissionAnyDecorator(
70 @HasRepoPermissionAnyDecorator(
73 'repository.read', 'repository.write', 'repository.admin')
71 'repository.read', 'repository.write', 'repository.admin')
74 def repo_forks_data(self):
72 def repo_forks_data(self):
75 _ = self.request.translate
73 _ = self.request.translate
76 self.load_default_context()
74 self.load_default_context()
77 column_map = {
75 column_map = {
78 'fork_name': 'repo_name',
76 'fork_name': 'repo_name',
79 'fork_date': 'created_on',
77 'fork_date': 'created_on',
80 'last_activity': 'updated_on'
78 'last_activity': 'updated_on'
81 }
79 }
82 draw, start, limit = self._extract_chunk(self.request)
80 draw, start, limit = self._extract_chunk(self.request)
83 search_q, order_by, order_dir = self._extract_ordering(
81 search_q, order_by, order_dir = self._extract_ordering(
84 self.request, column_map=column_map)
82 self.request, column_map=column_map)
85
83
86 acl_check = HasRepoPermissionAny(
84 acl_check = HasRepoPermissionAny(
87 'repository.read', 'repository.write', 'repository.admin')
85 'repository.read', 'repository.write', 'repository.admin')
88 repo_id = self.db_repo.repo_id
86 repo_id = self.db_repo.repo_id
89 allowed_ids = [-1]
87 allowed_ids = [-1]
90 for f in Repository.query().filter(Repository.fork_id == repo_id):
88 for f in Repository.query().filter(Repository.fork_id == repo_id):
91 if acl_check(f.repo_name, 'get forks check'):
89 if acl_check(f.repo_name, 'get forks check'):
92 allowed_ids.append(f.repo_id)
90 allowed_ids.append(f.repo_id)
93
91
94 forks_data_total_count = Repository.query()\
92 forks_data_total_count = Repository.query()\
95 .filter(Repository.fork_id == repo_id)\
93 .filter(Repository.fork_id == repo_id)\
96 .filter(Repository.repo_id.in_(allowed_ids))\
94 .filter(Repository.repo_id.in_(allowed_ids))\
97 .count()
95 .count()
98
96
99 # json generate
97 # json generate
100 base_q = Repository.query()\
98 base_q = Repository.query()\
101 .filter(Repository.fork_id == repo_id)\
99 .filter(Repository.fork_id == repo_id)\
102 .filter(Repository.repo_id.in_(allowed_ids))\
100 .filter(Repository.repo_id.in_(allowed_ids))\
103
101
104 if search_q:
102 if search_q:
105 like_expression = u'%{}%'.format(safe_str(search_q))
103 like_expression = f'%{safe_str(search_q)}%'
106 base_q = base_q.filter(or_(
104 base_q = base_q.filter(or_(
107 Repository.repo_name.ilike(like_expression),
105 Repository.repo_name.ilike(like_expression),
108 Repository.description.ilike(like_expression),
106 Repository.description.ilike(like_expression),
109 ))
107 ))
110
108
111 forks_data_total_filtered_count = base_q.count()
109 forks_data_total_filtered_count = base_q.count()
112
110
113 sort_col = getattr(Repository, order_by, None)
111 sort_col = getattr(Repository, order_by, None)
114 if sort_col:
112 if sort_col:
115 if order_dir == 'asc':
113 if order_dir == 'asc':
116 # handle null values properly to order by NULL last
114 # handle null values properly to order by NULL last
117 if order_by in ['last_activity']:
115 if order_by in ['last_activity']:
118 sort_col = coalesce(sort_col, datetime.date.max)
116 sort_col = coalesce(sort_col, datetime.date.max)
119 sort_col = sort_col.asc()
117 sort_col = sort_col.asc()
120 else:
118 else:
121 # handle null values properly to order by NULL last
119 # handle null values properly to order by NULL last
122 if order_by in ['last_activity']:
120 if order_by in ['last_activity']:
123 sort_col = coalesce(sort_col, datetime.date.min)
121 sort_col = coalesce(sort_col, datetime.date.min)
124 sort_col = sort_col.desc()
122 sort_col = sort_col.desc()
125
123
126 base_q = base_q.order_by(sort_col)
124 base_q = base_q.order_by(sort_col)
127 base_q = base_q.offset(start).limit(limit)
125 base_q = base_q.offset(start).limit(limit)
128
126
129 fork_list = base_q.all()
127 fork_list = base_q.all()
130
128
131 def fork_actions(fork):
129 def fork_actions(fork):
132 url_link = h.route_path(
130 url_link = h.route_path(
133 'repo_compare',
131 'repo_compare',
134 repo_name=fork.repo_name,
132 repo_name=fork.repo_name,
135 source_ref_type=self.db_repo.landing_ref_type,
133 source_ref_type=self.db_repo.landing_ref_type,
136 source_ref=self.db_repo.landing_ref_name,
134 source_ref=self.db_repo.landing_ref_name,
137 target_ref_type=self.db_repo.landing_ref_type,
135 target_ref_type=self.db_repo.landing_ref_type,
138 target_ref=self.db_repo.landing_ref_name,
136 target_ref=self.db_repo.landing_ref_name,
139 _query=dict(merge=1, target_repo=f.repo_name))
137 _query=dict(merge=1, target_repo=f.repo_name))
140 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
138 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
141
139
142 def fork_name(fork):
140 def fork_name(fork):
143 return h.link_to(fork.repo_name,
141 return h.link_to(fork.repo_name,
144 h.route_path('repo_summary', repo_name=fork.repo_name))
142 h.route_path('repo_summary', repo_name=fork.repo_name))
145
143
146 forks_data = []
144 forks_data = []
147 for fork in fork_list:
145 for fork in fork_list:
148 forks_data.append({
146 forks_data.append({
149 "username": h.gravatar_with_user(self.request, fork.user.username),
147 "username": h.gravatar_with_user(self.request, fork.user.username),
150 "fork_name": fork_name(fork),
148 "fork_name": fork_name(fork),
151 "description": fork.description_safe,
149 "description": fork.description_safe,
152 "fork_date": h.age_component(fork.created_on, time_is_local=True),
150 "fork_date": h.age_component(fork.created_on, time_is_local=True),
153 "last_activity": h.format_date(fork.updated_on),
151 "last_activity": h.format_date(fork.updated_on),
154 "action": fork_actions(fork),
152 "action": fork_actions(fork),
155 })
153 })
156
154
157 data = ({
155 data = ({
158 'draw': draw,
156 'draw': draw,
159 'data': forks_data,
157 'data': forks_data,
160 'recordsTotal': forks_data_total_count,
158 'recordsTotal': forks_data_total_count,
161 'recordsFiltered': forks_data_total_filtered_count,
159 'recordsFiltered': forks_data_total_filtered_count,
162 })
160 })
163
161
164 return data
162 return data
165
163
166 @LoginRequired()
164 @LoginRequired()
167 @NotAnonymous()
165 @NotAnonymous()
168 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
166 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
169 @HasRepoPermissionAnyDecorator(
167 @HasRepoPermissionAnyDecorator(
170 'repository.read', 'repository.write', 'repository.admin')
168 'repository.read', 'repository.write', 'repository.admin')
171 def repo_fork_new(self):
169 def repo_fork_new(self):
172 c = self.load_default_context()
170 c = self.load_default_context()
173
171
174 defaults = RepoModel()._get_defaults(self.db_repo_name)
172 defaults = RepoModel()._get_defaults(self.db_repo_name)
175 # alter the description to indicate a fork
173 # alter the description to indicate a fork
176 defaults['description'] = (
174 defaults['description'] = (
177 'fork of repository: %s \n%s' % (
175 'fork of repository: {} \n{}'.format(
178 defaults['repo_name'], defaults['description']))
176 defaults['repo_name'], defaults['description']))
179 # add suffix to fork
177 # add suffix to fork
180 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
178 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
181
179
182 data = render('rhodecode:templates/forks/fork.mako',
180 data = render('rhodecode:templates/forks/fork.mako',
183 self._get_template_context(c), self.request)
181 self._get_template_context(c), self.request)
184 html = formencode.htmlfill.render(
182 html = formencode.htmlfill.render(
185 data,
183 data,
186 defaults=defaults,
184 defaults=defaults,
187 encoding="UTF-8",
185 encoding="UTF-8",
188 force_defaults=False
186 force_defaults=False
189 )
187 )
190 return Response(html)
188 return Response(html)
191
189
192 @LoginRequired()
190 @LoginRequired()
193 @NotAnonymous()
191 @NotAnonymous()
194 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
192 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
195 @HasRepoPermissionAnyDecorator(
193 @HasRepoPermissionAnyDecorator(
196 'repository.read', 'repository.write', 'repository.admin')
194 'repository.read', 'repository.write', 'repository.admin')
197 @CSRFRequired()
195 @CSRFRequired()
198 def repo_fork_create(self):
196 def repo_fork_create(self):
199 _ = self.request.translate
197 _ = self.request.translate
200 c = self.load_default_context()
198 c = self.load_default_context()
201
199
202 _form = RepoForkForm(self.request.translate,
200 _form = RepoForkForm(self.request.translate,
203 old_data={'repo_type': self.db_repo.repo_type},
201 old_data={'repo_type': self.db_repo.repo_type},
204 repo_groups=c.repo_groups_choices)()
202 repo_groups=c.repo_groups_choices)()
205 post_data = dict(self.request.POST)
203 post_data = dict(self.request.POST)
206
204
207 # forbid injecting other repo by forging a request
205 # forbid injecting other repo by forging a request
208 post_data['fork_parent_id'] = self.db_repo.repo_id
206 post_data['fork_parent_id'] = self.db_repo.repo_id
209 post_data['landing_rev'] = self.db_repo._landing_revision
207 post_data['landing_rev'] = self.db_repo._landing_revision
210
208
211 form_result = {}
209 form_result = {}
212 task_id = None
210 task_id = None
213 try:
211 try:
214 form_result = _form.to_python(post_data)
212 form_result = _form.to_python(post_data)
215 copy_permissions = form_result.get('copy_permissions')
213 copy_permissions = form_result.get('copy_permissions')
216 # create fork is done sometimes async on celery, db transaction
214 # create fork is done sometimes async on celery, db transaction
217 # management is handled there.
215 # management is handled there.
218 task = RepoModel().create_fork(
216 task = RepoModel().create_fork(
219 form_result, c.rhodecode_user.user_id)
217 form_result, c.rhodecode_user.user_id)
220
218
221 task_id = get_task_id(task)
219 task_id = get_task_id(task)
222 except formencode.Invalid as errors:
220 except formencode.Invalid as errors:
223 c.rhodecode_db_repo = self.db_repo
221 c.rhodecode_db_repo = self.db_repo
224
222
225 data = render('rhodecode:templates/forks/fork.mako',
223 data = render('rhodecode:templates/forks/fork.mako',
226 self._get_template_context(c), self.request)
224 self._get_template_context(c), self.request)
227 html = formencode.htmlfill.render(
225 html = formencode.htmlfill.render(
228 data,
226 data,
229 defaults=errors.value,
227 defaults=errors.value,
230 errors=errors.error_dict or {},
228 errors=errors.error_dict or {},
231 prefix_error=False,
229 prefix_error=False,
232 encoding="UTF-8",
230 encoding="UTF-8",
233 force_defaults=False
231 force_defaults=False
234 )
232 )
235 return Response(html)
233 return Response(html)
236 except Exception:
234 except Exception:
237 log.exception(
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 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
237 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
240 h.flash(msg, category='error')
238 h.flash(msg, category='error')
241 raise HTTPFound(h.route_path('home'))
239 raise HTTPFound(h.route_path('home'))
242
240
243 repo_name = form_result.get('repo_name_full', self.db_repo_name)
241 repo_name = form_result.get('repo_name_full', self.db_repo_name)
244
242
245 affected_user_ids = [self._rhodecode_user.user_id]
243 affected_user_ids = [self._rhodecode_user.user_id]
246 if copy_permissions:
244 if copy_permissions:
247 # permission flush is done in repo creating
245 # permission flush is done in repo creating
248 pass
246 pass
249
247
250 PermissionModel().trigger_permission_flush(affected_user_ids)
248 PermissionModel().trigger_permission_flush(affected_user_ids)
251
249
252 raise HTTPFound(
250 raise HTTPFound(
253 h.route_path('repo_creating', repo_name=repo_name,
251 h.route_path('repo_creating', repo_name=repo_name,
254 _query=dict(task_id=task_id)))
252 _query=dict(task_id=task_id)))
@@ -1,56 +1,54 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
24 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib import repo_maintenance
25 from rhodecode.lib import repo_maintenance
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 class RepoMaintenanceView(RepoAppView):
30 class RepoMaintenanceView(RepoAppView):
33 def load_default_context(self):
31 def load_default_context(self):
34 c = self._get_local_tmpl_context()
32 c = self._get_local_tmpl_context()
35 return c
33 return c
36
34
37 @LoginRequired()
35 @LoginRequired()
38 @HasRepoPermissionAnyDecorator('repository.admin')
36 @HasRepoPermissionAnyDecorator('repository.admin')
39 def repo_maintenance(self):
37 def repo_maintenance(self):
40 c = self.load_default_context()
38 c = self.load_default_context()
41 c.active = 'maintenance'
39 c.active = 'maintenance'
42 maintenance = repo_maintenance.RepoMaintenance()
40 maintenance = repo_maintenance.RepoMaintenance()
43 c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo)
41 c.executable_tasks = maintenance.get_tasks_for_repo(self.db_repo)
44 return self._get_template_context(c)
42 return self._get_template_context(c)
45
43
46 @LoginRequired()
44 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.admin')
45 @HasRepoPermissionAnyDecorator('repository.admin')
48 def repo_maintenance_execute(self):
46 def repo_maintenance_execute(self):
49 c = self.load_default_context()
47 c = self.load_default_context()
50 c.active = 'maintenance'
48 c.active = 'maintenance'
51 _ = self.request.translate
49 _ = self.request.translate
52
50
53 maintenance = repo_maintenance.RepoMaintenance()
51 maintenance = repo_maintenance.RepoMaintenance()
54 executed_types = maintenance.execute(self.db_repo)
52 executed_types = maintenance.execute(self.db_repo)
55
53
56 return executed_types
54 return executed_types
@@ -1,130 +1,128 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound
21 from pyramid.httpexceptions import HTTPFound
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
27 from rhodecode.lib import audit_logger
25 from rhodecode.lib import audit_logger
28 from rhodecode.lib.auth import (
26 from rhodecode.lib.auth import (
29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
27 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 from rhodecode.lib.utils2 import str2bool
28 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.model.db import User
29 from rhodecode.model.db import User
32 from rhodecode.model.forms import RepoPermsForm
30 from rhodecode.model.forms import RepoPermsForm
33 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
34 from rhodecode.model.permission import PermissionModel
32 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
36
34
37 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
38
36
39
37
40 class RepoSettingsPermissionsView(RepoAppView):
38 class RepoSettingsPermissionsView(RepoAppView):
41
39
42 def load_default_context(self):
40 def load_default_context(self):
43 c = self._get_local_tmpl_context()
41 c = self._get_local_tmpl_context()
44 return c
42 return c
45
43
46 @LoginRequired()
44 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.admin')
45 @HasRepoPermissionAnyDecorator('repository.admin')
48 def edit_permissions(self):
46 def edit_permissions(self):
49 _ = self.request.translate
47 _ = self.request.translate
50 c = self.load_default_context()
48 c = self.load_default_context()
51 c.active = 'permissions'
49 c.active = 'permissions'
52 if self.request.GET.get('branch_permissions'):
50 if self.request.GET.get('branch_permissions'):
53 h.flash(_('Explicitly add user or user group with write or higher '
51 h.flash(_('Explicitly add user or user group with write or higher '
54 'permission to modify their branch permissions.'),
52 'permission to modify their branch permissions.'),
55 category='notice')
53 category='notice')
56 return self._get_template_context(c)
54 return self._get_template_context(c)
57
55
58 @LoginRequired()
56 @LoginRequired()
59 @HasRepoPermissionAnyDecorator('repository.admin')
57 @HasRepoPermissionAnyDecorator('repository.admin')
60 @CSRFRequired()
58 @CSRFRequired()
61 def edit_permissions_update(self):
59 def edit_permissions_update(self):
62 _ = self.request.translate
60 _ = self.request.translate
63 c = self.load_default_context()
61 c = self.load_default_context()
64 c.active = 'permissions'
62 c.active = 'permissions'
65 data = self.request.POST
63 data = self.request.POST
66 # store private flag outside of HTML to verify if we can modify
64 # store private flag outside of HTML to verify if we can modify
67 # default user permissions, prevents submission of FAKE post data
65 # default user permissions, prevents submission of FAKE post data
68 # into the form for private repos
66 # into the form for private repos
69 data['repo_private'] = self.db_repo.private
67 data['repo_private'] = self.db_repo.private
70 form = RepoPermsForm(self.request.translate)().to_python(data)
68 form = RepoPermsForm(self.request.translate)().to_python(data)
71 changes = RepoModel().update_permissions(
69 changes = RepoModel().update_permissions(
72 self.db_repo_name, form['perm_additions'], form['perm_updates'],
70 self.db_repo_name, form['perm_additions'], form['perm_updates'],
73 form['perm_deletions'])
71 form['perm_deletions'])
74
72
75 action_data = {
73 action_data = {
76 'added': changes['added'],
74 'added': changes['added'],
77 'updated': changes['updated'],
75 'updated': changes['updated'],
78 'deleted': changes['deleted'],
76 'deleted': changes['deleted'],
79 }
77 }
80 audit_logger.store_web(
78 audit_logger.store_web(
81 'repo.edit.permissions', action_data=action_data,
79 'repo.edit.permissions', action_data=action_data,
82 user=self._rhodecode_user, repo=self.db_repo)
80 user=self._rhodecode_user, repo=self.db_repo)
83
81
84 Session().commit()
82 Session().commit()
85 h.flash(_('Repository access permissions updated'), category='success')
83 h.flash(_('Repository access permissions updated'), category='success')
86
84
87 affected_user_ids = None
85 affected_user_ids = None
88 if changes.get('default_user_changed', False):
86 if changes.get('default_user_changed', False):
89 # if we change the default user, we need to flush everyone permissions
87 # if we change the default user, we need to flush everyone permissions
90 affected_user_ids = User.get_all_user_ids()
88 affected_user_ids = User.get_all_user_ids()
91 PermissionModel().flush_user_permission_caches(
89 PermissionModel().flush_user_permission_caches(
92 changes, affected_user_ids=affected_user_ids)
90 changes, affected_user_ids=affected_user_ids)
93
91
94 raise HTTPFound(
92 raise HTTPFound(
95 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
93 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
96
94
97 @LoginRequired()
95 @LoginRequired()
98 @HasRepoPermissionAnyDecorator('repository.admin')
96 @HasRepoPermissionAnyDecorator('repository.admin')
99 @CSRFRequired()
97 @CSRFRequired()
100 def edit_permissions_set_private_repo(self):
98 def edit_permissions_set_private_repo(self):
101 _ = self.request.translate
99 _ = self.request.translate
102 self.load_default_context()
100 self.load_default_context()
103
101
104 private_flag = str2bool(self.request.POST.get('private'))
102 private_flag = str2bool(self.request.POST.get('private'))
105
103
106 try:
104 try:
107 repo = RepoModel().get(self.db_repo.repo_id)
105 repo = RepoModel().get(self.db_repo.repo_id)
108 repo.private = private_flag
106 repo.private = private_flag
109 Session().add(repo)
107 Session().add(repo)
110 RepoModel().grant_user_permission(
108 RepoModel().grant_user_permission(
111 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
109 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
112 )
110 )
113
111
114 Session().commit()
112 Session().commit()
115
113
116 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
114 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
117 category='success')
115 category='success')
118 # NOTE(dan): we change repo private mode we need to notify all USERS
116 # NOTE(dan): we change repo private mode we need to notify all USERS
119 affected_user_ids = User.get_all_user_ids()
117 affected_user_ids = User.get_all_user_ids()
120 PermissionModel().trigger_permission_flush(affected_user_ids)
118 PermissionModel().trigger_permission_flush(affected_user_ids)
121
119
122 except Exception:
120 except Exception:
123 log.exception("Exception during update of repository")
121 log.exception("Exception during update of repository")
124 h.flash(_('Error occurred during update of repository {}').format(
122 h.flash(_('Error occurred during update of repository {}').format(
125 self.db_repo_name), category='error')
123 self.db_repo_name), category='error')
126
124
127 return {
125 return {
128 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
126 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
129 'private': private_flag
127 'private': private_flag
130 }
128 }
@@ -1,1876 +1,1874 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import collections
20 import collections
23
21
24 import formencode
22 import formencode
25 import formencode.htmlfill
23 import formencode.htmlfill
26 import peppercorn
24 import peppercorn
27 from pyramid.httpexceptions import (
25 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
27
30 from pyramid.renderers import render
28 from pyramid.renderers import render
31
29
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
30 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
31
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
33 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
35 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib import ext_json
36 from rhodecode.lib import ext_json
39 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
39 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
43 from rhodecode.lib.vcs.backends.base import (
41 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
47 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
49 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
56
54
57 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
58
56
59
57
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
59
62 def load_default_context(self):
60 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
65 c.renderer = 'plain'
68 return c
66 return c
69
67
70 def _get_pull_requests_list(
68 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
72
70
73 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
77
75
78 # pagination
76 # pagination
79
77
80 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name,
80 repo_name,
83 search_q=search_q, statuses=statuses,
81 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name,
84 repo_name,
87 search_q=search_q, statuses=statuses)
85 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, self._rhodecode_user.user_id,
88 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
89 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, self._rhodecode_user.user_id,
92 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
93 search_q=search_q, statuses=statuses)
96 else:
94 else:
97 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
101 opened_by=opened_by)
104
102
105 data = []
103 data = []
106 comments_model = CommentsModel()
104 comments_model = CommentsModel()
107 for pr in pull_requests:
105 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
106 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr,
107 self.db_repo.repo_id, pull_request=pr,
110 include_drafts=False, count_only=True)
108 include_drafts=False, count_only=True)
111
109
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
112 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
114 my_review_status = statuses[0][1].status
117
115
118 data.append({
116 data.append({
119 'name': _render('pullrequest_name',
117 'name': _render('pullrequest_name',
120 pr.pull_request_id, pr.pull_request_state,
118 pr.pull_request_id, pr.pull_request_state,
121 pr.work_in_progress, pr.target_repo.repo_name,
119 pr.work_in_progress, pr.target_repo.repo_name,
122 short=True),
120 short=True),
123 'name_raw': pr.pull_request_id,
121 'name_raw': pr.pull_request_id,
124 'status': _render('pullrequest_status',
122 'status': _render('pullrequest_status',
125 pr.calculated_review_status()),
123 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
124 'my_status': _render('pullrequest_status',
127 my_review_status),
125 my_review_status),
128 'title': _render('pullrequest_title', pr.title, pr.description),
126 'title': _render('pullrequest_title', pr.title, pr.description),
129 'description': h.escape(pr.description),
127 'description': h.escape(pr.description),
130 'updated_on': _render('pullrequest_updated_on',
128 'updated_on': _render('pullrequest_updated_on',
131 h.datetime_to_time(pr.updated_on),
129 h.datetime_to_time(pr.updated_on),
132 pr.versions_count),
130 pr.versions_count),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
131 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 'created_on': _render('pullrequest_updated_on',
132 'created_on': _render('pullrequest_updated_on',
135 h.datetime_to_time(pr.created_on)),
133 h.datetime_to_time(pr.created_on)),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
134 'created_on_raw': h.datetime_to_time(pr.created_on),
137 'state': pr.pull_request_state,
135 'state': pr.pull_request_state,
138 'author': _render('pullrequest_author',
136 'author': _render('pullrequest_author',
139 pr.author.full_contact, ),
137 pr.author.full_contact, ),
140 'author_raw': pr.author.full_name,
138 'author_raw': pr.author.full_name,
141 'comments': _render('pullrequest_comments', comments_count),
139 'comments': _render('pullrequest_comments', comments_count),
142 'comments_raw': comments_count,
140 'comments_raw': comments_count,
143 'closed': pr.is_closed(),
141 'closed': pr.is_closed(),
144 })
142 })
145
143
146 data = ({
144 data = ({
147 'draw': draw,
145 'draw': draw,
148 'data': data,
146 'data': data,
149 'recordsTotal': pull_requests_total_count,
147 'recordsTotal': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
148 'recordsFiltered': pull_requests_total_count,
151 })
149 })
152 return data
150 return data
153
151
154 @LoginRequired()
152 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
153 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
154 'repository.read', 'repository.write', 'repository.admin')
157 def pull_request_list(self):
155 def pull_request_list(self):
158 c = self.load_default_context()
156 c = self.load_default_context()
159
157
160 req_get = self.request.GET
158 req_get = self.request.GET
161 c.source = str2bool(req_get.get('source'))
159 c.source = str2bool(req_get.get('source'))
162 c.closed = str2bool(req_get.get('closed'))
160 c.closed = str2bool(req_get.get('closed'))
163 c.my = str2bool(req_get.get('my'))
161 c.my = str2bool(req_get.get('my'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
162 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
163 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166
164
167 c.active = 'open'
165 c.active = 'open'
168 if c.my:
166 if c.my:
169 c.active = 'my'
167 c.active = 'my'
170 if c.closed:
168 if c.closed:
171 c.active = 'closed'
169 c.active = 'closed'
172 if c.awaiting_review and not c.source:
170 if c.awaiting_review and not c.source:
173 c.active = 'awaiting'
171 c.active = 'awaiting'
174 if c.source and not c.awaiting_review:
172 if c.source and not c.awaiting_review:
175 c.active = 'source'
173 c.active = 'source'
176 if c.awaiting_my_review:
174 if c.awaiting_my_review:
177 c.active = 'awaiting_my'
175 c.active = 'awaiting_my'
178
176
179 return self._get_template_context(c)
177 return self._get_template_context(c)
180
178
181 @LoginRequired()
179 @LoginRequired()
182 @HasRepoPermissionAnyDecorator(
180 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
181 'repository.read', 'repository.write', 'repository.admin')
184 def pull_request_list_data(self):
182 def pull_request_list_data(self):
185 self.load_default_context()
183 self.load_default_context()
186
184
187 # additional filters
185 # additional filters
188 req_get = self.request.GET
186 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
187 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
188 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
189 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
192
195 filter_type = 'awaiting_review' if awaiting_review \
193 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
194 else 'awaiting_my_review' if awaiting_my_review \
197 else None
195 else None
198
196
199 opened_by = None
197 opened_by = None
200 if my:
198 if my:
201 opened_by = [self._rhodecode_user.user_id]
199 opened_by = [self._rhodecode_user.user_id]
202
200
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
202 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
203 statuses = [PullRequest.STATUS_CLOSED]
206
204
207 data = self._get_pull_requests_list(
205 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
206 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
208
211 return data
209 return data
212
210
213 def _is_diff_cache_enabled(self, target_repo):
211 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
212 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
213 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
214 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
215 return caching_enabled
218
216
219 def _get_diffset(self, source_repo_name, source_repo,
217 def _get_diffset(self, source_repo_name, source_repo,
220 ancestor_commit,
218 ancestor_commit,
221 source_ref_id, target_ref_id,
219 source_ref_id, target_ref_id,
222 target_commit, source_commit, diff_limit, file_limit,
220 target_commit, source_commit, diff_limit, file_limit,
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224
222
225 target_commit_final = target_commit
223 target_commit_final = target_commit
226 source_commit_final = source_commit
224 source_commit_final = source_commit
227
225
228 if use_ancestor:
226 if use_ancestor:
229 # we might want to not use it for versions
227 # we might want to not use it for versions
230 target_ref_id = ancestor_commit.raw_id
228 target_ref_id = ancestor_commit.raw_id
231 target_commit_final = ancestor_commit
229 target_commit_final = ancestor_commit
232
230
233 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
234 source_repo, source_ref_id, target_ref_id,
232 source_repo, source_ref_id, target_ref_id,
235 hide_whitespace_changes, diff_context)
233 hide_whitespace_changes, diff_context)
236
234
237 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
235 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
238 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
239
237
240 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
241
239
242 diffset = codeblocks.DiffSet(
240 diffset = codeblocks.DiffSet(
243 repo_name=self.db_repo_name,
241 repo_name=self.db_repo_name,
244 source_repo_name=source_repo_name,
242 source_repo_name=source_repo_name,
245 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
243 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
244 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 )
245 )
248 diffset = self.path_filter.render_patchset_filtered(
246 diffset = self.path_filter.render_patchset_filtered(
249 diffset, _parsed, target_ref_id, source_ref_id)
247 diffset, _parsed, target_ref_id, source_ref_id)
250
248
251 return diffset
249 return diffset
252
250
253 def _get_range_diffset(self, source_scm, source_repo,
251 def _get_range_diffset(self, source_scm, source_repo,
254 commit1, commit2, diff_limit, file_limit,
252 commit1, commit2, diff_limit, file_limit,
255 fulldiff, hide_whitespace_changes, diff_context):
253 fulldiff, hide_whitespace_changes, diff_context):
256 vcs_diff = source_scm.get_diff(
254 vcs_diff = source_scm.get_diff(
257 commit1, commit2,
255 commit1, commit2,
258 ignore_whitespace=hide_whitespace_changes,
256 ignore_whitespace=hide_whitespace_changes,
259 context=diff_context)
257 context=diff_context)
260
258
261 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
259 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
262 diff_limit=diff_limit,
260 diff_limit=diff_limit,
263 file_limit=file_limit, show_full_diff=fulldiff)
261 file_limit=file_limit, show_full_diff=fulldiff)
264
262
265 _parsed = diff_processor.prepare()
263 _parsed = diff_processor.prepare()
266
264
267 diffset = codeblocks.DiffSet(
265 diffset = codeblocks.DiffSet(
268 repo_name=source_repo.repo_name,
266 repo_name=source_repo.repo_name,
269 source_node_getter=codeblocks.diffset_node_getter(commit1),
267 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 target_node_getter=codeblocks.diffset_node_getter(commit2))
268 target_node_getter=codeblocks.diffset_node_getter(commit2))
271
269
272 diffset = self.path_filter.render_patchset_filtered(
270 diffset = self.path_filter.render_patchset_filtered(
273 diffset, _parsed, commit1.raw_id, commit2.raw_id)
271 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274
272
275 return diffset
273 return diffset
276
274
277 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
275 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 comments_model = CommentsModel()
276 comments_model = CommentsModel()
279
277
280 # GENERAL COMMENTS with versions #
278 # GENERAL COMMENTS with versions #
281 q = comments_model._all_general_comments_of_pull_request(pull_request)
279 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = q.order_by(ChangesetComment.comment_id.asc())
280 q = q.order_by(ChangesetComment.comment_id.asc())
283 if not include_drafts:
281 if not include_drafts:
284 q = q.filter(ChangesetComment.draft == false())
282 q = q.filter(ChangesetComment.draft == false())
285 general_comments = q
283 general_comments = q
286
284
287 # pick comments we want to render at current version
285 # pick comments we want to render at current version
288 c.comment_versions = comments_model.aggregate_comments(
286 c.comment_versions = comments_model.aggregate_comments(
289 general_comments, versions, c.at_version_num)
287 general_comments, versions, c.at_version_num)
290
288
291 # INLINE COMMENTS with versions #
289 # INLINE COMMENTS with versions #
292 q = comments_model._all_inline_comments_of_pull_request(pull_request)
290 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = q.order_by(ChangesetComment.comment_id.asc())
291 q = q.order_by(ChangesetComment.comment_id.asc())
294 if not include_drafts:
292 if not include_drafts:
295 q = q.filter(ChangesetComment.draft == false())
293 q = q.filter(ChangesetComment.draft == false())
296 inline_comments = q
294 inline_comments = q
297
295
298 c.inline_versions = comments_model.aggregate_comments(
296 c.inline_versions = comments_model.aggregate_comments(
299 inline_comments, versions, c.at_version_num, inline=True)
297 inline_comments, versions, c.at_version_num, inline=True)
300
298
301 # Comments inline+general
299 # Comments inline+general
302 if c.at_version:
300 if c.at_version:
303 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
301 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.comments = c.comment_versions[c.at_version_num]['display']
302 c.comments = c.comment_versions[c.at_version_num]['display']
305 else:
303 else:
306 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.comments = c.comment_versions[c.at_version_num]['until']
305 c.comments = c.comment_versions[c.at_version_num]['until']
308
306
309 return general_comments, inline_comments
307 return general_comments, inline_comments
310
308
311 @LoginRequired()
309 @LoginRequired()
312 @HasRepoPermissionAnyDecorator(
310 @HasRepoPermissionAnyDecorator(
313 'repository.read', 'repository.write', 'repository.admin')
311 'repository.read', 'repository.write', 'repository.admin')
314 def pull_request_show(self):
312 def pull_request_show(self):
315 _ = self.request.translate
313 _ = self.request.translate
316 c = self.load_default_context()
314 c = self.load_default_context()
317
315
318 pull_request = PullRequest.get_or_404(
316 pull_request = PullRequest.get_or_404(
319 self.request.matchdict['pull_request_id'])
317 self.request.matchdict['pull_request_id'])
320 pull_request_id = pull_request.pull_request_id
318 pull_request_id = pull_request.pull_request_id
321
319
322 c.state_progressing = pull_request.is_state_changing()
320 c.state_progressing = pull_request.is_state_changing()
323 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324
322
325 _new_state = {
323 _new_state = {
326 'created': PullRequest.STATE_CREATED,
324 'created': PullRequest.STATE_CREATED,
327 }.get(self.request.GET.get('force_state'))
325 }.get(self.request.GET.get('force_state'))
328 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
326 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329
327
330 if can_force_state and _new_state:
328 if can_force_state and _new_state:
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 h.flash(
330 h.flash(
333 _('Pull Request state was force changed to `{}`').format(_new_state),
331 _('Pull Request state was force changed to `{}`').format(_new_state),
334 category='success')
332 category='success')
335 Session().commit()
333 Session().commit()
336
334
337 raise HTTPFound(h.route_path(
335 raise HTTPFound(h.route_path(
338 'pullrequest_show', repo_name=self.db_repo_name,
336 'pullrequest_show', repo_name=self.db_repo_name,
339 pull_request_id=pull_request_id))
337 pull_request_id=pull_request_id))
340
338
341 version = self.request.GET.get('version')
339 version = self.request.GET.get('version')
342 from_version = self.request.GET.get('from_version') or version
340 from_version = self.request.GET.get('from_version') or version
343 merge_checks = self.request.GET.get('merge_checks')
341 merge_checks = self.request.GET.get('merge_checks')
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347
345
348 # fetch global flags of ignore ws or context lines
346 # fetch global flags of ignore ws or context lines
349 diff_context = diffs.get_diff_context(self.request)
347 diff_context = diffs.get_diff_context(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351
349
352 (pull_request_latest,
350 (pull_request_latest,
353 pull_request_at_ver,
351 pull_request_at_ver,
354 pull_request_display_obj,
352 pull_request_display_obj,
355 at_version) = PullRequestModel().get_pr_version(
353 at_version) = PullRequestModel().get_pr_version(
356 pull_request_id, version=version)
354 pull_request_id, version=version)
357
355
358 pr_closed = pull_request_latest.is_closed()
356 pr_closed = pull_request_latest.is_closed()
359
357
360 if pr_closed and (version or from_version):
358 if pr_closed and (version or from_version):
361 # not allow to browse versions for closed PR
359 # not allow to browse versions for closed PR
362 raise HTTPFound(h.route_path(
360 raise HTTPFound(h.route_path(
363 'pullrequest_show', repo_name=self.db_repo_name,
361 'pullrequest_show', repo_name=self.db_repo_name,
364 pull_request_id=pull_request_id))
362 pull_request_id=pull_request_id))
365
363
366 versions = pull_request_display_obj.versions()
364 versions = pull_request_display_obj.versions()
367
365
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
366 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369
367
370 # used to store per-commit range diffs
368 # used to store per-commit range diffs
371 c.changes = collections.OrderedDict()
369 c.changes = collections.OrderedDict()
372
370
373 c.at_version = at_version
371 c.at_version = at_version
374 c.at_version_num = (at_version
372 c.at_version_num = (at_version
375 if at_version and at_version != PullRequest.LATEST_VER
373 if at_version and at_version != PullRequest.LATEST_VER
376 else None)
374 else None)
377
375
378 c.at_version_index = ChangesetComment.get_index_from_version(
376 c.at_version_index = ChangesetComment.get_index_from_version(
379 c.at_version_num, versions)
377 c.at_version_num, versions)
380
378
381 (prev_pull_request_latest,
379 (prev_pull_request_latest,
382 prev_pull_request_at_ver,
380 prev_pull_request_at_ver,
383 prev_pull_request_display_obj,
381 prev_pull_request_display_obj,
384 prev_at_version) = PullRequestModel().get_pr_version(
382 prev_at_version) = PullRequestModel().get_pr_version(
385 pull_request_id, version=from_version)
383 pull_request_id, version=from_version)
386
384
387 c.from_version = prev_at_version
385 c.from_version = prev_at_version
388 c.from_version_num = (prev_at_version
386 c.from_version_num = (prev_at_version
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
387 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 else None)
388 else None)
391 c.from_version_index = ChangesetComment.get_index_from_version(
389 c.from_version_index = ChangesetComment.get_index_from_version(
392 c.from_version_num, versions)
390 c.from_version_num, versions)
393
391
394 # define if we're in COMPARE mode or VIEW at version mode
392 # define if we're in COMPARE mode or VIEW at version mode
395 compare = at_version != prev_at_version
393 compare = at_version != prev_at_version
396
394
397 # pull_requests repo_name we opened it against
395 # pull_requests repo_name we opened it against
398 # ie. target_repo must match
396 # ie. target_repo must match
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
397 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 log.warning('Mismatch between the current repo: %s, and target %s',
398 log.warning('Mismatch between the current repo: %s, and target %s',
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
399 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 raise HTTPNotFound()
400 raise HTTPNotFound()
403
401
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
402 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405
403
406 c.pull_request = pull_request_display_obj
404 c.pull_request = pull_request_display_obj
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
405 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 c.pull_request_latest = pull_request_latest
406 c.pull_request_latest = pull_request_latest
409
407
410 # inject latest version
408 # inject latest version
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
409 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 c.versions = versions + [latest_ver]
410 c.versions = versions + [latest_ver]
413
411
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
412 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 c.allowed_to_change_status = False
413 c.allowed_to_change_status = False
416 c.allowed_to_update = False
414 c.allowed_to_update = False
417 c.allowed_to_merge = False
415 c.allowed_to_merge = False
418 c.allowed_to_delete = False
416 c.allowed_to_delete = False
419 c.allowed_to_comment = False
417 c.allowed_to_comment = False
420 c.allowed_to_close = False
418 c.allowed_to_close = False
421 else:
419 else:
422 can_change_status = PullRequestModel().check_user_change_status(
420 can_change_status = PullRequestModel().check_user_change_status(
423 pull_request_at_ver, self._rhodecode_user)
421 pull_request_at_ver, self._rhodecode_user)
424 c.allowed_to_change_status = can_change_status and not pr_closed
422 c.allowed_to_change_status = can_change_status and not pr_closed
425
423
426 c.allowed_to_update = PullRequestModel().check_user_update(
424 c.allowed_to_update = PullRequestModel().check_user_update(
427 pull_request_latest, self._rhodecode_user) and not pr_closed
425 pull_request_latest, self._rhodecode_user) and not pr_closed
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
426 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 pull_request_latest, self._rhodecode_user) and not pr_closed
427 pull_request_latest, self._rhodecode_user) and not pr_closed
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
428 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 pull_request_latest, self._rhodecode_user) and not pr_closed
429 pull_request_latest, self._rhodecode_user) and not pr_closed
432 c.allowed_to_comment = not pr_closed
430 c.allowed_to_comment = not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
431 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434
432
435 c.forbid_adding_reviewers = False
433 c.forbid_adding_reviewers = False
436
434
437 if pull_request_latest.reviewer_data and \
435 if pull_request_latest.reviewer_data and \
438 'rules' in pull_request_latest.reviewer_data:
436 'rules' in pull_request_latest.reviewer_data:
439 rules = pull_request_latest.reviewer_data['rules'] or {}
437 rules = pull_request_latest.reviewer_data['rules'] or {}
440 try:
438 try:
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
439 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 except Exception:
440 except Exception:
443 pass
441 pass
444
442
445 # check merge capabilities
443 # check merge capabilities
446 _merge_check = MergeCheck.validate(
444 _merge_check = MergeCheck.validate(
447 pull_request_latest, auth_user=self._rhodecode_user,
445 pull_request_latest, auth_user=self._rhodecode_user,
448 translator=self.request.translate,
446 translator=self.request.translate,
449 force_shadow_repo_refresh=force_refresh)
447 force_shadow_repo_refresh=force_refresh)
450
448
451 c.pr_merge_errors = _merge_check.error_details
449 c.pr_merge_errors = _merge_check.error_details
452 c.pr_merge_possible = not _merge_check.failed
450 c.pr_merge_possible = not _merge_check.failed
453 c.pr_merge_message = _merge_check.merge_msg
451 c.pr_merge_message = _merge_check.merge_msg
454 c.pr_merge_source_commit = _merge_check.source_commit
452 c.pr_merge_source_commit = _merge_check.source_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
453 c.pr_merge_target_commit = _merge_check.target_commit
456
454
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 pull_request_latest, translator=self.request.translate)
456 pull_request_latest, translator=self.request.translate)
459
457
460 c.pull_request_review_status = _merge_check.review_status
458 c.pull_request_review_status = _merge_check.review_status
461 if merge_checks:
459 if merge_checks:
462 self.request.override_renderer = \
460 self.request.override_renderer = \
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 return self._get_template_context(c)
462 return self._get_template_context(c)
465
463
466 c.reviewers_count = pull_request.reviewers_count
464 c.reviewers_count = pull_request.reviewers_count
467 c.observers_count = pull_request.observers_count
465 c.observers_count = pull_request.observers_count
468
466
469 # reviewers and statuses
467 # reviewers and statuses
470 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
468 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473
471
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 member_reviewer = h.reviewer_as_json(
473 member_reviewer = h.reviewer_as_json(
476 member, reasons=reasons, mandatory=mandatory,
474 member, reasons=reasons, mandatory=mandatory,
477 role=review_obj.role,
475 role=review_obj.role,
478 user_group=review_obj.rule_user_group_data()
476 user_group=review_obj.rule_user_group_data()
479 )
477 )
480
478
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 member_reviewer['review_status'] = current_review_status
480 member_reviewer['review_status'] = current_review_status
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486
484
487 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
485 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
488
486
489 for observer_obj, member in pull_request_at_ver.observers():
487 for observer_obj, member in pull_request_at_ver.observers():
490 member_observer = h.reviewer_as_json(
488 member_observer = h.reviewer_as_json(
491 member, reasons=[], mandatory=False,
489 member, reasons=[], mandatory=False,
492 role=observer_obj.role,
490 role=observer_obj.role,
493 user_group=observer_obj.rule_user_group_data()
491 user_group=observer_obj.rule_user_group_data()
494 )
492 )
495 member_observer['allowed_to_update'] = c.allowed_to_update
493 member_observer['allowed_to_update'] = c.allowed_to_update
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497
495
498 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
496 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
499
497
500 general_comments, inline_comments = \
498 general_comments, inline_comments = \
501 self.register_comments_vars(c, pull_request_latest, versions)
499 self.register_comments_vars(c, pull_request_latest, versions)
502
500
503 # TODOs
501 # TODOs
504 c.unresolved_comments = CommentsModel() \
502 c.unresolved_comments = CommentsModel() \
505 .get_pull_request_unresolved_todos(pull_request_latest)
503 .get_pull_request_unresolved_todos(pull_request_latest)
506 c.resolved_comments = CommentsModel() \
504 c.resolved_comments = CommentsModel() \
507 .get_pull_request_resolved_todos(pull_request_latest)
505 .get_pull_request_resolved_todos(pull_request_latest)
508
506
509 # Drafts
507 # Drafts
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
508 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 self._rhodecode_db_user.user_id,
509 self._rhodecode_db_user.user_id,
512 pull_request_latest)
510 pull_request_latest)
513
511
514 # if we use version, then do not show later comments
512 # if we use version, then do not show later comments
515 # than current version
513 # than current version
516 display_inline_comments = collections.defaultdict(
514 display_inline_comments = collections.defaultdict(
517 lambda: collections.defaultdict(list))
515 lambda: collections.defaultdict(list))
518 for co in inline_comments:
516 for co in inline_comments:
519 if c.at_version_num:
517 if c.at_version_num:
520 # pick comments that are at least UPTO given version, so we
518 # pick comments that are at least UPTO given version, so we
521 # don't render comments for higher version
519 # don't render comments for higher version
522 should_render = co.pull_request_version_id and \
520 should_render = co.pull_request_version_id and \
523 co.pull_request_version_id <= c.at_version_num
521 co.pull_request_version_id <= c.at_version_num
524 else:
522 else:
525 # showing all, for 'latest'
523 # showing all, for 'latest'
526 should_render = True
524 should_render = True
527
525
528 if should_render:
526 if should_render:
529 display_inline_comments[co.f_path][co.line_no].append(co)
527 display_inline_comments[co.f_path][co.line_no].append(co)
530
528
531 # load diff data into template context, if we use compare mode then
529 # load diff data into template context, if we use compare mode then
532 # diff is calculated based on changes between versions of PR
530 # diff is calculated based on changes between versions of PR
533
531
534 source_repo = pull_request_at_ver.source_repo
532 source_repo = pull_request_at_ver.source_repo
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
533 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536
534
537 target_repo = pull_request_at_ver.target_repo
535 target_repo = pull_request_at_ver.target_repo
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
536 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539
537
540 if compare:
538 if compare:
541 # in compare switch the diff base to latest commit from prev version
539 # in compare switch the diff base to latest commit from prev version
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
540 target_ref_id = prev_pull_request_display_obj.revisions[0]
543
541
544 # despite opening commits for bookmarks/branches/tags, we always
542 # despite opening commits for bookmarks/branches/tags, we always
545 # convert this to rev to prevent changes after bookmark or branch change
543 # convert this to rev to prevent changes after bookmark or branch change
546 c.source_ref_type = 'rev'
544 c.source_ref_type = 'rev'
547 c.source_ref = source_ref_id
545 c.source_ref = source_ref_id
548
546
549 c.target_ref_type = 'rev'
547 c.target_ref_type = 'rev'
550 c.target_ref = target_ref_id
548 c.target_ref = target_ref_id
551
549
552 c.source_repo = source_repo
550 c.source_repo = source_repo
553 c.target_repo = target_repo
551 c.target_repo = target_repo
554
552
555 c.commit_ranges = []
553 c.commit_ranges = []
556 source_commit = EmptyCommit()
554 source_commit = EmptyCommit()
557 target_commit = EmptyCommit()
555 target_commit = EmptyCommit()
558 c.missing_requirements = False
556 c.missing_requirements = False
559
557
560 source_scm = source_repo.scm_instance()
558 source_scm = source_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
559 target_scm = target_repo.scm_instance()
562
560
563 shadow_scm = None
561 shadow_scm = None
564 try:
562 try:
565 shadow_scm = pull_request_latest.get_shadow_repo()
563 shadow_scm = pull_request_latest.get_shadow_repo()
566 except Exception:
564 except Exception:
567 log.debug('Failed to get shadow repo', exc_info=True)
565 log.debug('Failed to get shadow repo', exc_info=True)
568 # try first the existing source_repo, and then shadow
566 # try first the existing source_repo, and then shadow
569 # repo if we can obtain one
567 # repo if we can obtain one
570 commits_source_repo = source_scm
568 commits_source_repo = source_scm
571 if shadow_scm:
569 if shadow_scm:
572 commits_source_repo = shadow_scm
570 commits_source_repo = shadow_scm
573
571
574 c.commits_source_repo = commits_source_repo
572 c.commits_source_repo = commits_source_repo
575 c.ancestor = None # set it to None, to hide it from PR view
573 c.ancestor = None # set it to None, to hide it from PR view
576
574
577 # empty version means latest, so we keep this to prevent
575 # empty version means latest, so we keep this to prevent
578 # double caching
576 # double caching
579 version_normalized = version or PullRequest.LATEST_VER
577 version_normalized = version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
578 from_version_normalized = from_version or PullRequest.LATEST_VER
581
579
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
580 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_file_path = diff_cache_exist(
581 cache_file_path = diff_cache_exist(
584 cache_path, 'pull_request', pull_request_id, version_normalized,
582 cache_path, 'pull_request', pull_request_id, version_normalized,
585 from_version_normalized, source_ref_id, target_ref_id,
583 from_version_normalized, source_ref_id, target_ref_id,
586 hide_whitespace_changes, diff_context, c.fulldiff)
584 hide_whitespace_changes, diff_context, c.fulldiff)
587
585
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
586 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 force_recache = self.get_recache_flag()
587 force_recache = self.get_recache_flag()
590
588
591 cached_diff = None
589 cached_diff = None
592 if caching_enabled:
590 if caching_enabled:
593 cached_diff = load_cached_diff(cache_file_path)
591 cached_diff = load_cached_diff(cache_file_path)
594
592
595 has_proper_commit_cache = (
593 has_proper_commit_cache = (
596 cached_diff and cached_diff.get('commits')
594 cached_diff and cached_diff.get('commits')
597 and len(cached_diff.get('commits', [])) == 5
595 and len(cached_diff.get('commits', [])) == 5
598 and cached_diff.get('commits')[0]
596 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[3])
597 and cached_diff.get('commits')[3])
600
598
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
599 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 diff_commit_cache = \
600 diff_commit_cache = \
603 (ancestor_commit, commit_cache, missing_requirements,
601 (ancestor_commit, commit_cache, missing_requirements,
604 source_commit, target_commit) = cached_diff['commits']
602 source_commit, target_commit) = cached_diff['commits']
605 else:
603 else:
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
604 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # merge errors resulting in potentially hidden commits in the shadow repo.
605 # merge errors resulting in potentially hidden commits in the shadow repo.
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
606 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 and _merge_check.merge_response
607 and _merge_check.merge_response
610 maybe_unreachable = maybe_unreachable \
608 maybe_unreachable = maybe_unreachable \
611 and _merge_check.merge_response.metadata.get('unresolved_files')
609 and _merge_check.merge_response.metadata.get('unresolved_files')
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
610 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 diff_commit_cache = \
611 diff_commit_cache = \
614 (ancestor_commit, commit_cache, missing_requirements,
612 (ancestor_commit, commit_cache, missing_requirements,
615 source_commit, target_commit) = self.get_commits(
613 source_commit, target_commit) = self.get_commits(
616 commits_source_repo,
614 commits_source_repo,
617 pull_request_at_ver,
615 pull_request_at_ver,
618 source_commit,
616 source_commit,
619 source_ref_id,
617 source_ref_id,
620 source_scm,
618 source_scm,
621 target_commit,
619 target_commit,
622 target_ref_id,
620 target_ref_id,
623 target_scm,
621 target_scm,
624 maybe_unreachable=maybe_unreachable)
622 maybe_unreachable=maybe_unreachable)
625
623
626 # register our commit range
624 # register our commit range
627 for comm in commit_cache.values():
625 for comm in commit_cache.values():
628 c.commit_ranges.append(comm)
626 c.commit_ranges.append(comm)
629
627
630 c.missing_requirements = missing_requirements
628 c.missing_requirements = missing_requirements
631 c.ancestor_commit = ancestor_commit
629 c.ancestor_commit = ancestor_commit
632 c.statuses = source_repo.statuses(
630 c.statuses = source_repo.statuses(
633 [x.raw_id for x in c.commit_ranges])
631 [x.raw_id for x in c.commit_ranges])
634
632
635 # auto collapse if we have more than limit
633 # auto collapse if we have more than limit
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
634 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
635 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.compare_mode = compare
636 c.compare_mode = compare
639
637
640 # diff_limit is the old behavior, will cut off the whole diff
638 # diff_limit is the old behavior, will cut off the whole diff
641 # if the limit is applied otherwise will just hide the
639 # if the limit is applied otherwise will just hide the
642 # big files from the front-end
640 # big files from the front-end
643 diff_limit = c.visual.cut_off_limit_diff
641 diff_limit = c.visual.cut_off_limit_diff
644 file_limit = c.visual.cut_off_limit_file
642 file_limit = c.visual.cut_off_limit_file
645
643
646 c.missing_commits = False
644 c.missing_commits = False
647 if (c.missing_requirements
645 if (c.missing_requirements
648 or isinstance(source_commit, EmptyCommit)
646 or isinstance(source_commit, EmptyCommit)
649 or source_commit == target_commit):
647 or source_commit == target_commit):
650
648
651 c.missing_commits = True
649 c.missing_commits = True
652 else:
650 else:
653 c.inline_comments = display_inline_comments
651 c.inline_comments = display_inline_comments
654
652
655 use_ancestor = True
653 use_ancestor = True
656 if from_version_normalized != version_normalized:
654 if from_version_normalized != version_normalized:
657 use_ancestor = False
655 use_ancestor = False
658
656
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
657 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 if not force_recache and has_proper_diff_cache:
658 if not force_recache and has_proper_diff_cache:
661 c.diffset = cached_diff['diff']
659 c.diffset = cached_diff['diff']
662 else:
660 else:
663 try:
661 try:
664 c.diffset = self._get_diffset(
662 c.diffset = self._get_diffset(
665 c.source_repo.repo_name, commits_source_repo,
663 c.source_repo.repo_name, commits_source_repo,
666 c.ancestor_commit,
664 c.ancestor_commit,
667 source_ref_id, target_ref_id,
665 source_ref_id, target_ref_id,
668 target_commit, source_commit,
666 target_commit, source_commit,
669 diff_limit, file_limit, c.fulldiff,
667 diff_limit, file_limit, c.fulldiff,
670 hide_whitespace_changes, diff_context,
668 hide_whitespace_changes, diff_context,
671 use_ancestor=use_ancestor
669 use_ancestor=use_ancestor
672 )
670 )
673
671
674 # save cached diff
672 # save cached diff
675 if caching_enabled:
673 if caching_enabled:
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
674 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 except CommitDoesNotExistError:
675 except CommitDoesNotExistError:
678 log.exception('Failed to generate diffset')
676 log.exception('Failed to generate diffset')
679 c.missing_commits = True
677 c.missing_commits = True
680
678
681 if not c.missing_commits:
679 if not c.missing_commits:
682
680
683 c.limited_diff = c.diffset.limited_diff
681 c.limited_diff = c.diffset.limited_diff
684
682
685 # calculate removed files that are bound to comments
683 # calculate removed files that are bound to comments
686 comment_deleted_files = [
684 comment_deleted_files = [
687 fname for fname in display_inline_comments
685 fname for fname in display_inline_comments
688 if fname not in c.diffset.file_stats]
686 if fname not in c.diffset.file_stats]
689
687
690 c.deleted_files_comments = collections.defaultdict(dict)
688 c.deleted_files_comments = collections.defaultdict(dict)
691 for fname, per_line_comments in display_inline_comments.items():
689 for fname, per_line_comments in display_inline_comments.items():
692 if fname in comment_deleted_files:
690 if fname in comment_deleted_files:
693 c.deleted_files_comments[fname]['stats'] = 0
691 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['comments'] = list()
692 c.deleted_files_comments[fname]['comments'] = list()
695 for lno, comments in per_line_comments.items():
693 for lno, comments in per_line_comments.items():
696 c.deleted_files_comments[fname]['comments'].extend(comments)
694 c.deleted_files_comments[fname]['comments'].extend(comments)
697
695
698 # maybe calculate the range diff
696 # maybe calculate the range diff
699 if c.range_diff_on:
697 if c.range_diff_on:
700 # TODO(marcink): set whitespace/context
698 # TODO(marcink): set whitespace/context
701 context_lcl = 3
699 context_lcl = 3
702 ign_whitespace_lcl = False
700 ign_whitespace_lcl = False
703
701
704 for commit in c.commit_ranges:
702 for commit in c.commit_ranges:
705 commit2 = commit
703 commit2 = commit
706 commit1 = commit.first_parent
704 commit1 = commit.first_parent
707
705
708 range_diff_cache_file_path = diff_cache_exist(
706 range_diff_cache_file_path = diff_cache_exist(
709 cache_path, 'diff', commit.raw_id,
707 cache_path, 'diff', commit.raw_id,
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
708 ign_whitespace_lcl, context_lcl, c.fulldiff)
711
709
712 cached_diff = None
710 cached_diff = None
713 if caching_enabled:
711 if caching_enabled:
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
712 cached_diff = load_cached_diff(range_diff_cache_file_path)
715
713
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
714 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 if not force_recache and has_proper_diff_cache:
715 if not force_recache and has_proper_diff_cache:
718 diffset = cached_diff['diff']
716 diffset = cached_diff['diff']
719 else:
717 else:
720 diffset = self._get_range_diffset(
718 diffset = self._get_range_diffset(
721 commits_source_repo, source_repo,
719 commits_source_repo, source_repo,
722 commit1, commit2, diff_limit, file_limit,
720 commit1, commit2, diff_limit, file_limit,
723 c.fulldiff, ign_whitespace_lcl, context_lcl
721 c.fulldiff, ign_whitespace_lcl, context_lcl
724 )
722 )
725
723
726 # save cached diff
724 # save cached diff
727 if caching_enabled:
725 if caching_enabled:
728 cache_diff(range_diff_cache_file_path, diffset, None)
726 cache_diff(range_diff_cache_file_path, diffset, None)
729
727
730 c.changes[commit.raw_id] = diffset
728 c.changes[commit.raw_id] = diffset
731
729
732 # this is a hack to properly display links, when creating PR, the
730 # this is a hack to properly display links, when creating PR, the
733 # compare view and others uses different notation, and
731 # compare view and others uses different notation, and
734 # compare_commits.mako renders links based on the target_repo.
732 # compare_commits.mako renders links based on the target_repo.
735 # We need to swap that here to generate it properly on the html side
733 # We need to swap that here to generate it properly on the html side
736 c.target_repo = c.source_repo
734 c.target_repo = c.source_repo
737
735
738 c.commit_statuses = ChangesetStatus.STATUSES
736 c.commit_statuses = ChangesetStatus.STATUSES
739
737
740 c.show_version_changes = not pr_closed
738 c.show_version_changes = not pr_closed
741 if c.show_version_changes:
739 if c.show_version_changes:
742 cur_obj = pull_request_at_ver
740 cur_obj = pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
741 prev_obj = prev_pull_request_at_ver
744
742
745 old_commit_ids = prev_obj.revisions
743 old_commit_ids = prev_obj.revisions
746 new_commit_ids = cur_obj.revisions
744 new_commit_ids = cur_obj.revisions
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
745 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 old_commit_ids, new_commit_ids)
746 old_commit_ids, new_commit_ids)
749 c.commit_changes_summary = commit_changes
747 c.commit_changes_summary = commit_changes
750
748
751 # calculate the diff for commits between versions
749 # calculate the diff for commits between versions
752 c.commit_changes = []
750 c.commit_changes = []
753
751
754 def mark(cs, fw):
752 def mark(cs, fw):
755 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
753 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
756
754
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
755 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 + mark(commit_changes.removed, 'r') \
756 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.common, 'c'):
757 + mark(commit_changes.common, 'c'):
760
758
761 if raw_id in commit_cache:
759 if raw_id in commit_cache:
762 commit = commit_cache[raw_id]
760 commit = commit_cache[raw_id]
763 else:
761 else:
764 try:
762 try:
765 commit = commits_source_repo.get_commit(raw_id)
763 commit = commits_source_repo.get_commit(raw_id)
766 except CommitDoesNotExistError:
764 except CommitDoesNotExistError:
767 # in case we fail extracting still use "dummy" commit
765 # in case we fail extracting still use "dummy" commit
768 # for display in commit diff
766 # for display in commit diff
769 commit = h.AttributeDict(
767 commit = h.AttributeDict(
770 {'raw_id': raw_id,
768 {'raw_id': raw_id,
771 'message': 'EMPTY or MISSING COMMIT'})
769 'message': 'EMPTY or MISSING COMMIT'})
772 c.commit_changes.append([c_type, commit])
770 c.commit_changes.append([c_type, commit])
773
771
774 # current user review statuses for each version
772 # current user review statuses for each version
775 c.review_versions = {}
773 c.review_versions = {}
776 is_reviewer = PullRequestModel().is_user_reviewer(
774 is_reviewer = PullRequestModel().is_user_reviewer(
777 pull_request, self._rhodecode_user)
775 pull_request, self._rhodecode_user)
778 if is_reviewer:
776 if is_reviewer:
779 for co in general_comments:
777 for co in general_comments:
780 if co.author.user_id == self._rhodecode_user.user_id:
778 if co.author.user_id == self._rhodecode_user.user_id:
781 status = co.status_change
779 status = co.status_change
782 if status:
780 if status:
783 _ver_pr = status[0].comment.pull_request_version_id
781 _ver_pr = status[0].comment.pull_request_version_id
784 c.review_versions[_ver_pr] = status[0]
782 c.review_versions[_ver_pr] = status[0]
785
783
786 return self._get_template_context(c)
784 return self._get_template_context(c)
787
785
788 def get_commits(
786 def get_commits(
789 self, commits_source_repo, pull_request_at_ver, source_commit,
787 self, commits_source_repo, pull_request_at_ver, source_commit,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
788 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 maybe_unreachable=False):
789 maybe_unreachable=False):
792
790
793 commit_cache = collections.OrderedDict()
791 commit_cache = collections.OrderedDict()
794 missing_requirements = False
792 missing_requirements = False
795
793
796 try:
794 try:
797 pre_load = ["author", "date", "message", "branch", "parents"]
795 pre_load = ["author", "date", "message", "branch", "parents"]
798
796
799 pull_request_commits = pull_request_at_ver.revisions
797 pull_request_commits = pull_request_at_ver.revisions
800 log.debug('Loading %s commits from %s',
798 log.debug('Loading %s commits from %s',
801 len(pull_request_commits), commits_source_repo)
799 len(pull_request_commits), commits_source_repo)
802
800
803 for rev in pull_request_commits:
801 for rev in pull_request_commits:
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
802 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 maybe_unreachable=maybe_unreachable)
803 maybe_unreachable=maybe_unreachable)
806 commit_cache[comm.raw_id] = comm
804 commit_cache[comm.raw_id] = comm
807
805
808 # Order here matters, we first need to get target, and then
806 # Order here matters, we first need to get target, and then
809 # the source
807 # the source
810 target_commit = commits_source_repo.get_commit(
808 target_commit = commits_source_repo.get_commit(
811 commit_id=safe_str(target_ref_id))
809 commit_id=safe_str(target_ref_id))
812
810
813 source_commit = commits_source_repo.get_commit(
811 source_commit = commits_source_repo.get_commit(
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
812 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 except CommitDoesNotExistError:
813 except CommitDoesNotExistError:
816 log.warning('Failed to get commit from `{}` repo'.format(
814 log.warning('Failed to get commit from `{}` repo'.format(
817 commits_source_repo), exc_info=True)
815 commits_source_repo), exc_info=True)
818 except RepositoryRequirementError:
816 except RepositoryRequirementError:
819 log.warning('Failed to get all required data from repo', exc_info=True)
817 log.warning('Failed to get all required data from repo', exc_info=True)
820 missing_requirements = True
818 missing_requirements = True
821
819
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
820 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823
821
824 try:
822 try:
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
823 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 except Exception:
824 except Exception:
827 ancestor_commit = None
825 ancestor_commit = None
828
826
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
827 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830
828
831 def assure_not_empty_repo(self):
829 def assure_not_empty_repo(self):
832 _ = self.request.translate
830 _ = self.request.translate
833
831
834 try:
832 try:
835 self.db_repo.scm_instance().get_commit()
833 self.db_repo.scm_instance().get_commit()
836 except EmptyRepositoryError:
834 except EmptyRepositoryError:
837 h.flash(h.literal(_('There are no commits yet')),
835 h.flash(h.literal(_('There are no commits yet')),
838 category='warning')
836 category='warning')
839 raise HTTPFound(
837 raise HTTPFound(
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
838 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841
839
842 @LoginRequired()
840 @LoginRequired()
843 @NotAnonymous()
841 @NotAnonymous()
844 @HasRepoPermissionAnyDecorator(
842 @HasRepoPermissionAnyDecorator(
845 'repository.read', 'repository.write', 'repository.admin')
843 'repository.read', 'repository.write', 'repository.admin')
846 def pull_request_new(self):
844 def pull_request_new(self):
847 _ = self.request.translate
845 _ = self.request.translate
848 c = self.load_default_context()
846 c = self.load_default_context()
849
847
850 self.assure_not_empty_repo()
848 self.assure_not_empty_repo()
851 source_repo = self.db_repo
849 source_repo = self.db_repo
852
850
853 commit_id = self.request.GET.get('commit')
851 commit_id = self.request.GET.get('commit')
854 branch_ref = self.request.GET.get('branch')
852 branch_ref = self.request.GET.get('branch')
855 bookmark_ref = self.request.GET.get('bookmark')
853 bookmark_ref = self.request.GET.get('bookmark')
856
854
857 try:
855 try:
858 source_repo_data = PullRequestModel().generate_repo_data(
856 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo, commit_id=commit_id,
857 source_repo, commit_id=commit_id,
860 branch=branch_ref, bookmark=bookmark_ref,
858 branch=branch_ref, bookmark=bookmark_ref,
861 translator=self.request.translate)
859 translator=self.request.translate)
862 except CommitDoesNotExistError as e:
860 except CommitDoesNotExistError as e:
863 log.exception(e)
861 log.exception(e)
864 h.flash(_('Commit does not exist'), 'error')
862 h.flash(_('Commit does not exist'), 'error')
865 raise HTTPFound(
863 raise HTTPFound(
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
864 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867
865
868 default_target_repo = source_repo
866 default_target_repo = source_repo
869
867
870 if source_repo.parent and c.has_origin_repo_read_perm:
868 if source_repo.parent and c.has_origin_repo_read_perm:
871 parent_vcs_obj = source_repo.parent.scm_instance()
869 parent_vcs_obj = source_repo.parent.scm_instance()
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
870 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 # change default if we have a parent repo
871 # change default if we have a parent repo
874 default_target_repo = source_repo.parent
872 default_target_repo = source_repo.parent
875
873
876 target_repo_data = PullRequestModel().generate_repo_data(
874 target_repo_data = PullRequestModel().generate_repo_data(
877 default_target_repo, translator=self.request.translate)
875 default_target_repo, translator=self.request.translate)
878
876
879 selected_source_ref = source_repo_data['refs']['selected_ref']
877 selected_source_ref = source_repo_data['refs']['selected_ref']
880 title_source_ref = ''
878 title_source_ref = ''
881 if selected_source_ref:
879 if selected_source_ref:
882 title_source_ref = selected_source_ref.split(':', 2)[1]
880 title_source_ref = selected_source_ref.split(':', 2)[1]
883 c.default_title = PullRequestModel().generate_pullrequest_title(
881 c.default_title = PullRequestModel().generate_pullrequest_title(
884 source=source_repo.repo_name,
882 source=source_repo.repo_name,
885 source_ref=title_source_ref,
883 source_ref=title_source_ref,
886 target=default_target_repo.repo_name
884 target=default_target_repo.repo_name
887 )
885 )
888
886
889 c.default_repo_data = {
887 c.default_repo_data = {
890 'source_repo_name': source_repo.repo_name,
888 'source_repo_name': source_repo.repo_name,
891 'source_refs_json': ext_json.str_json(source_repo_data),
889 'source_refs_json': ext_json.str_json(source_repo_data),
892 'target_repo_name': default_target_repo.repo_name,
890 'target_repo_name': default_target_repo.repo_name,
893 'target_refs_json': ext_json.str_json(target_repo_data),
891 'target_refs_json': ext_json.str_json(target_repo_data),
894 }
892 }
895 c.default_source_ref = selected_source_ref
893 c.default_source_ref = selected_source_ref
896
894
897 return self._get_template_context(c)
895 return self._get_template_context(c)
898
896
899 @LoginRequired()
897 @LoginRequired()
900 @NotAnonymous()
898 @NotAnonymous()
901 @HasRepoPermissionAnyDecorator(
899 @HasRepoPermissionAnyDecorator(
902 'repository.read', 'repository.write', 'repository.admin')
900 'repository.read', 'repository.write', 'repository.admin')
903 def pull_request_repo_refs(self):
901 def pull_request_repo_refs(self):
904 self.load_default_context()
902 self.load_default_context()
905 target_repo_name = self.request.matchdict['target_repo_name']
903 target_repo_name = self.request.matchdict['target_repo_name']
906 repo = Repository.get_by_repo_name(target_repo_name)
904 repo = Repository.get_by_repo_name(target_repo_name)
907 if not repo:
905 if not repo:
908 raise HTTPNotFound()
906 raise HTTPNotFound()
909
907
910 target_perm = HasRepoPermissionAny(
908 target_perm = HasRepoPermissionAny(
911 'repository.read', 'repository.write', 'repository.admin')(
909 'repository.read', 'repository.write', 'repository.admin')(
912 target_repo_name)
910 target_repo_name)
913 if not target_perm:
911 if not target_perm:
914 raise HTTPNotFound()
912 raise HTTPNotFound()
915
913
916 return PullRequestModel().generate_repo_data(
914 return PullRequestModel().generate_repo_data(
917 repo, translator=self.request.translate)
915 repo, translator=self.request.translate)
918
916
919 @LoginRequired()
917 @LoginRequired()
920 @NotAnonymous()
918 @NotAnonymous()
921 @HasRepoPermissionAnyDecorator(
919 @HasRepoPermissionAnyDecorator(
922 'repository.read', 'repository.write', 'repository.admin')
920 'repository.read', 'repository.write', 'repository.admin')
923 def pullrequest_repo_targets(self):
921 def pullrequest_repo_targets(self):
924 _ = self.request.translate
922 _ = self.request.translate
925 filter_query = self.request.GET.get('query')
923 filter_query = self.request.GET.get('query')
926
924
927 # get the parents
925 # get the parents
928 parent_target_repos = []
926 parent_target_repos = []
929 if self.db_repo.parent:
927 if self.db_repo.parent:
930 parents_query = Repository.query() \
928 parents_query = Repository.query() \
931 .order_by(func.length(Repository.repo_name)) \
929 .order_by(func.length(Repository.repo_name)) \
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
930 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933
931
934 if filter_query:
932 if filter_query:
935 ilike_expression = u'%{}%'.format(safe_str(filter_query))
933 ilike_expression = f'%{safe_str(filter_query)}%'
936 parents_query = parents_query.filter(
934 parents_query = parents_query.filter(
937 Repository.repo_name.ilike(ilike_expression))
935 Repository.repo_name.ilike(ilike_expression))
938 parents = parents_query.limit(20).all()
936 parents = parents_query.limit(20).all()
939
937
940 for parent in parents:
938 for parent in parents:
941 parent_vcs_obj = parent.scm_instance()
939 parent_vcs_obj = parent.scm_instance()
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
940 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 parent_target_repos.append(parent)
941 parent_target_repos.append(parent)
944
942
945 # get other forks, and repo itself
943 # get other forks, and repo itself
946 query = Repository.query() \
944 query = Repository.query() \
947 .order_by(func.length(Repository.repo_name)) \
945 .order_by(func.length(Repository.repo_name)) \
948 .filter(
946 .filter(
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
947 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
948 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 ) \
949 ) \
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
950 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953
951
954 if filter_query:
952 if filter_query:
955 ilike_expression = u'%{}%'.format(safe_str(filter_query))
953 ilike_expression = f'%{safe_str(filter_query)}%'
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
954 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957
955
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
956 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 target_repos = query.limit(limit).all()
957 target_repos = query.limit(limit).all()
960
958
961 all_target_repos = target_repos + parent_target_repos
959 all_target_repos = target_repos + parent_target_repos
962
960
963 repos = []
961 repos = []
964 # This checks permissions to the repositories
962 # This checks permissions to the repositories
965 for obj in ScmModel().get_repos(all_target_repos):
963 for obj in ScmModel().get_repos(all_target_repos):
966 repos.append({
964 repos.append({
967 'id': obj['name'],
965 'id': obj['name'],
968 'text': obj['name'],
966 'text': obj['name'],
969 'type': 'repo',
967 'type': 'repo',
970 'repo_id': obj['dbrepo']['repo_id'],
968 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_type': obj['dbrepo']['repo_type'],
969 'repo_type': obj['dbrepo']['repo_type'],
972 'private': obj['dbrepo']['private'],
970 'private': obj['dbrepo']['private'],
973
971
974 })
972 })
975
973
976 data = {
974 data = {
977 'more': False,
975 'more': False,
978 'results': [{
976 'results': [{
979 'text': _('Repositories'),
977 'text': _('Repositories'),
980 'children': repos
978 'children': repos
981 }] if repos else []
979 }] if repos else []
982 }
980 }
983 return data
981 return data
984
982
985 @classmethod
983 @classmethod
986 def get_comment_ids(cls, post_data):
984 def get_comment_ids(cls, post_data):
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
985 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988
986
989 @LoginRequired()
987 @LoginRequired()
990 @NotAnonymous()
988 @NotAnonymous()
991 @HasRepoPermissionAnyDecorator(
989 @HasRepoPermissionAnyDecorator(
992 'repository.read', 'repository.write', 'repository.admin')
990 'repository.read', 'repository.write', 'repository.admin')
993 def pullrequest_comments(self):
991 def pullrequest_comments(self):
994 self.load_default_context()
992 self.load_default_context()
995
993
996 pull_request = PullRequest.get_or_404(
994 pull_request = PullRequest.get_or_404(
997 self.request.matchdict['pull_request_id'])
995 self.request.matchdict['pull_request_id'])
998 pull_request_id = pull_request.pull_request_id
996 pull_request_id = pull_request.pull_request_id
999 version = self.request.GET.get('version')
997 version = self.request.GET.get('version')
1000
998
1001 _render = self.request.get_partial_renderer(
999 _render = self.request.get_partial_renderer(
1002 'rhodecode:templates/base/sidebar.mako')
1000 'rhodecode:templates/base/sidebar.mako')
1003 c = _render.get_call_context()
1001 c = _render.get_call_context()
1004
1002
1005 (pull_request_latest,
1003 (pull_request_latest,
1006 pull_request_at_ver,
1004 pull_request_at_ver,
1007 pull_request_display_obj,
1005 pull_request_display_obj,
1008 at_version) = PullRequestModel().get_pr_version(
1006 at_version) = PullRequestModel().get_pr_version(
1009 pull_request_id, version=version)
1007 pull_request_id, version=version)
1010 versions = pull_request_display_obj.versions()
1008 versions = pull_request_display_obj.versions()
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1009 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 c.versions = versions + [latest_ver]
1010 c.versions = versions + [latest_ver]
1013
1011
1014 c.at_version = at_version
1012 c.at_version = at_version
1015 c.at_version_num = (at_version
1013 c.at_version_num = (at_version
1016 if at_version and at_version != PullRequest.LATEST_VER
1014 if at_version and at_version != PullRequest.LATEST_VER
1017 else None)
1015 else None)
1018
1016
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1017 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 all_comments = c.inline_comments_flat + c.comments
1018 all_comments = c.inline_comments_flat + c.comments
1021
1019
1022 existing_ids = self.get_comment_ids(self.request.POST)
1020 existing_ids = self.get_comment_ids(self.request.POST)
1023 return _render('comments_table', all_comments, len(all_comments),
1021 return _render('comments_table', all_comments, len(all_comments),
1024 existing_ids=existing_ids)
1022 existing_ids=existing_ids)
1025
1023
1026 @LoginRequired()
1024 @LoginRequired()
1027 @NotAnonymous()
1025 @NotAnonymous()
1028 @HasRepoPermissionAnyDecorator(
1026 @HasRepoPermissionAnyDecorator(
1029 'repository.read', 'repository.write', 'repository.admin')
1027 'repository.read', 'repository.write', 'repository.admin')
1030 def pullrequest_todos(self):
1028 def pullrequest_todos(self):
1031 self.load_default_context()
1029 self.load_default_context()
1032
1030
1033 pull_request = PullRequest.get_or_404(
1031 pull_request = PullRequest.get_or_404(
1034 self.request.matchdict['pull_request_id'])
1032 self.request.matchdict['pull_request_id'])
1035 pull_request_id = pull_request.pull_request_id
1033 pull_request_id = pull_request.pull_request_id
1036 version = self.request.GET.get('version')
1034 version = self.request.GET.get('version')
1037
1035
1038 _render = self.request.get_partial_renderer(
1036 _render = self.request.get_partial_renderer(
1039 'rhodecode:templates/base/sidebar.mako')
1037 'rhodecode:templates/base/sidebar.mako')
1040 c = _render.get_call_context()
1038 c = _render.get_call_context()
1041 (pull_request_latest,
1039 (pull_request_latest,
1042 pull_request_at_ver,
1040 pull_request_at_ver,
1043 pull_request_display_obj,
1041 pull_request_display_obj,
1044 at_version) = PullRequestModel().get_pr_version(
1042 at_version) = PullRequestModel().get_pr_version(
1045 pull_request_id, version=version)
1043 pull_request_id, version=version)
1046 versions = pull_request_display_obj.versions()
1044 versions = pull_request_display_obj.versions()
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1045 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 c.versions = versions + [latest_ver]
1046 c.versions = versions + [latest_ver]
1049
1047
1050 c.at_version = at_version
1048 c.at_version = at_version
1051 c.at_version_num = (at_version
1049 c.at_version_num = (at_version
1052 if at_version and at_version != PullRequest.LATEST_VER
1050 if at_version and at_version != PullRequest.LATEST_VER
1053 else None)
1051 else None)
1054
1052
1055 c.unresolved_comments = CommentsModel() \
1053 c.unresolved_comments = CommentsModel() \
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1054 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 c.resolved_comments = CommentsModel() \
1055 c.resolved_comments = CommentsModel() \
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1056 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059
1057
1060 all_comments = c.unresolved_comments + c.resolved_comments
1058 all_comments = c.unresolved_comments + c.resolved_comments
1061 existing_ids = self.get_comment_ids(self.request.POST)
1059 existing_ids = self.get_comment_ids(self.request.POST)
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1060 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 todo_comments=True, existing_ids=existing_ids)
1061 todo_comments=True, existing_ids=existing_ids)
1064
1062
1065 @LoginRequired()
1063 @LoginRequired()
1066 @NotAnonymous()
1064 @NotAnonymous()
1067 @HasRepoPermissionAnyDecorator(
1065 @HasRepoPermissionAnyDecorator(
1068 'repository.read', 'repository.write', 'repository.admin')
1066 'repository.read', 'repository.write', 'repository.admin')
1069 def pullrequest_drafts(self):
1067 def pullrequest_drafts(self):
1070 self.load_default_context()
1068 self.load_default_context()
1071
1069
1072 pull_request = PullRequest.get_or_404(
1070 pull_request = PullRequest.get_or_404(
1073 self.request.matchdict['pull_request_id'])
1071 self.request.matchdict['pull_request_id'])
1074 pull_request_id = pull_request.pull_request_id
1072 pull_request_id = pull_request.pull_request_id
1075 version = self.request.GET.get('version')
1073 version = self.request.GET.get('version')
1076
1074
1077 _render = self.request.get_partial_renderer(
1075 _render = self.request.get_partial_renderer(
1078 'rhodecode:templates/base/sidebar.mako')
1076 'rhodecode:templates/base/sidebar.mako')
1079 c = _render.get_call_context()
1077 c = _render.get_call_context()
1080
1078
1081 (pull_request_latest,
1079 (pull_request_latest,
1082 pull_request_at_ver,
1080 pull_request_at_ver,
1083 pull_request_display_obj,
1081 pull_request_display_obj,
1084 at_version) = PullRequestModel().get_pr_version(
1082 at_version) = PullRequestModel().get_pr_version(
1085 pull_request_id, version=version)
1083 pull_request_id, version=version)
1086 versions = pull_request_display_obj.versions()
1084 versions = pull_request_display_obj.versions()
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1085 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 c.versions = versions + [latest_ver]
1086 c.versions = versions + [latest_ver]
1089
1087
1090 c.at_version = at_version
1088 c.at_version = at_version
1091 c.at_version_num = (at_version
1089 c.at_version_num = (at_version
1092 if at_version and at_version != PullRequest.LATEST_VER
1090 if at_version and at_version != PullRequest.LATEST_VER
1093 else None)
1091 else None)
1094
1092
1095 c.draft_comments = CommentsModel() \
1093 c.draft_comments = CommentsModel() \
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1094 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097
1095
1098 all_comments = c.draft_comments
1096 all_comments = c.draft_comments
1099
1097
1100 existing_ids = self.get_comment_ids(self.request.POST)
1098 existing_ids = self.get_comment_ids(self.request.POST)
1101 return _render('comments_table', all_comments, len(all_comments),
1099 return _render('comments_table', all_comments, len(all_comments),
1102 existing_ids=existing_ids, draft_comments=True)
1100 existing_ids=existing_ids, draft_comments=True)
1103
1101
1104 @LoginRequired()
1102 @LoginRequired()
1105 @NotAnonymous()
1103 @NotAnonymous()
1106 @HasRepoPermissionAnyDecorator(
1104 @HasRepoPermissionAnyDecorator(
1107 'repository.read', 'repository.write', 'repository.admin')
1105 'repository.read', 'repository.write', 'repository.admin')
1108 @CSRFRequired()
1106 @CSRFRequired()
1109 def pull_request_create(self):
1107 def pull_request_create(self):
1110 _ = self.request.translate
1108 _ = self.request.translate
1111 self.assure_not_empty_repo()
1109 self.assure_not_empty_repo()
1112 self.load_default_context()
1110 self.load_default_context()
1113
1111
1114 controls = peppercorn.parse(self.request.POST.items())
1112 controls = peppercorn.parse(self.request.POST.items())
1115
1113
1116 try:
1114 try:
1117 form = PullRequestForm(
1115 form = PullRequestForm(
1118 self.request.translate, self.db_repo.repo_id)()
1116 self.request.translate, self.db_repo.repo_id)()
1119 _form = form.to_python(controls)
1117 _form = form.to_python(controls)
1120 except formencode.Invalid as errors:
1118 except formencode.Invalid as errors:
1121 if errors.error_dict.get('revisions'):
1119 if errors.error_dict.get('revisions'):
1122 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1120 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1123 elif errors.error_dict.get('pullrequest_title'):
1121 elif errors.error_dict.get('pullrequest_title'):
1124 msg = errors.error_dict.get('pullrequest_title')
1122 msg = errors.error_dict.get('pullrequest_title')
1125 else:
1123 else:
1126 msg = _('Error creating pull request: {}').format(errors)
1124 msg = _('Error creating pull request: {}').format(errors)
1127 log.exception(msg)
1125 log.exception(msg)
1128 h.flash(msg, 'error')
1126 h.flash(msg, 'error')
1129
1127
1130 # would rather just go back to form ...
1128 # would rather just go back to form ...
1131 raise HTTPFound(
1129 raise HTTPFound(
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1130 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133
1131
1134 source_repo = _form['source_repo']
1132 source_repo = _form['source_repo']
1135 source_ref = _form['source_ref']
1133 source_ref = _form['source_ref']
1136 target_repo = _form['target_repo']
1134 target_repo = _form['target_repo']
1137 target_ref = _form['target_ref']
1135 target_ref = _form['target_ref']
1138 commit_ids = _form['revisions'][::-1]
1136 commit_ids = _form['revisions'][::-1]
1139 common_ancestor_id = _form['common_ancestor']
1137 common_ancestor_id = _form['common_ancestor']
1140
1138
1141 # find the ancestor for this pr
1139 # find the ancestor for this pr
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1140 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1141 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144
1142
1145 if not (source_db_repo or target_db_repo):
1143 if not (source_db_repo or target_db_repo):
1146 h.flash(_('source_repo or target repo not found'), category='error')
1144 h.flash(_('source_repo or target repo not found'), category='error')
1147 raise HTTPFound(
1145 raise HTTPFound(
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1146 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149
1147
1150 # re-check permissions again here
1148 # re-check permissions again here
1151 # source_repo we must have read permissions
1149 # source_repo we must have read permissions
1152
1150
1153 source_perm = HasRepoPermissionAny(
1151 source_perm = HasRepoPermissionAny(
1154 'repository.read', 'repository.write', 'repository.admin')(
1152 'repository.read', 'repository.write', 'repository.admin')(
1155 source_db_repo.repo_name)
1153 source_db_repo.repo_name)
1156 if not source_perm:
1154 if not source_perm:
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1155 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 source_db_repo.repo_name))
1156 source_db_repo.repo_name))
1159 h.flash(msg, category='error')
1157 h.flash(msg, category='error')
1160 # copy the args back to redirect
1158 # copy the args back to redirect
1161 org_query = self.request.GET.mixed()
1159 org_query = self.request.GET.mixed()
1162 raise HTTPFound(
1160 raise HTTPFound(
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1161 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 _query=org_query))
1162 _query=org_query))
1165
1163
1166 # target repo we must have read permissions, and also later on
1164 # target repo we must have read permissions, and also later on
1167 # we want to check branch permissions here
1165 # we want to check branch permissions here
1168 target_perm = HasRepoPermissionAny(
1166 target_perm = HasRepoPermissionAny(
1169 'repository.read', 'repository.write', 'repository.admin')(
1167 'repository.read', 'repository.write', 'repository.admin')(
1170 target_db_repo.repo_name)
1168 target_db_repo.repo_name)
1171 if not target_perm:
1169 if not target_perm:
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1170 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 target_db_repo.repo_name))
1171 target_db_repo.repo_name))
1174 h.flash(msg, category='error')
1172 h.flash(msg, category='error')
1175 # copy the args back to redirect
1173 # copy the args back to redirect
1176 org_query = self.request.GET.mixed()
1174 org_query = self.request.GET.mixed()
1177 raise HTTPFound(
1175 raise HTTPFound(
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1176 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 _query=org_query))
1177 _query=org_query))
1180
1178
1181 source_scm = source_db_repo.scm_instance()
1179 source_scm = source_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1180 target_scm = target_db_repo.scm_instance()
1183
1181
1184 source_ref_obj = unicode_to_reference(source_ref)
1182 source_ref_obj = unicode_to_reference(source_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1183 target_ref_obj = unicode_to_reference(target_ref)
1186
1184
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1185 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1186 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189
1187
1190 ancestor = source_scm.get_common_ancestor(
1188 ancestor = source_scm.get_common_ancestor(
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1189 source_commit.raw_id, target_commit.raw_id, target_scm)
1192
1190
1193 # recalculate target ref based on ancestor
1191 # recalculate target ref based on ancestor
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1192 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195
1193
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1194 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 PullRequestModel().get_reviewer_functions()
1195 PullRequestModel().get_reviewer_functions()
1198
1196
1199 # recalculate reviewers logic, to make sure we can validate this
1197 # recalculate reviewers logic, to make sure we can validate this
1200 reviewer_rules = get_default_reviewers_data(
1198 reviewer_rules = get_default_reviewers_data(
1201 self._rhodecode_db_user,
1199 self._rhodecode_db_user,
1202 source_db_repo,
1200 source_db_repo,
1203 source_ref_obj,
1201 source_ref_obj,
1204 target_db_repo,
1202 target_db_repo,
1205 target_ref_obj,
1203 target_ref_obj,
1206 include_diff_info=False)
1204 include_diff_info=False)
1207
1205
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1206 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1207 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210
1208
1211 pullrequest_title = _form['pullrequest_title']
1209 pullrequest_title = _form['pullrequest_title']
1212 title_source_ref = source_ref_obj.name
1210 title_source_ref = source_ref_obj.name
1213 if not pullrequest_title:
1211 if not pullrequest_title:
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1212 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 source=source_repo,
1213 source=source_repo,
1216 source_ref=title_source_ref,
1214 source_ref=title_source_ref,
1217 target=target_repo
1215 target=target_repo
1218 )
1216 )
1219
1217
1220 description = _form['pullrequest_desc']
1218 description = _form['pullrequest_desc']
1221 description_renderer = _form['description_renderer']
1219 description_renderer = _form['description_renderer']
1222
1220
1223 try:
1221 try:
1224 pull_request = PullRequestModel().create(
1222 pull_request = PullRequestModel().create(
1225 created_by=self._rhodecode_user.user_id,
1223 created_by=self._rhodecode_user.user_id,
1226 source_repo=source_repo,
1224 source_repo=source_repo,
1227 source_ref=source_ref,
1225 source_ref=source_ref,
1228 target_repo=target_repo,
1226 target_repo=target_repo,
1229 target_ref=target_ref,
1227 target_ref=target_ref,
1230 revisions=commit_ids,
1228 revisions=commit_ids,
1231 common_ancestor_id=common_ancestor_id,
1229 common_ancestor_id=common_ancestor_id,
1232 reviewers=reviewers,
1230 reviewers=reviewers,
1233 observers=observers,
1231 observers=observers,
1234 title=pullrequest_title,
1232 title=pullrequest_title,
1235 description=description,
1233 description=description,
1236 description_renderer=description_renderer,
1234 description_renderer=description_renderer,
1237 reviewer_data=reviewer_rules,
1235 reviewer_data=reviewer_rules,
1238 auth_user=self._rhodecode_user
1236 auth_user=self._rhodecode_user
1239 )
1237 )
1240 Session().commit()
1238 Session().commit()
1241
1239
1242 h.flash(_('Successfully opened new pull request'),
1240 h.flash(_('Successfully opened new pull request'),
1243 category='success')
1241 category='success')
1244 except Exception:
1242 except Exception:
1245 msg = _('Error occurred during creation of this pull request.')
1243 msg = _('Error occurred during creation of this pull request.')
1246 log.exception(msg)
1244 log.exception(msg)
1247 h.flash(msg, category='error')
1245 h.flash(msg, category='error')
1248
1246
1249 # copy the args back to redirect
1247 # copy the args back to redirect
1250 org_query = self.request.GET.mixed()
1248 org_query = self.request.GET.mixed()
1251 raise HTTPFound(
1249 raise HTTPFound(
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1250 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 _query=org_query))
1251 _query=org_query))
1254
1252
1255 raise HTTPFound(
1253 raise HTTPFound(
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1254 h.route_path('pullrequest_show', repo_name=target_repo,
1257 pull_request_id=pull_request.pull_request_id))
1255 pull_request_id=pull_request.pull_request_id))
1258
1256
1259 @LoginRequired()
1257 @LoginRequired()
1260 @NotAnonymous()
1258 @NotAnonymous()
1261 @HasRepoPermissionAnyDecorator(
1259 @HasRepoPermissionAnyDecorator(
1262 'repository.read', 'repository.write', 'repository.admin')
1260 'repository.read', 'repository.write', 'repository.admin')
1263 @CSRFRequired()
1261 @CSRFRequired()
1264 def pull_request_update(self):
1262 def pull_request_update(self):
1265 pull_request = PullRequest.get_or_404(
1263 pull_request = PullRequest.get_or_404(
1266 self.request.matchdict['pull_request_id'])
1264 self.request.matchdict['pull_request_id'])
1267 _ = self.request.translate
1265 _ = self.request.translate
1268
1266
1269 c = self.load_default_context()
1267 c = self.load_default_context()
1270 redirect_url = None
1268 redirect_url = None
1271 # we do this check as first, because we want to know ASAP in the flow that
1269 # we do this check as first, because we want to know ASAP in the flow that
1272 # pr is updating currently
1270 # pr is updating currently
1273 is_state_changing = pull_request.is_state_changing()
1271 is_state_changing = pull_request.is_state_changing()
1274
1272
1275 if pull_request.is_closed():
1273 if pull_request.is_closed():
1276 log.debug('update: forbidden because pull request is closed')
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 h.flash(msg, category='error')
1276 h.flash(msg, category='error')
1279 return {'response': True,
1277 return {'response': True,
1280 'redirect_url': redirect_url}
1278 'redirect_url': redirect_url}
1281
1279
1282 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283
1281
1284 # only owner or admin can update it
1282 # only owner or admin can update it
1285 allowed_to_update = PullRequestModel().check_user_update(
1283 allowed_to_update = PullRequestModel().check_user_update(
1286 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1287
1285
1288 if allowed_to_update:
1286 if allowed_to_update:
1289 controls = peppercorn.parse(self.request.POST.items())
1287 controls = peppercorn.parse(self.request.POST.items())
1290 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1288 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1291 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1289 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1292
1290
1293 if 'review_members' in controls:
1291 if 'review_members' in controls:
1294 self._update_reviewers(
1292 self._update_reviewers(
1295 c,
1293 c,
1296 pull_request, controls['review_members'],
1294 pull_request, controls['review_members'],
1297 pull_request.reviewer_data,
1295 pull_request.reviewer_data,
1298 PullRequestReviewers.ROLE_REVIEWER)
1296 PullRequestReviewers.ROLE_REVIEWER)
1299 elif 'observer_members' in controls:
1297 elif 'observer_members' in controls:
1300 self._update_reviewers(
1298 self._update_reviewers(
1301 c,
1299 c,
1302 pull_request, controls['observer_members'],
1300 pull_request, controls['observer_members'],
1303 pull_request.reviewer_data,
1301 pull_request.reviewer_data,
1304 PullRequestReviewers.ROLE_OBSERVER)
1302 PullRequestReviewers.ROLE_OBSERVER)
1305 elif do_update_commits:
1303 elif do_update_commits:
1306 if is_state_changing:
1304 if is_state_changing:
1307 log.debug('commits update: forbidden because pull request is in state %s',
1305 log.debug('commits update: forbidden because pull request is in state %s',
1308 pull_request.pull_request_state)
1306 pull_request.pull_request_state)
1309 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1307 msg = _('Cannot update pull requests commits in state other than `{}`. '
1310 u'Current state is: `{}`').format(
1308 'Current state is: `{}`').format(
1311 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1309 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1312 h.flash(msg, category='error')
1310 h.flash(msg, category='error')
1313 return {'response': True,
1311 return {'response': True,
1314 'redirect_url': redirect_url}
1312 'redirect_url': redirect_url}
1315
1313
1316 self._update_commits(c, pull_request)
1314 self._update_commits(c, pull_request)
1317 if force_refresh:
1315 if force_refresh:
1318 redirect_url = h.route_path(
1316 redirect_url = h.route_path(
1319 'pullrequest_show', repo_name=self.db_repo_name,
1317 'pullrequest_show', repo_name=self.db_repo_name,
1320 pull_request_id=pull_request.pull_request_id,
1318 pull_request_id=pull_request.pull_request_id,
1321 _query={"force_refresh": 1})
1319 _query={"force_refresh": 1})
1322 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1320 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1323 self._edit_pull_request(pull_request)
1321 self._edit_pull_request(pull_request)
1324 else:
1322 else:
1325 log.error('Unhandled update data.')
1323 log.error('Unhandled update data.')
1326 raise HTTPBadRequest()
1324 raise HTTPBadRequest()
1327
1325
1328 return {'response': True,
1326 return {'response': True,
1329 'redirect_url': redirect_url}
1327 'redirect_url': redirect_url}
1330 raise HTTPForbidden()
1328 raise HTTPForbidden()
1331
1329
1332 def _edit_pull_request(self, pull_request):
1330 def _edit_pull_request(self, pull_request):
1333 """
1331 """
1334 Edit title and description
1332 Edit title and description
1335 """
1333 """
1336 _ = self.request.translate
1334 _ = self.request.translate
1337
1335
1338 try:
1336 try:
1339 PullRequestModel().edit(
1337 PullRequestModel().edit(
1340 pull_request,
1338 pull_request,
1341 self.request.POST.get('title'),
1339 self.request.POST.get('title'),
1342 self.request.POST.get('description'),
1340 self.request.POST.get('description'),
1343 self.request.POST.get('description_renderer'),
1341 self.request.POST.get('description_renderer'),
1344 self._rhodecode_user)
1342 self._rhodecode_user)
1345 except ValueError:
1343 except ValueError:
1346 msg = _(u'Cannot update closed pull requests.')
1344 msg = _('Cannot update closed pull requests.')
1347 h.flash(msg, category='error')
1345 h.flash(msg, category='error')
1348 return
1346 return
1349 else:
1347 else:
1350 Session().commit()
1348 Session().commit()
1351
1349
1352 msg = _(u'Pull request title & description updated.')
1350 msg = _('Pull request title & description updated.')
1353 h.flash(msg, category='success')
1351 h.flash(msg, category='success')
1354 return
1352 return
1355
1353
1356 def _update_commits(self, c, pull_request):
1354 def _update_commits(self, c, pull_request):
1357 _ = self.request.translate
1355 _ = self.request.translate
1358 log.debug('pull-request: running update commits actions')
1356 log.debug('pull-request: running update commits actions')
1359
1357
1360 @retry(exception=Exception, n_tries=3, delay=2)
1358 @retry(exception=Exception, n_tries=3, delay=2)
1361 def commits_update():
1359 def commits_update():
1362 return PullRequestModel().update_commits(
1360 return PullRequestModel().update_commits(
1363 pull_request, self._rhodecode_db_user)
1361 pull_request, self._rhodecode_db_user)
1364
1362
1365 with pull_request.set_state(PullRequest.STATE_UPDATING):
1363 with pull_request.set_state(PullRequest.STATE_UPDATING):
1366 resp = commits_update() # retry x3
1364 resp = commits_update() # retry x3
1367
1365
1368 if resp.executed:
1366 if resp.executed:
1369
1367
1370 if resp.target_changed and resp.source_changed:
1368 if resp.target_changed and resp.source_changed:
1371 changed = 'target and source repositories'
1369 changed = 'target and source repositories'
1372 elif resp.target_changed and not resp.source_changed:
1370 elif resp.target_changed and not resp.source_changed:
1373 changed = 'target repository'
1371 changed = 'target repository'
1374 elif not resp.target_changed and resp.source_changed:
1372 elif not resp.target_changed and resp.source_changed:
1375 changed = 'source repository'
1373 changed = 'source repository'
1376 else:
1374 else:
1377 changed = 'nothing'
1375 changed = 'nothing'
1378
1376
1379 msg = _(u'Pull request updated to "{source_commit_id}" with '
1377 msg = _('Pull request updated to "{source_commit_id}" with '
1380 u'{count_added} added, {count_removed} removed commits. '
1378 '{count_added} added, {count_removed} removed commits. '
1381 u'Source of changes: {change_source}.')
1379 'Source of changes: {change_source}.')
1382 msg = msg.format(
1380 msg = msg.format(
1383 source_commit_id=pull_request.source_ref_parts.commit_id,
1381 source_commit_id=pull_request.source_ref_parts.commit_id,
1384 count_added=len(resp.changes.added),
1382 count_added=len(resp.changes.added),
1385 count_removed=len(resp.changes.removed),
1383 count_removed=len(resp.changes.removed),
1386 change_source=changed)
1384 change_source=changed)
1387 h.flash(msg, category='success')
1385 h.flash(msg, category='success')
1388 channelstream.pr_update_channelstream_push(
1386 channelstream.pr_update_channelstream_push(
1389 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1387 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 else:
1388 else:
1391 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1389 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1392 warning_reasons = [
1390 warning_reasons = [
1393 UpdateFailureReason.NO_CHANGE,
1391 UpdateFailureReason.NO_CHANGE,
1394 UpdateFailureReason.WRONG_REF_TYPE,
1392 UpdateFailureReason.WRONG_REF_TYPE,
1395 ]
1393 ]
1396 category = 'warning' if resp.reason in warning_reasons else 'error'
1394 category = 'warning' if resp.reason in warning_reasons else 'error'
1397 h.flash(msg, category=category)
1395 h.flash(msg, category=category)
1398
1396
1399 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1397 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1400 _ = self.request.translate
1398 _ = self.request.translate
1401
1399
1402 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1400 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1403 PullRequestModel().get_reviewer_functions()
1401 PullRequestModel().get_reviewer_functions()
1404
1402
1405 if role == PullRequestReviewers.ROLE_REVIEWER:
1403 if role == PullRequestReviewers.ROLE_REVIEWER:
1406 try:
1404 try:
1407 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1405 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1408 except ValueError as e:
1406 except ValueError as e:
1409 log.error('Reviewers Validation: {}'.format(e))
1407 log.error(f'Reviewers Validation: {e}')
1410 h.flash(e, category='error')
1408 h.flash(e, category='error')
1411 return
1409 return
1412
1410
1413 old_calculated_status = pull_request.calculated_review_status()
1411 old_calculated_status = pull_request.calculated_review_status()
1414 PullRequestModel().update_reviewers(
1412 PullRequestModel().update_reviewers(
1415 pull_request, reviewers, self._rhodecode_db_user)
1413 pull_request, reviewers, self._rhodecode_db_user)
1416
1414
1417 Session().commit()
1415 Session().commit()
1418
1416
1419 msg = _('Pull request reviewers updated.')
1417 msg = _('Pull request reviewers updated.')
1420 h.flash(msg, category='success')
1418 h.flash(msg, category='success')
1421 channelstream.pr_update_channelstream_push(
1419 channelstream.pr_update_channelstream_push(
1422 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1420 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1423
1421
1424 # trigger status changed if change in reviewers changes the status
1422 # trigger status changed if change in reviewers changes the status
1425 calculated_status = pull_request.calculated_review_status()
1423 calculated_status = pull_request.calculated_review_status()
1426 if old_calculated_status != calculated_status:
1424 if old_calculated_status != calculated_status:
1427 PullRequestModel().trigger_pull_request_hook(
1425 PullRequestModel().trigger_pull_request_hook(
1428 pull_request, self._rhodecode_user, 'review_status_change',
1426 pull_request, self._rhodecode_user, 'review_status_change',
1429 data={'status': calculated_status})
1427 data={'status': calculated_status})
1430
1428
1431 elif role == PullRequestReviewers.ROLE_OBSERVER:
1429 elif role == PullRequestReviewers.ROLE_OBSERVER:
1432 try:
1430 try:
1433 observers = validate_observers(review_members, reviewer_rules)
1431 observers = validate_observers(review_members, reviewer_rules)
1434 except ValueError as e:
1432 except ValueError as e:
1435 log.error('Observers Validation: {}'.format(e))
1433 log.error(f'Observers Validation: {e}')
1436 h.flash(e, category='error')
1434 h.flash(e, category='error')
1437 return
1435 return
1438
1436
1439 PullRequestModel().update_observers(
1437 PullRequestModel().update_observers(
1440 pull_request, observers, self._rhodecode_db_user)
1438 pull_request, observers, self._rhodecode_db_user)
1441
1439
1442 Session().commit()
1440 Session().commit()
1443 msg = _('Pull request observers updated.')
1441 msg = _('Pull request observers updated.')
1444 h.flash(msg, category='success')
1442 h.flash(msg, category='success')
1445 channelstream.pr_update_channelstream_push(
1443 channelstream.pr_update_channelstream_push(
1446 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1444 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1447
1445
1448 @LoginRequired()
1446 @LoginRequired()
1449 @NotAnonymous()
1447 @NotAnonymous()
1450 @HasRepoPermissionAnyDecorator(
1448 @HasRepoPermissionAnyDecorator(
1451 'repository.read', 'repository.write', 'repository.admin')
1449 'repository.read', 'repository.write', 'repository.admin')
1452 @CSRFRequired()
1450 @CSRFRequired()
1453 def pull_request_merge(self):
1451 def pull_request_merge(self):
1454 """
1452 """
1455 Merge will perform a server-side merge of the specified
1453 Merge will perform a server-side merge of the specified
1456 pull request, if the pull request is approved and mergeable.
1454 pull request, if the pull request is approved and mergeable.
1457 After successful merging, the pull request is automatically
1455 After successful merging, the pull request is automatically
1458 closed, with a relevant comment.
1456 closed, with a relevant comment.
1459 """
1457 """
1460 pull_request = PullRequest.get_or_404(
1458 pull_request = PullRequest.get_or_404(
1461 self.request.matchdict['pull_request_id'])
1459 self.request.matchdict['pull_request_id'])
1462 _ = self.request.translate
1460 _ = self.request.translate
1463
1461
1464 if pull_request.is_state_changing():
1462 if pull_request.is_state_changing():
1465 log.debug('show: forbidden because pull request is in state %s',
1463 log.debug('show: forbidden because pull request is in state %s',
1466 pull_request.pull_request_state)
1464 pull_request.pull_request_state)
1467 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1465 msg = _('Cannot merge pull requests in state other than `{}`. '
1468 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1466 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1469 pull_request.pull_request_state)
1467 pull_request.pull_request_state)
1470 h.flash(msg, category='error')
1468 h.flash(msg, category='error')
1471 raise HTTPFound(
1469 raise HTTPFound(
1472 h.route_path('pullrequest_show',
1470 h.route_path('pullrequest_show',
1473 repo_name=pull_request.target_repo.repo_name,
1471 repo_name=pull_request.target_repo.repo_name,
1474 pull_request_id=pull_request.pull_request_id))
1472 pull_request_id=pull_request.pull_request_id))
1475
1473
1476 self.load_default_context()
1474 self.load_default_context()
1477
1475
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1476 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 check = MergeCheck.validate(
1477 check = MergeCheck.validate(
1480 pull_request, auth_user=self._rhodecode_user,
1478 pull_request, auth_user=self._rhodecode_user,
1481 translator=self.request.translate)
1479 translator=self.request.translate)
1482 merge_possible = not check.failed
1480 merge_possible = not check.failed
1483
1481
1484 for err_type, error_msg in check.errors:
1482 for err_type, error_msg in check.errors:
1485 h.flash(error_msg, category=err_type)
1483 h.flash(error_msg, category=err_type)
1486
1484
1487 if merge_possible:
1485 if merge_possible:
1488 log.debug("Pre-conditions checked, trying to merge.")
1486 log.debug("Pre-conditions checked, trying to merge.")
1489 extras = vcs_operation_context(
1487 extras = vcs_operation_context(
1490 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1488 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1491 username=self._rhodecode_db_user.username, action='push',
1489 username=self._rhodecode_db_user.username, action='push',
1492 scm=pull_request.target_repo.repo_type)
1490 scm=pull_request.target_repo.repo_type)
1493 with pull_request.set_state(PullRequest.STATE_UPDATING):
1491 with pull_request.set_state(PullRequest.STATE_UPDATING):
1494 self._merge_pull_request(
1492 self._merge_pull_request(
1495 pull_request, self._rhodecode_db_user, extras)
1493 pull_request, self._rhodecode_db_user, extras)
1496 else:
1494 else:
1497 log.debug("Pre-conditions failed, NOT merging.")
1495 log.debug("Pre-conditions failed, NOT merging.")
1498
1496
1499 raise HTTPFound(
1497 raise HTTPFound(
1500 h.route_path('pullrequest_show',
1498 h.route_path('pullrequest_show',
1501 repo_name=pull_request.target_repo.repo_name,
1499 repo_name=pull_request.target_repo.repo_name,
1502 pull_request_id=pull_request.pull_request_id))
1500 pull_request_id=pull_request.pull_request_id))
1503
1501
1504 def _merge_pull_request(self, pull_request, user, extras):
1502 def _merge_pull_request(self, pull_request, user, extras):
1505 _ = self.request.translate
1503 _ = self.request.translate
1506 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1504 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1507
1505
1508 if merge_resp.executed:
1506 if merge_resp.executed:
1509 log.debug("The merge was successful, closing the pull request.")
1507 log.debug("The merge was successful, closing the pull request.")
1510 PullRequestModel().close_pull_request(
1508 PullRequestModel().close_pull_request(
1511 pull_request.pull_request_id, user)
1509 pull_request.pull_request_id, user)
1512 Session().commit()
1510 Session().commit()
1513 msg = _('Pull request was successfully merged and closed.')
1511 msg = _('Pull request was successfully merged and closed.')
1514 h.flash(msg, category='success')
1512 h.flash(msg, category='success')
1515 else:
1513 else:
1516 log.debug(
1514 log.debug(
1517 "The merge was not successful. Merge response: %s", merge_resp)
1515 "The merge was not successful. Merge response: %s", merge_resp)
1518 msg = merge_resp.merge_status_message
1516 msg = merge_resp.merge_status_message
1519 h.flash(msg, category='error')
1517 h.flash(msg, category='error')
1520
1518
1521 @LoginRequired()
1519 @LoginRequired()
1522 @NotAnonymous()
1520 @NotAnonymous()
1523 @HasRepoPermissionAnyDecorator(
1521 @HasRepoPermissionAnyDecorator(
1524 'repository.read', 'repository.write', 'repository.admin')
1522 'repository.read', 'repository.write', 'repository.admin')
1525 @CSRFRequired()
1523 @CSRFRequired()
1526 def pull_request_delete(self):
1524 def pull_request_delete(self):
1527 _ = self.request.translate
1525 _ = self.request.translate
1528
1526
1529 pull_request = PullRequest.get_or_404(
1527 pull_request = PullRequest.get_or_404(
1530 self.request.matchdict['pull_request_id'])
1528 self.request.matchdict['pull_request_id'])
1531 self.load_default_context()
1529 self.load_default_context()
1532
1530
1533 pr_closed = pull_request.is_closed()
1531 pr_closed = pull_request.is_closed()
1534 allowed_to_delete = PullRequestModel().check_user_delete(
1532 allowed_to_delete = PullRequestModel().check_user_delete(
1535 pull_request, self._rhodecode_user) and not pr_closed
1533 pull_request, self._rhodecode_user) and not pr_closed
1536
1534
1537 # only owner can delete it !
1535 # only owner can delete it !
1538 if allowed_to_delete:
1536 if allowed_to_delete:
1539 PullRequestModel().delete(pull_request, self._rhodecode_user)
1537 PullRequestModel().delete(pull_request, self._rhodecode_user)
1540 Session().commit()
1538 Session().commit()
1541 h.flash(_('Successfully deleted pull request'),
1539 h.flash(_('Successfully deleted pull request'),
1542 category='success')
1540 category='success')
1543 raise HTTPFound(h.route_path('pullrequest_show_all',
1541 raise HTTPFound(h.route_path('pullrequest_show_all',
1544 repo_name=self.db_repo_name))
1542 repo_name=self.db_repo_name))
1545
1543
1546 log.warning('user %s tried to delete pull request without access',
1544 log.warning('user %s tried to delete pull request without access',
1547 self._rhodecode_user)
1545 self._rhodecode_user)
1548 raise HTTPNotFound()
1546 raise HTTPNotFound()
1549
1547
1550 def _pull_request_comments_create(self, pull_request, comments):
1548 def _pull_request_comments_create(self, pull_request, comments):
1551 _ = self.request.translate
1549 _ = self.request.translate
1552 data = {}
1550 data = {}
1553 if not comments:
1551 if not comments:
1554 return
1552 return
1555 pull_request_id = pull_request.pull_request_id
1553 pull_request_id = pull_request.pull_request_id
1556
1554
1557 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1555 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1558
1556
1559 for entry in comments:
1557 for entry in comments:
1560 c = self.load_default_context()
1558 c = self.load_default_context()
1561 comment_type = entry['comment_type']
1559 comment_type = entry['comment_type']
1562 text = entry['text']
1560 text = entry['text']
1563 status = entry['status']
1561 status = entry['status']
1564 is_draft = str2bool(entry['is_draft'])
1562 is_draft = str2bool(entry['is_draft'])
1565 resolves_comment_id = entry['resolves_comment_id']
1563 resolves_comment_id = entry['resolves_comment_id']
1566 close_pull_request = entry['close_pull_request']
1564 close_pull_request = entry['close_pull_request']
1567 f_path = entry['f_path']
1565 f_path = entry['f_path']
1568 line_no = entry['line']
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 # the logic here should work like following, if we submit close
1569 # the logic here should work like following, if we submit close
1572 # pr comment, use `close_pull_request_with_comment` function
1570 # pr comment, use `close_pull_request_with_comment` function
1573 # else handle regular comment logic
1571 # else handle regular comment logic
1574
1572
1575 if close_pull_request:
1573 if close_pull_request:
1576 # only owner or admin or person with write permissions
1574 # only owner or admin or person with write permissions
1577 allowed_to_close = PullRequestModel().check_user_update(
1575 allowed_to_close = PullRequestModel().check_user_update(
1578 pull_request, self._rhodecode_user)
1576 pull_request, self._rhodecode_user)
1579 if not allowed_to_close:
1577 if not allowed_to_close:
1580 log.debug('comment: forbidden because not allowed to close '
1578 log.debug('comment: forbidden because not allowed to close '
1581 'pull request %s', pull_request_id)
1579 'pull request %s', pull_request_id)
1582 raise HTTPForbidden()
1580 raise HTTPForbidden()
1583
1581
1584 # This also triggers `review_status_change`
1582 # This also triggers `review_status_change`
1585 comment, status = PullRequestModel().close_pull_request_with_comment(
1583 comment, status = PullRequestModel().close_pull_request_with_comment(
1586 pull_request, self._rhodecode_user, self.db_repo, message=text,
1584 pull_request, self._rhodecode_user, self.db_repo, message=text,
1587 auth_user=self._rhodecode_user)
1585 auth_user=self._rhodecode_user)
1588 Session().flush()
1586 Session().flush()
1589 is_inline = comment.is_inline
1587 is_inline = comment.is_inline
1590
1588
1591 PullRequestModel().trigger_pull_request_hook(
1589 PullRequestModel().trigger_pull_request_hook(
1592 pull_request, self._rhodecode_user, 'comment',
1590 pull_request, self._rhodecode_user, 'comment',
1593 data={'comment': comment})
1591 data={'comment': comment})
1594
1592
1595 else:
1593 else:
1596 # regular comment case, could be inline, or one with status.
1594 # regular comment case, could be inline, or one with status.
1597 # for that one we check also permissions
1595 # for that one we check also permissions
1598 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1596 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1599 allowed_to_change_status = PullRequestModel().check_user_change_status(
1597 allowed_to_change_status = PullRequestModel().check_user_change_status(
1600 pull_request, self._rhodecode_user) and not is_draft
1598 pull_request, self._rhodecode_user) and not is_draft
1601
1599
1602 if status and allowed_to_change_status:
1600 if status and allowed_to_change_status:
1603 message = (_('Status change %(transition_icon)s %(status)s')
1601 message = (_('Status change %(transition_icon)s %(status)s')
1604 % {'transition_icon': '>',
1602 % {'transition_icon': '>',
1605 'status': ChangesetStatus.get_status_lbl(status)})
1603 'status': ChangesetStatus.get_status_lbl(status)})
1606 text = text or message
1604 text = text or message
1607
1605
1608 comment = CommentsModel().create(
1606 comment = CommentsModel().create(
1609 text=text,
1607 text=text,
1610 repo=self.db_repo.repo_id,
1608 repo=self.db_repo.repo_id,
1611 user=self._rhodecode_user.user_id,
1609 user=self._rhodecode_user.user_id,
1612 pull_request=pull_request,
1610 pull_request=pull_request,
1613 f_path=f_path,
1611 f_path=f_path,
1614 line_no=line_no,
1612 line_no=line_no,
1615 status_change=(ChangesetStatus.get_status_lbl(status)
1613 status_change=(ChangesetStatus.get_status_lbl(status)
1616 if status and allowed_to_change_status else None),
1614 if status and allowed_to_change_status else None),
1617 status_change_type=(status
1615 status_change_type=(status
1618 if status and allowed_to_change_status else None),
1616 if status and allowed_to_change_status else None),
1619 comment_type=comment_type,
1617 comment_type=comment_type,
1620 is_draft=is_draft,
1618 is_draft=is_draft,
1621 resolves_comment_id=resolves_comment_id,
1619 resolves_comment_id=resolves_comment_id,
1622 auth_user=self._rhodecode_user,
1620 auth_user=self._rhodecode_user,
1623 send_email=not is_draft, # skip notification for draft comments
1621 send_email=not is_draft, # skip notification for draft comments
1624 )
1622 )
1625 is_inline = comment.is_inline
1623 is_inline = comment.is_inline
1626
1624
1627 if allowed_to_change_status:
1625 if allowed_to_change_status:
1628 # calculate old status before we change it
1626 # calculate old status before we change it
1629 old_calculated_status = pull_request.calculated_review_status()
1627 old_calculated_status = pull_request.calculated_review_status()
1630
1628
1631 # get status if set !
1629 # get status if set !
1632 if status:
1630 if status:
1633 ChangesetStatusModel().set_status(
1631 ChangesetStatusModel().set_status(
1634 self.db_repo.repo_id,
1632 self.db_repo.repo_id,
1635 status,
1633 status,
1636 self._rhodecode_user.user_id,
1634 self._rhodecode_user.user_id,
1637 comment,
1635 comment,
1638 pull_request=pull_request
1636 pull_request=pull_request
1639 )
1637 )
1640
1638
1641 Session().flush()
1639 Session().flush()
1642 # this is somehow required to get access to some relationship
1640 # this is somehow required to get access to some relationship
1643 # loaded on comment
1641 # loaded on comment
1644 Session().refresh(comment)
1642 Session().refresh(comment)
1645
1643
1646 # skip notifications for drafts
1644 # skip notifications for drafts
1647 if not is_draft:
1645 if not is_draft:
1648 PullRequestModel().trigger_pull_request_hook(
1646 PullRequestModel().trigger_pull_request_hook(
1649 pull_request, self._rhodecode_user, 'comment',
1647 pull_request, self._rhodecode_user, 'comment',
1650 data={'comment': comment})
1648 data={'comment': comment})
1651
1649
1652 # we now calculate the status of pull request, and based on that
1650 # we now calculate the status of pull request, and based on that
1653 # calculation we set the commits status
1651 # calculation we set the commits status
1654 calculated_status = pull_request.calculated_review_status()
1652 calculated_status = pull_request.calculated_review_status()
1655 if old_calculated_status != calculated_status:
1653 if old_calculated_status != calculated_status:
1656 PullRequestModel().trigger_pull_request_hook(
1654 PullRequestModel().trigger_pull_request_hook(
1657 pull_request, self._rhodecode_user, 'review_status_change',
1655 pull_request, self._rhodecode_user, 'review_status_change',
1658 data={'status': calculated_status})
1656 data={'status': calculated_status})
1659
1657
1660 comment_id = comment.comment_id
1658 comment_id = comment.comment_id
1661 data[comment_id] = {
1659 data[comment_id] = {
1662 'target_id': target_elem_id
1660 'target_id': target_elem_id
1663 }
1661 }
1664 Session().flush()
1662 Session().flush()
1665
1663
1666 c.co = comment
1664 c.co = comment
1667 c.at_version_num = None
1665 c.at_version_num = None
1668 c.is_new = True
1666 c.is_new = True
1669 rendered_comment = render(
1667 rendered_comment = render(
1670 'rhodecode:templates/changeset/changeset_comment_block.mako',
1668 'rhodecode:templates/changeset/changeset_comment_block.mako',
1671 self._get_template_context(c), self.request)
1669 self._get_template_context(c), self.request)
1672
1670
1673 data[comment_id].update(comment.get_dict())
1671 data[comment_id].update(comment.get_dict())
1674 data[comment_id].update({'rendered_text': rendered_comment})
1672 data[comment_id].update({'rendered_text': rendered_comment})
1675
1673
1676 Session().commit()
1674 Session().commit()
1677
1675
1678 # skip channelstream for draft comments
1676 # skip channelstream for draft comments
1679 if not all_drafts:
1677 if not all_drafts:
1680 comment_broadcast_channel = channelstream.comment_channel(
1678 comment_broadcast_channel = channelstream.comment_channel(
1681 self.db_repo_name, pull_request_obj=pull_request)
1679 self.db_repo_name, pull_request_obj=pull_request)
1682
1680
1683 comment_data = data
1681 comment_data = data
1684 posted_comment_type = 'inline' if is_inline else 'general'
1682 posted_comment_type = 'inline' if is_inline else 'general'
1685 if len(data) == 1:
1683 if len(data) == 1:
1686 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1684 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1687 else:
1685 else:
1688 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1686 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1689
1687
1690 channelstream.comment_channelstream_push(
1688 channelstream.comment_channelstream_push(
1691 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1689 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1692 comment_data=comment_data)
1690 comment_data=comment_data)
1693
1691
1694 return data
1692 return data
1695
1693
1696 @LoginRequired()
1694 @LoginRequired()
1697 @NotAnonymous()
1695 @NotAnonymous()
1698 @HasRepoPermissionAnyDecorator(
1696 @HasRepoPermissionAnyDecorator(
1699 'repository.read', 'repository.write', 'repository.admin')
1697 'repository.read', 'repository.write', 'repository.admin')
1700 @CSRFRequired()
1698 @CSRFRequired()
1701 def pull_request_comment_create(self):
1699 def pull_request_comment_create(self):
1702 _ = self.request.translate
1700 _ = self.request.translate
1703
1701
1704 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1702 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1705
1703
1706 if pull_request.is_closed():
1704 if pull_request.is_closed():
1707 log.debug('comment: forbidden because pull request is closed')
1705 log.debug('comment: forbidden because pull request is closed')
1708 raise HTTPForbidden()
1706 raise HTTPForbidden()
1709
1707
1710 allowed_to_comment = PullRequestModel().check_user_comment(
1708 allowed_to_comment = PullRequestModel().check_user_comment(
1711 pull_request, self._rhodecode_user)
1709 pull_request, self._rhodecode_user)
1712 if not allowed_to_comment:
1710 if not allowed_to_comment:
1713 log.debug('comment: forbidden because pull request is from forbidden repo')
1711 log.debug('comment: forbidden because pull request is from forbidden repo')
1714 raise HTTPForbidden()
1712 raise HTTPForbidden()
1715
1713
1716 comment_data = {
1714 comment_data = {
1717 'comment_type': self.request.POST.get('comment_type'),
1715 'comment_type': self.request.POST.get('comment_type'),
1718 'text': self.request.POST.get('text'),
1716 'text': self.request.POST.get('text'),
1719 'status': self.request.POST.get('changeset_status', None),
1717 'status': self.request.POST.get('changeset_status', None),
1720 'is_draft': self.request.POST.get('draft'),
1718 'is_draft': self.request.POST.get('draft'),
1721 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1719 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1722 'close_pull_request': self.request.POST.get('close_pull_request'),
1720 'close_pull_request': self.request.POST.get('close_pull_request'),
1723 'f_path': self.request.POST.get('f_path'),
1721 'f_path': self.request.POST.get('f_path'),
1724 'line': self.request.POST.get('line'),
1722 'line': self.request.POST.get('line'),
1725 }
1723 }
1726 data = self._pull_request_comments_create(pull_request, [comment_data])
1724 data = self._pull_request_comments_create(pull_request, [comment_data])
1727
1725
1728 return data
1726 return data
1729
1727
1730 @LoginRequired()
1728 @LoginRequired()
1731 @NotAnonymous()
1729 @NotAnonymous()
1732 @HasRepoPermissionAnyDecorator(
1730 @HasRepoPermissionAnyDecorator(
1733 'repository.read', 'repository.write', 'repository.admin')
1731 'repository.read', 'repository.write', 'repository.admin')
1734 @CSRFRequired()
1732 @CSRFRequired()
1735 def pull_request_comment_delete(self):
1733 def pull_request_comment_delete(self):
1736 pull_request = PullRequest.get_or_404(
1734 pull_request = PullRequest.get_or_404(
1737 self.request.matchdict['pull_request_id'])
1735 self.request.matchdict['pull_request_id'])
1738
1736
1739 comment = ChangesetComment.get_or_404(
1737 comment = ChangesetComment.get_or_404(
1740 self.request.matchdict['comment_id'])
1738 self.request.matchdict['comment_id'])
1741 comment_id = comment.comment_id
1739 comment_id = comment.comment_id
1742
1740
1743 if comment.immutable:
1741 if comment.immutable:
1744 # don't allow deleting comments that are immutable
1742 # don't allow deleting comments that are immutable
1745 raise HTTPForbidden()
1743 raise HTTPForbidden()
1746
1744
1747 if pull_request.is_closed():
1745 if pull_request.is_closed():
1748 log.debug('comment: forbidden because pull request is closed')
1746 log.debug('comment: forbidden because pull request is closed')
1749 raise HTTPForbidden()
1747 raise HTTPForbidden()
1750
1748
1751 if not comment:
1749 if not comment:
1752 log.debug('Comment with id:%s not found, skipping', comment_id)
1750 log.debug('Comment with id:%s not found, skipping', comment_id)
1753 # comment already deleted in another call probably
1751 # comment already deleted in another call probably
1754 return True
1752 return True
1755
1753
1756 if comment.pull_request.is_closed():
1754 if comment.pull_request.is_closed():
1757 # don't allow deleting comments on closed pull request
1755 # don't allow deleting comments on closed pull request
1758 raise HTTPForbidden()
1756 raise HTTPForbidden()
1759
1757
1760 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1758 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1761 super_admin = h.HasPermissionAny('hg.admin')()
1759 super_admin = h.HasPermissionAny('hg.admin')()
1762 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1760 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1763 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1761 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1764 comment_repo_admin = is_repo_admin and is_repo_comment
1762 comment_repo_admin = is_repo_admin and is_repo_comment
1765
1763
1766 if comment.draft and not comment_owner:
1764 if comment.draft and not comment_owner:
1767 # We never allow to delete draft comments for other than owners
1765 # We never allow to delete draft comments for other than owners
1768 raise HTTPNotFound()
1766 raise HTTPNotFound()
1769
1767
1770 if super_admin or comment_owner or comment_repo_admin:
1768 if super_admin or comment_owner or comment_repo_admin:
1771 old_calculated_status = comment.pull_request.calculated_review_status()
1769 old_calculated_status = comment.pull_request.calculated_review_status()
1772 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1770 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1773 Session().commit()
1771 Session().commit()
1774 calculated_status = comment.pull_request.calculated_review_status()
1772 calculated_status = comment.pull_request.calculated_review_status()
1775 if old_calculated_status != calculated_status:
1773 if old_calculated_status != calculated_status:
1776 PullRequestModel().trigger_pull_request_hook(
1774 PullRequestModel().trigger_pull_request_hook(
1777 comment.pull_request, self._rhodecode_user, 'review_status_change',
1775 comment.pull_request, self._rhodecode_user, 'review_status_change',
1778 data={'status': calculated_status})
1776 data={'status': calculated_status})
1779 return True
1777 return True
1780 else:
1778 else:
1781 log.warning('No permissions for user %s to delete comment_id: %s',
1779 log.warning('No permissions for user %s to delete comment_id: %s',
1782 self._rhodecode_db_user, comment_id)
1780 self._rhodecode_db_user, comment_id)
1783 raise HTTPNotFound()
1781 raise HTTPNotFound()
1784
1782
1785 @LoginRequired()
1783 @LoginRequired()
1786 @NotAnonymous()
1784 @NotAnonymous()
1787 @HasRepoPermissionAnyDecorator(
1785 @HasRepoPermissionAnyDecorator(
1788 'repository.read', 'repository.write', 'repository.admin')
1786 'repository.read', 'repository.write', 'repository.admin')
1789 @CSRFRequired()
1787 @CSRFRequired()
1790 def pull_request_comment_edit(self):
1788 def pull_request_comment_edit(self):
1791 self.load_default_context()
1789 self.load_default_context()
1792
1790
1793 pull_request = PullRequest.get_or_404(
1791 pull_request = PullRequest.get_or_404(
1794 self.request.matchdict['pull_request_id']
1792 self.request.matchdict['pull_request_id']
1795 )
1793 )
1796 comment = ChangesetComment.get_or_404(
1794 comment = ChangesetComment.get_or_404(
1797 self.request.matchdict['comment_id']
1795 self.request.matchdict['comment_id']
1798 )
1796 )
1799 comment_id = comment.comment_id
1797 comment_id = comment.comment_id
1800
1798
1801 if comment.immutable:
1799 if comment.immutable:
1802 # don't allow deleting comments that are immutable
1800 # don't allow deleting comments that are immutable
1803 raise HTTPForbidden()
1801 raise HTTPForbidden()
1804
1802
1805 if pull_request.is_closed():
1803 if pull_request.is_closed():
1806 log.debug('comment: forbidden because pull request is closed')
1804 log.debug('comment: forbidden because pull request is closed')
1807 raise HTTPForbidden()
1805 raise HTTPForbidden()
1808
1806
1809 if comment.pull_request.is_closed():
1807 if comment.pull_request.is_closed():
1810 # don't allow deleting comments on closed pull request
1808 # don't allow deleting comments on closed pull request
1811 raise HTTPForbidden()
1809 raise HTTPForbidden()
1812
1810
1813 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1811 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1814 super_admin = h.HasPermissionAny('hg.admin')()
1812 super_admin = h.HasPermissionAny('hg.admin')()
1815 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1813 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1816 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1814 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1817 comment_repo_admin = is_repo_admin and is_repo_comment
1815 comment_repo_admin = is_repo_admin and is_repo_comment
1818
1816
1819 if super_admin or comment_owner or comment_repo_admin:
1817 if super_admin or comment_owner or comment_repo_admin:
1820 text = self.request.POST.get('text')
1818 text = self.request.POST.get('text')
1821 version = self.request.POST.get('version')
1819 version = self.request.POST.get('version')
1822 if text == comment.text:
1820 if text == comment.text:
1823 log.warning(
1821 log.warning(
1824 'Comment(PR): '
1822 'Comment(PR): '
1825 'Trying to create new version '
1823 'Trying to create new version '
1826 'with the same comment body {}'.format(
1824 'with the same comment body {}'.format(
1827 comment_id,
1825 comment_id,
1828 )
1826 )
1829 )
1827 )
1830 raise HTTPNotFound()
1828 raise HTTPNotFound()
1831
1829
1832 if version.isdigit():
1830 if version.isdigit():
1833 version = int(version)
1831 version = int(version)
1834 else:
1832 else:
1835 log.warning(
1833 log.warning(
1836 'Comment(PR): Wrong version type {} {} '
1834 'Comment(PR): Wrong version type {} {} '
1837 'for comment {}'.format(
1835 'for comment {}'.format(
1838 version,
1836 version,
1839 type(version),
1837 type(version),
1840 comment_id,
1838 comment_id,
1841 )
1839 )
1842 )
1840 )
1843 raise HTTPNotFound()
1841 raise HTTPNotFound()
1844
1842
1845 try:
1843 try:
1846 comment_history = CommentsModel().edit(
1844 comment_history = CommentsModel().edit(
1847 comment_id=comment_id,
1845 comment_id=comment_id,
1848 text=text,
1846 text=text,
1849 auth_user=self._rhodecode_user,
1847 auth_user=self._rhodecode_user,
1850 version=version,
1848 version=version,
1851 )
1849 )
1852 except CommentVersionMismatch:
1850 except CommentVersionMismatch:
1853 raise HTTPConflict()
1851 raise HTTPConflict()
1854
1852
1855 if not comment_history:
1853 if not comment_history:
1856 raise HTTPNotFound()
1854 raise HTTPNotFound()
1857
1855
1858 Session().commit()
1856 Session().commit()
1859 if not comment.draft:
1857 if not comment.draft:
1860 PullRequestModel().trigger_pull_request_hook(
1858 PullRequestModel().trigger_pull_request_hook(
1861 pull_request, self._rhodecode_user, 'comment_edit',
1859 pull_request, self._rhodecode_user, 'comment_edit',
1862 data={'comment': comment})
1860 data={'comment': comment})
1863
1861
1864 return {
1862 return {
1865 'comment_history_id': comment_history.comment_history_id,
1863 'comment_history_id': comment_history.comment_history_id,
1866 'comment_id': comment.comment_id,
1864 'comment_id': comment.comment_id,
1867 'comment_version': comment_history.version,
1865 'comment_version': comment_history.version,
1868 'comment_author_username': comment_history.author.username,
1866 'comment_author_username': comment_history.author.username,
1869 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1867 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1870 'comment_created_on': h.age_component(comment_history.created_on,
1868 'comment_created_on': h.age_component(comment_history.created_on,
1871 time_is_local=True),
1869 time_is_local=True),
1872 }
1870 }
1873 else:
1871 else:
1874 log.warning('No permissions for user %s to edit comment_id: %s',
1872 log.warning('No permissions for user %s to edit comment_id: %s',
1875 self._rhodecode_db_user, comment_id)
1873 self._rhodecode_db_user, comment_id)
1876 raise HTTPNotFound()
1874 raise HTTPNotFound()
@@ -1,83 +1,81 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24
22
25 from rhodecode.apps._base import RepoAppView
23 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
24 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
25 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 from rhodecode.lib.vcs.backends.base import Reference
26 from rhodecode.lib.vcs.backends.base import Reference
29 from rhodecode.model.db import Repository
27 from rhodecode.model.db import Repository
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class RepoReviewRulesView(RepoAppView):
32 class RepoReviewRulesView(RepoAppView):
35 def load_default_context(self):
33 def load_default_context(self):
36 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
37 return c
35 return c
38
36
39 @LoginRequired()
37 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.admin')
38 @HasRepoPermissionAnyDecorator('repository.admin')
41 def repo_review_rules(self):
39 def repo_review_rules(self):
42 c = self.load_default_context()
40 c = self.load_default_context()
43 c.active = 'reviewers'
41 c.active = 'reviewers'
44
42
45 return self._get_template_context(c)
43 return self._get_template_context(c)
46
44
47 @LoginRequired()
45 @LoginRequired()
48 @HasRepoPermissionAnyDecorator(
46 @HasRepoPermissionAnyDecorator(
49 'repository.read', 'repository.write', 'repository.admin')
47 'repository.read', 'repository.write', 'repository.admin')
50 def repo_default_reviewers_data(self):
48 def repo_default_reviewers_data(self):
51 self.load_default_context()
49 self.load_default_context()
52
50
53 request = self.request
51 request = self.request
54 source_repo = self.db_repo
52 source_repo = self.db_repo
55 source_repo_name = source_repo.repo_name
53 source_repo_name = source_repo.repo_name
56 target_repo_name = request.GET.get('target_repo', source_repo_name)
54 target_repo_name = request.GET.get('target_repo', source_repo_name)
57 target_repo = Repository.get_by_repo_name(target_repo_name)
55 target_repo = Repository.get_by_repo_name(target_repo_name)
58
56
59 current_user = request.user.get_instance()
57 current_user = request.user.get_instance()
60
58
61 source_commit_id = request.GET['source_ref']
59 source_commit_id = request.GET['source_ref']
62 source_type = request.GET['source_ref_type']
60 source_type = request.GET['source_ref_type']
63 source_name = request.GET['source_ref_name']
61 source_name = request.GET['source_ref_name']
64
62
65 target_commit_id = request.GET['target_ref']
63 target_commit_id = request.GET['target_ref']
66 target_type = request.GET['target_ref_type']
64 target_type = request.GET['target_ref_type']
67 target_name = request.GET['target_ref_name']
65 target_name = request.GET['target_ref_name']
68
66
69 try:
67 try:
70 review_data = get_default_reviewers_data(
68 review_data = get_default_reviewers_data(
71 current_user,
69 current_user,
72 source_repo,
70 source_repo,
73 Reference(source_type, source_name, source_commit_id),
71 Reference(source_type, source_name, source_commit_id),
74 target_repo,
72 target_repo,
75 Reference(target_type, target_name, target_commit_id)
73 Reference(target_type, target_name, target_commit_id)
76 )
74 )
77 except ValueError:
75 except ValueError:
78 # No common ancestor
76 # No common ancestor
79 msg = "No Common ancestor found between target and source reference"
77 msg = "No Common ancestor found between target and source reference"
80 log.exception(msg)
78 log.exception(msg)
81 return {'diff_info': {'error': msg}}
79 return {'diff_info': {'error': msg}}
82
80
83 return review_data
81 return review_data
@@ -1,277 +1,275 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 import deform
21 import deform
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25
23
26 from rhodecode import events
24 from rhodecode import events
27 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
28 from rhodecode.forms import RcForm
26 from rhodecode.forms import RcForm
29 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
31 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
35 from rhodecode.model.permission import PermissionModel
33 from rhodecode.model.permission import PermissionModel
36 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.scm import RepoGroupList, ScmModel
35 from rhodecode.model.scm import RepoGroupList, ScmModel
38 from rhodecode.model.validation_schema.schemas import repo_schema
36 from rhodecode.model.validation_schema.schemas import repo_schema
39
37
40 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
41
39
42
40
43 class RepoSettingsView(RepoAppView):
41 class RepoSettingsView(RepoAppView):
44
42
45 def load_default_context(self):
43 def load_default_context(self):
46 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
47
45
48 acl_groups = RepoGroupList(
46 acl_groups = RepoGroupList(
49 RepoGroup.query().all(),
47 RepoGroup.query().all(),
50 perm_set=['group.write', 'group.admin'])
48 perm_set=['group.write', 'group.admin'])
51 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
49 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
52 c.repo_groups_choices = list(map(lambda k: k[0], c.repo_groups))
50 c.repo_groups_choices = list(map(lambda k: k[0], c.repo_groups))
53
51
54 # in case someone no longer have a group.write access to a repository
52 # in case someone no longer have a group.write access to a repository
55 # pre fill the list with this entry, we don't care if this is the same
53 # pre fill the list with this entry, we don't care if this is the same
56 # but it will allow saving repo data properly.
54 # but it will allow saving repo data properly.
57 repo_group = self.db_repo.group
55 repo_group = self.db_repo.group
58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
56 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 c.repo_groups_choices.append(repo_group.group_id)
57 c.repo_groups_choices.append(repo_group.group_id)
60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
58 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61
59
62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
60 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 # we might be in missing requirement state, so we load things
61 # we might be in missing requirement state, so we load things
64 # without touching scm_instance()
62 # without touching scm_instance()
65 c.landing_revs_choices, c.landing_revs = \
63 c.landing_revs_choices, c.landing_revs = \
66 ScmModel().get_repo_landing_revs(self.request.translate)
64 ScmModel().get_repo_landing_revs(self.request.translate)
67 else:
65 else:
68 c.landing_revs_choices, c.landing_revs = \
66 c.landing_revs_choices, c.landing_revs = \
69 ScmModel().get_repo_landing_revs(
67 ScmModel().get_repo_landing_revs(
70 self.request.translate, self.db_repo)
68 self.request.translate, self.db_repo)
71
69
72 c.personal_repo_group = c.auth_user.personal_repo_group
70 c.personal_repo_group = c.auth_user.personal_repo_group
73 c.repo_fields = RepositoryField.query()\
71 c.repo_fields = RepositoryField.query()\
74 .filter(RepositoryField.repository == self.db_repo).all()
72 .filter(RepositoryField.repository == self.db_repo).all()
75 return c
73 return c
76
74
77 def _get_schema(self, c, old_values=None):
75 def _get_schema(self, c, old_values=None):
78 return repo_schema.RepoSettingsSchema().bind(
76 return repo_schema.RepoSettingsSchema().bind(
79 repo_type=self.db_repo.repo_type,
77 repo_type=self.db_repo.repo_type,
80 repo_type_options=[self.db_repo.repo_type],
78 repo_type_options=[self.db_repo.repo_type],
81 repo_ref_options=c.landing_revs_choices,
79 repo_ref_options=c.landing_revs_choices,
82 repo_ref_items=c.landing_revs,
80 repo_ref_items=c.landing_revs,
83 repo_repo_group_options=c.repo_groups_choices,
81 repo_repo_group_options=c.repo_groups_choices,
84 repo_repo_group_items=c.repo_groups,
82 repo_repo_group_items=c.repo_groups,
85 # user caller
83 # user caller
86 user=self._rhodecode_user,
84 user=self._rhodecode_user,
87 old_values=old_values
85 old_values=old_values
88 )
86 )
89
87
90 @LoginRequired()
88 @LoginRequired()
91 @HasRepoPermissionAnyDecorator('repository.admin')
89 @HasRepoPermissionAnyDecorator('repository.admin')
92 def edit_settings(self):
90 def edit_settings(self):
93 c = self.load_default_context()
91 c = self.load_default_context()
94 c.active = 'settings'
92 c.active = 'settings'
95
93
96 defaults = RepoModel()._get_defaults(self.db_repo_name)
94 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 defaults['repo_owner'] = defaults['user']
95 defaults['repo_owner'] = defaults['user']
98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
96 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99
97
100 schema = self._get_schema(c)
98 schema = self._get_schema(c)
101 c.form = RcForm(schema, appstruct=defaults)
99 c.form = RcForm(schema, appstruct=defaults)
102 return self._get_template_context(c)
100 return self._get_template_context(c)
103
101
104 @LoginRequired()
102 @LoginRequired()
105 @HasRepoPermissionAnyDecorator('repository.admin')
103 @HasRepoPermissionAnyDecorator('repository.admin')
106 @CSRFRequired()
104 @CSRFRequired()
107 def edit_settings_update(self):
105 def edit_settings_update(self):
108 _ = self.request.translate
106 _ = self.request.translate
109 c = self.load_default_context()
107 c = self.load_default_context()
110 c.active = 'settings'
108 c.active = 'settings'
111 old_repo_name = self.db_repo_name
109 old_repo_name = self.db_repo_name
112
110
113 old_values = self.db_repo.get_api_data()
111 old_values = self.db_repo.get_api_data()
114 schema = self._get_schema(c, old_values=old_values)
112 schema = self._get_schema(c, old_values=old_values)
115
113
116 c.form = RcForm(schema)
114 c.form = RcForm(schema)
117 pstruct = list(self.request.POST.items())
115 pstruct = list(self.request.POST.items())
118 pstruct.append(('repo_type', self.db_repo.repo_type))
116 pstruct.append(('repo_type', self.db_repo.repo_type))
119 try:
117 try:
120 schema_data = c.form.validate(pstruct)
118 schema_data = c.form.validate(pstruct)
121 except deform.ValidationFailure as err_form:
119 except deform.ValidationFailure as err_form:
122 return self._get_template_context(c)
120 return self._get_template_context(c)
123
121
124 # data is now VALID, proceed with updates
122 # data is now VALID, proceed with updates
125 # save validated data back into the updates dict
123 # save validated data back into the updates dict
126 validated_updates = dict(
124 validated_updates = dict(
127 repo_name=schema_data['repo_group']['repo_name_without_group'],
125 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 repo_group=schema_data['repo_group']['repo_group_id'],
126 repo_group=schema_data['repo_group']['repo_group_id'],
129
127
130 user=schema_data['repo_owner'],
128 user=schema_data['repo_owner'],
131 repo_description=schema_data['repo_description'],
129 repo_description=schema_data['repo_description'],
132 repo_private=schema_data['repo_private'],
130 repo_private=schema_data['repo_private'],
133 clone_uri=schema_data['repo_clone_uri'],
131 clone_uri=schema_data['repo_clone_uri'],
134 push_uri=schema_data['repo_push_uri'],
132 push_uri=schema_data['repo_push_uri'],
135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
133 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 repo_enable_statistics=schema_data['repo_enable_statistics'],
134 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 repo_enable_locking=schema_data['repo_enable_locking'],
135 repo_enable_locking=schema_data['repo_enable_locking'],
138 repo_enable_downloads=schema_data['repo_enable_downloads'],
136 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 )
137 )
140 # detect if SYNC URI changed, if we get OLD means we keep old values
138 # detect if SYNC URI changed, if we get OLD means we keep old values
141 if schema_data['repo_clone_uri_change'] == 'OLD':
139 if schema_data['repo_clone_uri_change'] == 'OLD':
142 validated_updates['clone_uri'] = self.db_repo.clone_uri
140 validated_updates['clone_uri'] = self.db_repo.clone_uri
143
141
144 if schema_data['repo_push_uri_change'] == 'OLD':
142 if schema_data['repo_push_uri_change'] == 'OLD':
145 validated_updates['push_uri'] = self.db_repo.push_uri
143 validated_updates['push_uri'] = self.db_repo.push_uri
146
144
147 # use the new full name for redirect
145 # use the new full name for redirect
148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
146 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149
147
150 # save extra fields into our validated data
148 # save extra fields into our validated data
151 for key, value in pstruct:
149 for key, value in pstruct:
152 if key.startswith(RepositoryField.PREFIX):
150 if key.startswith(RepositoryField.PREFIX):
153 validated_updates[key] = value
151 validated_updates[key] = value
154
152
155 try:
153 try:
156 RepoModel().update(self.db_repo, **validated_updates)
154 RepoModel().update(self.db_repo, **validated_updates)
157 ScmModel().mark_for_invalidation(new_repo_name)
155 ScmModel().mark_for_invalidation(new_repo_name)
158
156
159 audit_logger.store_web(
157 audit_logger.store_web(
160 'repo.edit', action_data={'old_data': old_values},
158 'repo.edit', action_data={'old_data': old_values},
161 user=self._rhodecode_user, repo=self.db_repo)
159 user=self._rhodecode_user, repo=self.db_repo)
162
160
163 Session().commit()
161 Session().commit()
164
162
165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
163 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 category='success')
164 category='success')
167 except Exception:
165 except Exception:
168 log.exception("Exception during update of repository")
166 log.exception("Exception during update of repository")
169 h.flash(_('Error occurred during update of repository {}').format(
167 h.flash(_('Error occurred during update of repository {}').format(
170 old_repo_name), category='error')
168 old_repo_name), category='error')
171
169
172 name_changed = old_repo_name != new_repo_name
170 name_changed = old_repo_name != new_repo_name
173 if name_changed:
171 if name_changed:
174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
172 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 affected_user_ids = [perm['user_id'] for perm in current_perms]
173 affected_user_ids = [perm['user_id'] for perm in current_perms]
176
174
177 # NOTE(marcink): also add owner maybe it has changed
175 # NOTE(marcink): also add owner maybe it has changed
178 owner = User.get_by_username(schema_data['repo_owner'])
176 owner = User.get_by_username(schema_data['repo_owner'])
179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
177 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
178 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 PermissionModel().trigger_permission_flush(affected_user_ids)
179 PermissionModel().trigger_permission_flush(affected_user_ids)
182
180
183 raise HTTPFound(
181 raise HTTPFound(
184 h.route_path('edit_repo', repo_name=new_repo_name))
182 h.route_path('edit_repo', repo_name=new_repo_name))
185
183
186 @LoginRequired()
184 @LoginRequired()
187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
185 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 def toggle_locking(self):
186 def toggle_locking(self):
189 """
187 """
190 Toggle locking of repository by simple GET call to url
188 Toggle locking of repository by simple GET call to url
191 """
189 """
192 _ = self.request.translate
190 _ = self.request.translate
193 repo = self.db_repo
191 repo = self.db_repo
194
192
195 try:
193 try:
196 if repo.enable_locking:
194 if repo.enable_locking:
197 if repo.locked[0]:
195 if repo.locked[0]:
198 Repository.unlock(repo)
196 Repository.unlock(repo)
199 action = _('Unlocked')
197 action = _('Unlocked')
200 else:
198 else:
201 Repository.lock(
199 Repository.lock(
202 repo, self._rhodecode_user.user_id,
200 repo, self._rhodecode_user.user_id,
203 lock_reason=Repository.LOCK_WEB)
201 lock_reason=Repository.LOCK_WEB)
204 action = _('Locked')
202 action = _('Locked')
205
203
206 h.flash(_('Repository has been %s') % action,
204 h.flash(_('Repository has been %s') % action,
207 category='success')
205 category='success')
208 except Exception:
206 except Exception:
209 log.exception("Exception during unlocking")
207 log.exception("Exception during unlocking")
210 h.flash(_('An error occurred during unlocking'),
208 h.flash(_('An error occurred during unlocking'),
211 category='error')
209 category='error')
212 raise HTTPFound(
210 raise HTTPFound(
213 h.route_path('repo_summary', repo_name=self.db_repo_name))
211 h.route_path('repo_summary', repo_name=self.db_repo_name))
214
212
215 @LoginRequired()
213 @LoginRequired()
216 @HasRepoPermissionAnyDecorator('repository.admin')
214 @HasRepoPermissionAnyDecorator('repository.admin')
217 def edit_statistics_form(self):
215 def edit_statistics_form(self):
218 c = self.load_default_context()
216 c = self.load_default_context()
219
217
220 if self.db_repo.stats:
218 if self.db_repo.stats:
221 # this is on what revision we ended up so we add +1 for count
219 # this is on what revision we ended up so we add +1 for count
222 last_rev = self.db_repo.stats.stat_on_revision + 1
220 last_rev = self.db_repo.stats.stat_on_revision + 1
223 else:
221 else:
224 last_rev = 0
222 last_rev = 0
225
223
226 c.active = 'statistics'
224 c.active = 'statistics'
227 c.stats_revision = last_rev
225 c.stats_revision = last_rev
228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
226 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229
227
230 if last_rev == 0 or c.repo_last_rev == 0:
228 if last_rev == 0 or c.repo_last_rev == 0:
231 c.stats_percentage = 0
229 c.stats_percentage = 0
232 else:
230 else:
233 c.stats_percentage = '%.2f' % (
231 c.stats_percentage = '%.2f' % (
234 (float((last_rev)) / c.repo_last_rev) * 100)
232 (float(last_rev) / c.repo_last_rev) * 100)
235 return self._get_template_context(c)
233 return self._get_template_context(c)
236
234
237 @LoginRequired()
235 @LoginRequired()
238 @HasRepoPermissionAnyDecorator('repository.admin')
236 @HasRepoPermissionAnyDecorator('repository.admin')
239 @CSRFRequired()
237 @CSRFRequired()
240 def repo_statistics_reset(self):
238 def repo_statistics_reset(self):
241 _ = self.request.translate
239 _ = self.request.translate
242
240
243 try:
241 try:
244 RepoModel().delete_stats(self.db_repo_name)
242 RepoModel().delete_stats(self.db_repo_name)
245 Session().commit()
243 Session().commit()
246 except Exception:
244 except Exception:
247 log.exception('Edit statistics failure')
245 log.exception('Edit statistics failure')
248 h.flash(_('An error occurred during deletion of repository stats'),
246 h.flash(_('An error occurred during deletion of repository stats'),
249 category='error')
247 category='error')
250 raise HTTPFound(
248 raise HTTPFound(
251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
249 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
252
250
253 @LoginRequired()
251 @LoginRequired()
254 @HasRepoPermissionAnyDecorator('repository.admin')
252 @HasRepoPermissionAnyDecorator('repository.admin')
255 def repo_settings_quick_actions(self):
253 def repo_settings_quick_actions(self):
256 _ = self.request.translate
254 _ = self.request.translate
257
255
258 set_lock = self.request.GET.get('set_lock')
256 set_lock = self.request.GET.get('set_lock')
259 set_unlock = self.request.GET.get('set_unlock')
257 set_unlock = self.request.GET.get('set_unlock')
260
258
261 try:
259 try:
262 if set_lock:
260 if set_lock:
263 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
261 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
264 lock_reason=Repository.LOCK_WEB)
262 lock_reason=Repository.LOCK_WEB)
265 h.flash(_('Locked repository'), category='success')
263 h.flash(_('Locked repository'), category='success')
266 elif set_unlock:
264 elif set_unlock:
267 Repository.unlock(self.db_repo)
265 Repository.unlock(self.db_repo)
268 h.flash(_('Unlocked repository'), category='success')
266 h.flash(_('Unlocked repository'), category='success')
269 except Exception as e:
267 except Exception as e:
270 log.exception("Exception during unlocking")
268 log.exception("Exception during unlocking")
271 h.flash(_('An error occurred during unlocking'), category='error')
269 h.flash(_('An error occurred during unlocking'), category='error')
272
270
273 raise HTTPFound(
271 raise HTTPFound(
274 h.route_path('repo_summary', repo_name=self.db_repo_name))
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 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from pyramid.httpexceptions import HTTPFound
22 from pyramid.httpexceptions import HTTPFound
25 from packaging.version import Version
23 from packaging.version import Version
26
24
27 from rhodecode import events
25 from rhodecode import events
28 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps._base import RepoAppView
29 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
33 HasRepoPermissionAny)
31 HasRepoPermissionAny)
34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
35 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
36 from rhodecode.lib.vcs import RepositoryError
34 from rhodecode.lib.vcs import RepositoryError
37 from rhodecode.model.db import Session, UserFollowing, User, Repository
35 from rhodecode.model.db import Session, UserFollowing, User, Repository
38 from rhodecode.model.permission import PermissionModel
36 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.scm import ScmModel
41
39
42 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
43
41
44
42
45 class RepoSettingsAdvancedView(RepoAppView):
43 class RepoSettingsAdvancedView(RepoAppView):
46
44
47 def load_default_context(self):
45 def load_default_context(self):
48 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
49 return c
47 return c
50
48
51 def _get_users_with_permissions(self):
49 def _get_users_with_permissions(self):
52 user_permissions = {}
50 user_permissions = {}
53 for perm in self.db_repo.permissions():
51 for perm in self.db_repo.permissions():
54 user_permissions[perm.user_id] = perm
52 user_permissions[perm.user_id] = perm
55
53
56 return user_permissions
54 return user_permissions
57
55
58 @LoginRequired()
56 @LoginRequired()
59 @HasRepoPermissionAnyDecorator('repository.admin')
57 @HasRepoPermissionAnyDecorator('repository.admin')
60 def edit_advanced(self):
58 def edit_advanced(self):
61 _ = self.request.translate
59 _ = self.request.translate
62 c = self.load_default_context()
60 c = self.load_default_context()
63 c.active = 'advanced'
61 c.active = 'advanced'
64
62
65 c.default_user_id = User.get_default_user_id()
63 c.default_user_id = User.get_default_user_id()
66 c.in_public_journal = UserFollowing.query() \
64 c.in_public_journal = UserFollowing.query() \
67 .filter(UserFollowing.user_id == c.default_user_id) \
65 .filter(UserFollowing.user_id == c.default_user_id) \
68 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
66 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
69
67
70 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
68 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
71 c.hooks_outdated = False
69 c.hooks_outdated = False
72
70
73 try:
71 try:
74 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
72 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
75 c.hooks_outdated = True
73 c.hooks_outdated = True
76 except Exception:
74 except Exception:
77 pass
75 pass
78
76
79 # update commit cache if GET flag is present
77 # update commit cache if GET flag is present
80 if self.request.GET.get('update_commit_cache'):
78 if self.request.GET.get('update_commit_cache'):
81 self.db_repo.update_commit_cache()
79 self.db_repo.update_commit_cache()
82 h.flash(_('updated commit cache'), category='success')
80 h.flash(_('updated commit cache'), category='success')
83
81
84 return self._get_template_context(c)
82 return self._get_template_context(c)
85
83
86 @LoginRequired()
84 @LoginRequired()
87 @HasRepoPermissionAnyDecorator('repository.admin')
85 @HasRepoPermissionAnyDecorator('repository.admin')
88 @CSRFRequired()
86 @CSRFRequired()
89 def edit_advanced_archive(self):
87 def edit_advanced_archive(self):
90 """
88 """
91 Archives the repository. It will become read-only, and not visible in search
89 Archives the repository. It will become read-only, and not visible in search
92 or other queries. But still visible for super-admins.
90 or other queries. But still visible for super-admins.
93 """
91 """
94
92
95 _ = self.request.translate
93 _ = self.request.translate
96
94
97 try:
95 try:
98 old_data = self.db_repo.get_api_data()
96 old_data = self.db_repo.get_api_data()
99 RepoModel().archive(self.db_repo)
97 RepoModel().archive(self.db_repo)
100
98
101 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
99 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
102 audit_logger.store_web(
100 audit_logger.store_web(
103 'repo.archive', action_data={'old_data': old_data},
101 'repo.archive', action_data={'old_data': old_data},
104 user=self._rhodecode_user, repo=repo)
102 user=self._rhodecode_user, repo=repo)
105
103
106 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
104 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
107 h.flash(
105 h.flash(
108 _('Archived repository `%s`') % self.db_repo_name,
106 _('Archived repository `%s`') % self.db_repo_name,
109 category='success')
107 category='success')
110 Session().commit()
108 Session().commit()
111 except Exception:
109 except Exception:
112 log.exception("Exception during archiving of repository")
110 log.exception("Exception during archiving of repository")
113 h.flash(_('An error occurred during archiving of `%s`')
111 h.flash(_('An error occurred during archiving of `%s`')
114 % self.db_repo_name, category='error')
112 % self.db_repo_name, category='error')
115 # redirect to advanced for more deletion options
113 # redirect to advanced for more deletion options
116 raise HTTPFound(
114 raise HTTPFound(
117 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
115 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
118 _anchor='advanced-archive'))
116 _anchor='advanced-archive'))
119
117
120 # flush permissions for all users defined in permissions
118 # flush permissions for all users defined in permissions
121 affected_user_ids = self._get_users_with_permissions().keys()
119 affected_user_ids = self._get_users_with_permissions().keys()
122 PermissionModel().trigger_permission_flush(affected_user_ids)
120 PermissionModel().trigger_permission_flush(affected_user_ids)
123
121
124 raise HTTPFound(h.route_path('home'))
122 raise HTTPFound(h.route_path('home'))
125
123
126 @LoginRequired()
124 @LoginRequired()
127 @HasRepoPermissionAnyDecorator('repository.admin')
125 @HasRepoPermissionAnyDecorator('repository.admin')
128 @CSRFRequired()
126 @CSRFRequired()
129 def edit_advanced_delete(self):
127 def edit_advanced_delete(self):
130 """
128 """
131 Deletes the repository, or shows warnings if deletion is not possible
129 Deletes the repository, or shows warnings if deletion is not possible
132 because of attached forks or other errors.
130 because of attached forks or other errors.
133 """
131 """
134 _ = self.request.translate
132 _ = self.request.translate
135 handle_forks = self.request.POST.get('forks', None)
133 handle_forks = self.request.POST.get('forks', None)
136 if handle_forks == 'detach_forks':
134 if handle_forks == 'detach_forks':
137 handle_forks = 'detach'
135 handle_forks = 'detach'
138 elif handle_forks == 'delete_forks':
136 elif handle_forks == 'delete_forks':
139 handle_forks = 'delete'
137 handle_forks = 'delete'
140
138
141 try:
139 try:
142 old_data = self.db_repo.get_api_data()
140 old_data = self.db_repo.get_api_data()
143 RepoModel().delete(self.db_repo, forks=handle_forks)
141 RepoModel().delete(self.db_repo, forks=handle_forks)
144
142
145 _forks = self.db_repo.forks.count()
143 _forks = self.db_repo.forks.count()
146 if _forks and handle_forks:
144 if _forks and handle_forks:
147 if handle_forks == 'detach_forks':
145 if handle_forks == 'detach_forks':
148 h.flash(_('Detached %s forks') % _forks, category='success')
146 h.flash(_('Detached %s forks') % _forks, category='success')
149 elif handle_forks == 'delete_forks':
147 elif handle_forks == 'delete_forks':
150 h.flash(_('Deleted %s forks') % _forks, category='success')
148 h.flash(_('Deleted %s forks') % _forks, category='success')
151
149
152 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
150 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
153 audit_logger.store_web(
151 audit_logger.store_web(
154 'repo.delete', action_data={'old_data': old_data},
152 'repo.delete', action_data={'old_data': old_data},
155 user=self._rhodecode_user, repo=repo)
153 user=self._rhodecode_user, repo=repo)
156
154
157 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
155 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
158 h.flash(
156 h.flash(
159 _('Deleted repository `%s`') % self.db_repo_name,
157 _('Deleted repository `%s`') % self.db_repo_name,
160 category='success')
158 category='success')
161 Session().commit()
159 Session().commit()
162 except AttachedForksError:
160 except AttachedForksError:
163 repo_advanced_url = h.route_path(
161 repo_advanced_url = h.route_path(
164 'edit_repo_advanced', repo_name=self.db_repo_name,
162 'edit_repo_advanced', repo_name=self.db_repo_name,
165 _anchor='advanced-delete')
163 _anchor='advanced-delete')
166 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
164 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
167 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
165 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
168 'Try using {delete_or_detach} option.')
166 'Try using {delete_or_detach} option.')
169 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
167 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
170 category='warning')
168 category='warning')
171
169
172 # redirect to advanced for forks handle action ?
170 # redirect to advanced for forks handle action ?
173 raise HTTPFound(repo_advanced_url)
171 raise HTTPFound(repo_advanced_url)
174
172
175 except AttachedPullRequestsError:
173 except AttachedPullRequestsError:
176 repo_advanced_url = h.route_path(
174 repo_advanced_url = h.route_path(
177 'edit_repo_advanced', repo_name=self.db_repo_name,
175 'edit_repo_advanced', repo_name=self.db_repo_name,
178 _anchor='advanced-delete')
176 _anchor='advanced-delete')
179 attached_prs = len(self.db_repo.pull_requests_source +
177 attached_prs = len(self.db_repo.pull_requests_source +
180 self.db_repo.pull_requests_target)
178 self.db_repo.pull_requests_target)
181 h.flash(
179 h.flash(
182 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
180 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
183 'Consider archiving the repository instead.').format(
181 'Consider archiving the repository instead.').format(
184 repo=self.db_repo_name, num=attached_prs), category='warning')
182 repo=self.db_repo_name, num=attached_prs), category='warning')
185
183
186 # redirect to advanced for forks handle action ?
184 # redirect to advanced for forks handle action ?
187 raise HTTPFound(repo_advanced_url)
185 raise HTTPFound(repo_advanced_url)
188
186
189 except Exception:
187 except Exception:
190 log.exception("Exception during deletion of repository")
188 log.exception("Exception during deletion of repository")
191 h.flash(_('An error occurred during deletion of `%s`')
189 h.flash(_('An error occurred during deletion of `%s`')
192 % self.db_repo_name, category='error')
190 % self.db_repo_name, category='error')
193 # redirect to advanced for more deletion options
191 # redirect to advanced for more deletion options
194 raise HTTPFound(
192 raise HTTPFound(
195 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
193 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
196 _anchor='advanced-delete'))
194 _anchor='advanced-delete'))
197
195
198 raise HTTPFound(h.route_path('home'))
196 raise HTTPFound(h.route_path('home'))
199
197
200 @LoginRequired()
198 @LoginRequired()
201 @HasRepoPermissionAnyDecorator('repository.admin')
199 @HasRepoPermissionAnyDecorator('repository.admin')
202 @CSRFRequired()
200 @CSRFRequired()
203 def edit_advanced_journal(self):
201 def edit_advanced_journal(self):
204 """
202 """
205 Set's this repository to be visible in public journal,
203 Set's this repository to be visible in public journal,
206 in other words making default user to follow this repo
204 in other words making default user to follow this repo
207 """
205 """
208 _ = self.request.translate
206 _ = self.request.translate
209
207
210 try:
208 try:
211 user_id = User.get_default_user_id()
209 user_id = User.get_default_user_id()
212 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
210 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
213 h.flash(_('Updated repository visibility in public journal'),
211 h.flash(_('Updated repository visibility in public journal'),
214 category='success')
212 category='success')
215 Session().commit()
213 Session().commit()
216 except Exception:
214 except Exception:
217 h.flash(_('An error occurred during setting this '
215 h.flash(_('An error occurred during setting this '
218 'repository in public journal'),
216 'repository in public journal'),
219 category='error')
217 category='error')
220
218
221 raise HTTPFound(
219 raise HTTPFound(
222 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
220 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
223
221
224 @LoginRequired()
222 @LoginRequired()
225 @HasRepoPermissionAnyDecorator('repository.admin')
223 @HasRepoPermissionAnyDecorator('repository.admin')
226 @CSRFRequired()
224 @CSRFRequired()
227 def edit_advanced_fork(self):
225 def edit_advanced_fork(self):
228 """
226 """
229 Mark given repository as a fork of another
227 Mark given repository as a fork of another
230 """
228 """
231 _ = self.request.translate
229 _ = self.request.translate
232
230
233 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
231 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
234
232
235 # valid repo, re-check permissions
233 # valid repo, re-check permissions
236 if new_fork_id:
234 if new_fork_id:
237 repo = Repository.get(new_fork_id)
235 repo = Repository.get(new_fork_id)
238 # ensure we have at least read access to the repo we mark
236 # ensure we have at least read access to the repo we mark
239 perm_check = HasRepoPermissionAny(
237 perm_check = HasRepoPermissionAny(
240 'repository.read', 'repository.write', 'repository.admin')
238 'repository.read', 'repository.write', 'repository.admin')
241
239
242 if repo and perm_check(repo_name=repo.repo_name):
240 if repo and perm_check(repo_name=repo.repo_name):
243 new_fork_id = repo.repo_id
241 new_fork_id = repo.repo_id
244 else:
242 else:
245 new_fork_id = None
243 new_fork_id = None
246
244
247 try:
245 try:
248 repo = ScmModel().mark_as_fork(
246 repo = ScmModel().mark_as_fork(
249 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
247 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
250 fork = repo.fork.repo_name if repo.fork else _('Nothing')
248 fork = repo.fork.repo_name if repo.fork else _('Nothing')
251 Session().commit()
249 Session().commit()
252 h.flash(
250 h.flash(
253 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
251 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
254 category='success')
252 category='success')
255 except RepositoryError as e:
253 except RepositoryError as e:
256 log.exception("Repository Error occurred")
254 log.exception("Repository Error occurred")
257 h.flash(str(e), category='error')
255 h.flash(str(e), category='error')
258 except Exception:
256 except Exception:
259 log.exception("Exception while editing fork")
257 log.exception("Exception while editing fork")
260 h.flash(_('An error occurred during this operation'),
258 h.flash(_('An error occurred during this operation'),
261 category='error')
259 category='error')
262
260
263 raise HTTPFound(
261 raise HTTPFound(
264 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
262 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
265
263
266 @LoginRequired()
264 @LoginRequired()
267 @HasRepoPermissionAnyDecorator('repository.admin')
265 @HasRepoPermissionAnyDecorator('repository.admin')
268 @CSRFRequired()
266 @CSRFRequired()
269 def edit_advanced_toggle_locking(self):
267 def edit_advanced_toggle_locking(self):
270 """
268 """
271 Toggle locking of repository
269 Toggle locking of repository
272 """
270 """
273 _ = self.request.translate
271 _ = self.request.translate
274 set_lock = self.request.POST.get('set_lock')
272 set_lock = self.request.POST.get('set_lock')
275 set_unlock = self.request.POST.get('set_unlock')
273 set_unlock = self.request.POST.get('set_unlock')
276
274
277 try:
275 try:
278 if set_lock:
276 if set_lock:
279 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
277 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
280 lock_reason=Repository.LOCK_WEB)
278 lock_reason=Repository.LOCK_WEB)
281 h.flash(_('Locked repository'), category='success')
279 h.flash(_('Locked repository'), category='success')
282 elif set_unlock:
280 elif set_unlock:
283 Repository.unlock(self.db_repo)
281 Repository.unlock(self.db_repo)
284 h.flash(_('Unlocked repository'), category='success')
282 h.flash(_('Unlocked repository'), category='success')
285 except Exception as e:
283 except Exception as e:
286 log.exception("Exception during unlocking")
284 log.exception("Exception during unlocking")
287 h.flash(_('An error occurred during unlocking'), category='error')
285 h.flash(_('An error occurred during unlocking'), category='error')
288
286
289 raise HTTPFound(
287 raise HTTPFound(
290 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
288 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
291
289
292 @LoginRequired()
290 @LoginRequired()
293 @HasRepoPermissionAnyDecorator('repository.admin')
291 @HasRepoPermissionAnyDecorator('repository.admin')
294 def edit_advanced_install_hooks(self):
292 def edit_advanced_install_hooks(self):
295 """
293 """
296 Install Hooks for repository
294 Install Hooks for repository
297 """
295 """
298 _ = self.request.translate
296 _ = self.request.translate
299 self.load_default_context()
297 self.load_default_context()
300 self.rhodecode_vcs_repo.install_hooks(force=True)
298 self.rhodecode_vcs_repo.install_hooks(force=True)
301 h.flash(_('installed updated hooks into this repository'),
299 h.flash(_('installed updated hooks into this repository'),
302 category='success')
300 category='success')
303 raise HTTPFound(
301 raise HTTPFound(
304 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
302 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,102 +1,100 b''
1
2
3 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 import formencode
21 import formencode
24 import formencode.htmlfill
22 import formencode.htmlfill
25
23
26 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
27
25
28 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps._base import RepoAppView
29 from rhodecode.lib import audit_logger
27 from rhodecode.lib import audit_logger
30 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
31 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 from rhodecode.model.db import RepositoryField
31 from rhodecode.model.db import RepositoryField
34 from rhodecode.model.forms import RepoFieldForm
32 from rhodecode.model.forms import RepoFieldForm
35 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
36 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo import RepoModel
37
35
38 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
39
37
40
38
41 class RepoSettingsFieldsView(RepoAppView):
39 class RepoSettingsFieldsView(RepoAppView):
42 def load_default_context(self):
40 def load_default_context(self):
43 c = self._get_local_tmpl_context()
41 c = self._get_local_tmpl_context()
44
42
45
43
46 return c
44 return c
47
45
48 @LoginRequired()
46 @LoginRequired()
49 @HasRepoPermissionAnyDecorator('repository.admin')
47 @HasRepoPermissionAnyDecorator('repository.admin')
50 def repo_field_edit(self):
48 def repo_field_edit(self):
51 c = self.load_default_context()
49 c = self.load_default_context()
52
50
53 c.active = 'fields'
51 c.active = 'fields'
54 c.repo_fields = RepositoryField.query() \
52 c.repo_fields = RepositoryField.query() \
55 .filter(RepositoryField.repository == self.db_repo).all()
53 .filter(RepositoryField.repository == self.db_repo).all()
56
54
57 return self._get_template_context(c)
55 return self._get_template_context(c)
58
56
59 @LoginRequired()
57 @LoginRequired()
60 @HasRepoPermissionAnyDecorator('repository.admin')
58 @HasRepoPermissionAnyDecorator('repository.admin')
61 @CSRFRequired()
59 @CSRFRequired()
62 def repo_field_create(self):
60 def repo_field_create(self):
63 _ = self.request.translate
61 _ = self.request.translate
64
62
65 try:
63 try:
66 form = RepoFieldForm(self.request.translate)()
64 form = RepoFieldForm(self.request.translate)()
67 form_result = form.to_python(dict(self.request.POST))
65 form_result = form.to_python(dict(self.request.POST))
68 RepoModel().add_repo_field(
66 RepoModel().add_repo_field(
69 self.db_repo_name,
67 self.db_repo_name,
70 form_result['new_field_key'],
68 form_result['new_field_key'],
71 field_type=form_result['new_field_type'],
69 field_type=form_result['new_field_type'],
72 field_value=form_result['new_field_value'],
70 field_value=form_result['new_field_value'],
73 field_label=form_result['new_field_label'],
71 field_label=form_result['new_field_label'],
74 field_desc=form_result['new_field_desc'])
72 field_desc=form_result['new_field_desc'])
75
73
76 Session().commit()
74 Session().commit()
77 except Exception as e:
75 except Exception as e:
78 log.exception("Exception creating field")
76 log.exception("Exception creating field")
79 msg = _('An error occurred during creation of field')
77 msg = _('An error occurred during creation of field')
80 if isinstance(e, formencode.Invalid):
78 if isinstance(e, formencode.Invalid):
81 msg += ". " + e.msg
79 msg += ". " + e.msg
82 h.flash(msg, category='error')
80 h.flash(msg, category='error')
83
81
84 raise HTTPFound(
82 raise HTTPFound(
85 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
83 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
86
84
87 @LoginRequired()
85 @LoginRequired()
88 @HasRepoPermissionAnyDecorator('repository.admin')
86 @HasRepoPermissionAnyDecorator('repository.admin')
89 @CSRFRequired()
87 @CSRFRequired()
90 def repo_field_delete(self):
88 def repo_field_delete(self):
91 _ = self.request.translate
89 _ = self.request.translate
92 field = RepositoryField.get_or_404(self.request.matchdict['field_id'])
90 field = RepositoryField.get_or_404(self.request.matchdict['field_id'])
93 try:
91 try:
94 RepoModel().delete_repo_field(self.db_repo_name, field.field_key)
92 RepoModel().delete_repo_field(self.db_repo_name, field.field_key)
95 Session().commit()
93 Session().commit()
96 except Exception:
94 except Exception:
97 log.exception('Exception during removal of field')
95 log.exception('Exception during removal of field')
98 msg = _('An error occurred during removal of field')
96 msg = _('An error occurred during removal of field')
99 h.flash(msg, category='error')
97 h.flash(msg, category='error')
100
98
101 raise HTTPFound(
99 raise HTTPFound(
102 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
100 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
@@ -1,125 +1,123 b''
1
2
3 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24
22
25 import formencode
23 import formencode
26
24
27 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
28 from rhodecode.lib import audit_logger
26 from rhodecode.lib import audit_logger
29 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
30 from rhodecode.lib.auth import (
28 from rhodecode.lib.auth import (
31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 from rhodecode.model.forms import IssueTrackerPatternsForm
30 from rhodecode.model.forms import IssueTrackerPatternsForm
33 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
34 from rhodecode.model.settings import SettingsModel
32 from rhodecode.model.settings import SettingsModel
35
33
36 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
37
35
38
36
39 class RepoSettingsIssueTrackersView(RepoAppView):
37 class RepoSettingsIssueTrackersView(RepoAppView):
40 def load_default_context(self):
38 def load_default_context(self):
41 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
42 return c
40 return c
43
41
44 @LoginRequired()
42 @LoginRequired()
45 @HasRepoPermissionAnyDecorator('repository.admin')
43 @HasRepoPermissionAnyDecorator('repository.admin')
46 def repo_issuetracker(self):
44 def repo_issuetracker(self):
47 c = self.load_default_context()
45 c = self.load_default_context()
48 c.active = 'issuetracker'
46 c.active = 'issuetracker'
49 c.data = 'data'
47 c.data = 'data'
50
48
51 c.settings_model = self.db_repo_patterns
49 c.settings_model = self.db_repo_patterns
52 c.global_patterns = c.settings_model.get_global_settings()
50 c.global_patterns = c.settings_model.get_global_settings()
53 c.repo_patterns = c.settings_model.get_repo_settings()
51 c.repo_patterns = c.settings_model.get_repo_settings()
54
52
55 return self._get_template_context(c)
53 return self._get_template_context(c)
56
54
57 @LoginRequired()
55 @LoginRequired()
58 @HasRepoPermissionAnyDecorator('repository.admin')
56 @HasRepoPermissionAnyDecorator('repository.admin')
59 @CSRFRequired()
57 @CSRFRequired()
60 def repo_issuetracker_test(self):
58 def repo_issuetracker_test(self):
61 return h.urlify_commit_message(
59 return h.urlify_commit_message(
62 self.request.POST.get('test_text', ''),
60 self.request.POST.get('test_text', ''),
63 self.db_repo_name)
61 self.db_repo_name)
64
62
65 @LoginRequired()
63 @LoginRequired()
66 @HasRepoPermissionAnyDecorator('repository.admin')
64 @HasRepoPermissionAnyDecorator('repository.admin')
67 @CSRFRequired()
65 @CSRFRequired()
68 def repo_issuetracker_delete(self):
66 def repo_issuetracker_delete(self):
69 _ = self.request.translate
67 _ = self.request.translate
70 uid = self.request.POST.get('uid')
68 uid = self.request.POST.get('uid')
71 repo_settings = self.db_repo_patterns
69 repo_settings = self.db_repo_patterns
72 try:
70 try:
73 repo_settings.delete_entries(uid)
71 repo_settings.delete_entries(uid)
74 except Exception:
72 except Exception:
75 h.flash(_('Error occurred during deleting issue tracker entry'),
73 h.flash(_('Error occurred during deleting issue tracker entry'),
76 category='error')
74 category='error')
77 raise HTTPNotFound()
75 raise HTTPNotFound()
78
76
79 SettingsModel().invalidate_settings_cache()
77 SettingsModel().invalidate_settings_cache()
80 h.flash(_('Removed issue tracker entry.'), category='success')
78 h.flash(_('Removed issue tracker entry.'), category='success')
81
79
82 return {'deleted': uid}
80 return {'deleted': uid}
83
81
84 def _update_patterns(self, form, repo_settings):
82 def _update_patterns(self, form, repo_settings):
85 for uid in form['delete_patterns']:
83 for uid in form['delete_patterns']:
86 repo_settings.delete_entries(uid)
84 repo_settings.delete_entries(uid)
87
85
88 for pattern_data in form['patterns']:
86 for pattern_data in form['patterns']:
89 for setting_key, pattern, type_ in pattern_data:
87 for setting_key, pattern, type_ in pattern_data:
90 sett = repo_settings.create_or_update_setting(
88 sett = repo_settings.create_or_update_setting(
91 setting_key, pattern.strip(), type_)
89 setting_key, pattern.strip(), type_)
92 Session().add(sett)
90 Session().add(sett)
93
91
94 Session().commit()
92 Session().commit()
95
93
96 @LoginRequired()
94 @LoginRequired()
97 @HasRepoPermissionAnyDecorator('repository.admin')
95 @HasRepoPermissionAnyDecorator('repository.admin')
98 @CSRFRequired()
96 @CSRFRequired()
99 def repo_issuetracker_update(self):
97 def repo_issuetracker_update(self):
100 _ = self.request.translate
98 _ = self.request.translate
101 # Save inheritance
99 # Save inheritance
102 repo_settings = self.db_repo_patterns
100 repo_settings = self.db_repo_patterns
103 inherited = (
101 inherited = (
104 self.request.POST.get('inherit_global_issuetracker') == "inherited")
102 self.request.POST.get('inherit_global_issuetracker') == "inherited")
105 repo_settings.inherit_global_settings = inherited
103 repo_settings.inherit_global_settings = inherited
106 Session().commit()
104 Session().commit()
107
105
108 try:
106 try:
109 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
107 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
110 except formencode.Invalid as errors:
108 except formencode.Invalid as errors:
111 log.exception('Failed to add new pattern')
109 log.exception('Failed to add new pattern')
112 error = errors
110 error = errors
113 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
111 h.flash(_(f'Invalid issue tracker pattern: {error}'),
114 category='error')
112 category='error')
115 raise HTTPFound(
113 raise HTTPFound(
116 h.route_path('edit_repo_issuetracker',
114 h.route_path('edit_repo_issuetracker',
117 repo_name=self.db_repo_name))
115 repo_name=self.db_repo_name))
118
116
119 if form:
117 if form:
120 self._update_patterns(form, repo_settings)
118 self._update_patterns(form, repo_settings)
121
119
122 h.flash(_('Updated issue tracker entries'), category='success')
120 h.flash(_('Updated issue tracker entries'), category='success')
123 raise HTTPFound(
121 raise HTTPFound(
124 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
122 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
125
123
@@ -1,64 +1,62 b''
1
2
3 # Copyright (C) 2017-2023 RhodeCode GmbH
1 # Copyright (C) 2017-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPFound
21 from pyramid.httpexceptions import HTTPFound
24
22
25
23
26 from rhodecode.apps._base import RepoAppView
24 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import (
26 from rhodecode.lib.auth import (
29 LoginRequired, CSRFRequired, HasRepoPermissionAnyDecorator)
27 LoginRequired, CSRFRequired, HasRepoPermissionAnyDecorator)
30 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
31
29
32 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
33
31
34
32
35 class RepoSettingsRemoteView(RepoAppView):
33 class RepoSettingsRemoteView(RepoAppView):
36 def load_default_context(self):
34 def load_default_context(self):
37 c = self._get_local_tmpl_context()
35 c = self._get_local_tmpl_context()
38 return c
36 return c
39
37
40 @LoginRequired()
38 @LoginRequired()
41 @HasRepoPermissionAnyDecorator('repository.admin')
39 @HasRepoPermissionAnyDecorator('repository.admin')
42 def repo_remote_edit_form(self):
40 def repo_remote_edit_form(self):
43 c = self.load_default_context()
41 c = self.load_default_context()
44 c.active = 'remote'
42 c.active = 'remote'
45
43
46 return self._get_template_context(c)
44 return self._get_template_context(c)
47
45
48 @LoginRequired()
46 @LoginRequired()
49 @HasRepoPermissionAnyDecorator('repository.admin')
47 @HasRepoPermissionAnyDecorator('repository.admin')
50 @CSRFRequired()
48 @CSRFRequired()
51 def repo_remote_pull_changes(self):
49 def repo_remote_pull_changes(self):
52 _ = self.request.translate
50 _ = self.request.translate
53 self.load_default_context()
51 self.load_default_context()
54
52
55 try:
53 try:
56 ScmModel().pull_changes(
54 ScmModel().pull_changes(
57 self.db_repo_name, self._rhodecode_user.username)
55 self.db_repo_name, self._rhodecode_user.username)
58 h.flash(_('Pulled from remote location'), category='success')
56 h.flash(_('Pulled from remote location'), category='success')
59 except Exception:
57 except Exception:
60 log.exception("Exception during pull from remote")
58 log.exception("Exception during pull from remote")
61 h.flash(_('An error occurred during pull from remote location'),
59 h.flash(_('An error occurred during pull from remote location'),
62 category='error')
60 category='error')
63 raise HTTPFound(
61 raise HTTPFound(
64 h.route_path('edit_repo_remote', repo_name=self.db_repo_name))
62 h.route_path('edit_repo_remote', repo_name=self.db_repo_name))
@@ -1,274 +1,272 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import string
20 import string
23 import time
21 import time
24
22
25 import rhodecode
23 import rhodecode
26
24
27
25
28
26
29 from rhodecode.lib.view_utils import get_format_ref_id
27 from rhodecode.lib.view_utils import get_format_ref_id
30 from rhodecode.apps._base import RepoAppView
28 from rhodecode.apps._base import RepoAppView
31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 from rhodecode.lib import helpers as h, rc_cache
30 from rhodecode.lib import helpers as h, rc_cache
33 from rhodecode.lib.utils2 import safe_str, safe_int
31 from rhodecode.lib.utils2 import safe_str, safe_int
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.exceptions import (
35 from rhodecode.lib.vcs.exceptions import (
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
36 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 from rhodecode.model.db import Statistics, CacheKey, User
37 from rhodecode.model.db import Statistics, CacheKey, User
40 from rhodecode.model.meta import Session
38 from rhodecode.model.meta import Session
41 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
42
40
43 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
44
42
45
43
46 class RepoSummaryView(RepoAppView):
44 class RepoSummaryView(RepoAppView):
47
45
48 def load_default_context(self):
46 def load_default_context(self):
49 c = self._get_local_tmpl_context(include_app_defaults=True)
47 c = self._get_local_tmpl_context(include_app_defaults=True)
50 c.rhodecode_repo = None
48 c.rhodecode_repo = None
51 if not c.repository_requirements_missing:
49 if not c.repository_requirements_missing:
52 c.rhodecode_repo = self.rhodecode_vcs_repo
50 c.rhodecode_repo = self.rhodecode_vcs_repo
53 return c
51 return c
54
52
55 def _load_commits_context(self, c):
53 def _load_commits_context(self, c):
56 p = safe_int(self.request.GET.get('page'), 1)
54 p = safe_int(self.request.GET.get('page'), 1)
57 size = safe_int(self.request.GET.get('size'), 10)
55 size = safe_int(self.request.GET.get('size'), 10)
58
56
59 def url_generator(page_num):
57 def url_generator(page_num):
60 query_params = {
58 query_params = {
61 'page': page_num,
59 'page': page_num,
62 'size': size
60 'size': size
63 }
61 }
64 return h.route_path(
62 return h.route_path(
65 'repo_summary_commits',
63 'repo_summary_commits',
66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
64 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67
65
68 pre_load = self.get_commit_preload_attrs()
66 pre_load = self.get_commit_preload_attrs()
69
67
70 try:
68 try:
71 collection = self.rhodecode_vcs_repo.get_commits(
69 collection = self.rhodecode_vcs_repo.get_commits(
72 pre_load=pre_load, translate_tags=False)
70 pre_load=pre_load, translate_tags=False)
73 except EmptyRepositoryError:
71 except EmptyRepositoryError:
74 collection = self.rhodecode_vcs_repo
72 collection = self.rhodecode_vcs_repo
75
73
76 c.repo_commits = h.RepoPage(
74 c.repo_commits = h.RepoPage(
77 collection, page=p, items_per_page=size, url_maker=url_generator)
75 collection, page=p, items_per_page=size, url_maker=url_generator)
78 page_ids = [x.raw_id for x in c.repo_commits]
76 page_ids = [x.raw_id for x in c.repo_commits]
79 c.comments = self.db_repo.get_comments(page_ids)
77 c.comments = self.db_repo.get_comments(page_ids)
80 c.statuses = self.db_repo.statuses(page_ids)
78 c.statuses = self.db_repo.statuses(page_ids)
81
79
82 @LoginRequired()
80 @LoginRequired()
83 @HasRepoPermissionAnyDecorator(
81 @HasRepoPermissionAnyDecorator(
84 'repository.read', 'repository.write', 'repository.admin')
82 'repository.read', 'repository.write', 'repository.admin')
85 def summary_commits(self):
83 def summary_commits(self):
86 c = self.load_default_context()
84 c = self.load_default_context()
87 self._prepare_and_set_clone_url(c)
85 self._prepare_and_set_clone_url(c)
88 self._load_commits_context(c)
86 self._load_commits_context(c)
89 return self._get_template_context(c)
87 return self._get_template_context(c)
90
88
91 @LoginRequired()
89 @LoginRequired()
92 @HasRepoPermissionAnyDecorator(
90 @HasRepoPermissionAnyDecorator(
93 'repository.read', 'repository.write', 'repository.admin')
91 'repository.read', 'repository.write', 'repository.admin')
94 def summary(self):
92 def summary(self):
95 c = self.load_default_context()
93 c = self.load_default_context()
96
94
97 # Prepare the clone URL
95 # Prepare the clone URL
98 self._prepare_and_set_clone_url(c)
96 self._prepare_and_set_clone_url(c)
99
97
100 # If enabled, get statistics data
98 # If enabled, get statistics data
101 c.show_stats = bool(self.db_repo.enable_statistics)
99 c.show_stats = bool(self.db_repo.enable_statistics)
102
100
103 stats = Session().query(Statistics) \
101 stats = Session().query(Statistics) \
104 .filter(Statistics.repository == self.db_repo) \
102 .filter(Statistics.repository == self.db_repo) \
105 .scalar()
103 .scalar()
106
104
107 c.stats_percentage = 0
105 c.stats_percentage = 0
108
106
109 if stats and stats.languages:
107 if stats and stats.languages:
110 c.no_data = False is self.db_repo.enable_statistics
108 c.no_data = False is self.db_repo.enable_statistics
111 lang_stats_d = json.loads(stats.languages)
109 lang_stats_d = json.loads(stats.languages)
112
110
113 # Sort first by decreasing count and second by the file extension,
111 # Sort first by decreasing count and second by the file extension,
114 # so we have a consistent output.
112 # so we have a consistent output.
115 lang_stats_items = sorted(lang_stats_d.items(),
113 lang_stats_items = sorted(lang_stats_d.items(),
116 key=lambda k: (-k[1], k[0]))[:10]
114 key=lambda k: (-k[1], k[0]))[:10]
117 lang_stats = [(x, {"count": y,
115 lang_stats = [(x, {"count": y,
118 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
116 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
119 for x, y in lang_stats_items]
117 for x, y in lang_stats_items]
120
118
121 c.trending_languages = json.dumps(lang_stats)
119 c.trending_languages = json.dumps(lang_stats)
122 else:
120 else:
123 c.no_data = True
121 c.no_data = True
124 c.trending_languages = json.dumps({})
122 c.trending_languages = json.dumps({})
125
123
126 scm_model = ScmModel()
124 scm_model = ScmModel()
127 c.enable_downloads = self.db_repo.enable_downloads
125 c.enable_downloads = self.db_repo.enable_downloads
128 c.repository_followers = scm_model.get_followers(self.db_repo)
126 c.repository_followers = scm_model.get_followers(self.db_repo)
129 c.repository_forks = scm_model.get_forks(self.db_repo)
127 c.repository_forks = scm_model.get_forks(self.db_repo)
130
128
131 # first interaction with the VCS instance after here...
129 # first interaction with the VCS instance after here...
132 if c.repository_requirements_missing:
130 if c.repository_requirements_missing:
133 self.request.override_renderer = \
131 self.request.override_renderer = \
134 'rhodecode:templates/summary/missing_requirements.mako'
132 'rhodecode:templates/summary/missing_requirements.mako'
135 return self._get_template_context(c)
133 return self._get_template_context(c)
136
134
137 c.readme_data, c.readme_file = \
135 c.readme_data, c.readme_file = \
138 self._get_readme_data(self.db_repo, c.visual.default_renderer)
136 self._get_readme_data(self.db_repo, c.visual.default_renderer)
139
137
140 # loads the summary commits template context
138 # loads the summary commits template context
141 self._load_commits_context(c)
139 self._load_commits_context(c)
142
140
143 return self._get_template_context(c)
141 return self._get_template_context(c)
144
142
145 @LoginRequired()
143 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
148 def repo_stats(self):
146 def repo_stats(self):
149 show_stats = bool(self.db_repo.enable_statistics)
147 show_stats = bool(self.db_repo.enable_statistics)
150 repo_id = self.db_repo.repo_id
148 repo_id = self.db_repo.repo_id
151
149
152 landing_commit = self.db_repo.get_landing_commit()
150 landing_commit = self.db_repo.get_landing_commit()
153 if isinstance(landing_commit, EmptyCommit):
151 if isinstance(landing_commit, EmptyCommit):
154 return {'size': 0, 'code_stats': {}}
152 return {'size': 0, 'code_stats': {}}
155
153
156 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
154 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
157 cache_on = cache_seconds > 0
155 cache_on = cache_seconds > 0
158
156
159 log.debug(
157 log.debug(
160 'Computing REPO STATS for repo_id %s commit_id `%s` '
158 'Computing REPO STATS for repo_id %s commit_id `%s` '
161 'with caching: %s[TTL: %ss]' % (
159 'with caching: %s[TTL: %ss]' % (
162 repo_id, landing_commit, cache_on, cache_seconds or 0))
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 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
163 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166
164
167 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
165 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
168 condition=cache_on)
166 condition=cache_on)
169 def compute_stats(repo_id, commit_id, _show_stats):
167 def compute_stats(repo_id, commit_id, _show_stats):
170 code_stats = {}
168 code_stats = {}
171 size = 0
169 size = 0
172 try:
170 try:
173 commit = self.db_repo.get_commit(commit_id)
171 commit = self.db_repo.get_commit(commit_id)
174
172
175 for node in commit.get_filenodes_generator():
173 for node in commit.get_filenodes_generator():
176 size += node.size
174 size += node.size
177 if not _show_stats:
175 if not _show_stats:
178 continue
176 continue
179 ext = node.extension.lower()
177 ext = node.extension.lower()
180 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
178 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
181 if ext_info:
179 if ext_info:
182 if ext in code_stats:
180 if ext in code_stats:
183 code_stats[ext]['count'] += 1
181 code_stats[ext]['count'] += 1
184 else:
182 else:
185 code_stats[ext] = {"count": 1, "desc": ext_info}
183 code_stats[ext] = {"count": 1, "desc": ext_info}
186 except (EmptyRepositoryError, CommitDoesNotExistError):
184 except (EmptyRepositoryError, CommitDoesNotExistError):
187 pass
185 pass
188 return {'size': h.format_byte_size_binary(size),
186 return {'size': h.format_byte_size_binary(size),
189 'code_stats': code_stats}
187 'code_stats': code_stats}
190
188
191 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
189 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
192 return stats
190 return stats
193
191
194 @LoginRequired()
192 @LoginRequired()
195 @HasRepoPermissionAnyDecorator(
193 @HasRepoPermissionAnyDecorator(
196 'repository.read', 'repository.write', 'repository.admin')
194 'repository.read', 'repository.write', 'repository.admin')
197 def repo_refs_data(self):
195 def repo_refs_data(self):
198 _ = self.request.translate
196 _ = self.request.translate
199 self.load_default_context()
197 self.load_default_context()
200
198
201 repo = self.rhodecode_vcs_repo
199 repo = self.rhodecode_vcs_repo
202 refs_to_create = [
200 refs_to_create = [
203 (_("Branch"), repo.branches, 'branch'),
201 (_("Branch"), repo.branches, 'branch'),
204 (_("Tag"), repo.tags, 'tag'),
202 (_("Tag"), repo.tags, 'tag'),
205 (_("Bookmark"), repo.bookmarks, 'book'),
203 (_("Bookmark"), repo.bookmarks, 'book'),
206 ]
204 ]
207 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
205 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
208 data = {
206 data = {
209 'more': False,
207 'more': False,
210 'results': res
208 'results': res
211 }
209 }
212 return data
210 return data
213
211
214 @LoginRequired()
212 @LoginRequired()
215 @HasRepoPermissionAnyDecorator(
213 @HasRepoPermissionAnyDecorator(
216 'repository.read', 'repository.write', 'repository.admin')
214 'repository.read', 'repository.write', 'repository.admin')
217 def repo_refs_changelog_data(self):
215 def repo_refs_changelog_data(self):
218 _ = self.request.translate
216 _ = self.request.translate
219 self.load_default_context()
217 self.load_default_context()
220
218
221 repo = self.rhodecode_vcs_repo
219 repo = self.rhodecode_vcs_repo
222
220
223 refs_to_create = [
221 refs_to_create = [
224 (_("Branches"), repo.branches, 'branch'),
222 (_("Branches"), repo.branches, 'branch'),
225 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
223 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
226 # TODO: enable when vcs can handle bookmarks filters
224 # TODO: enable when vcs can handle bookmarks filters
227 # (_("Bookmarks"), repo.bookmarks, "book"),
225 # (_("Bookmarks"), repo.bookmarks, "book"),
228 ]
226 ]
229 res = self._create_reference_data(
227 res = self._create_reference_data(
230 repo, self.db_repo_name, refs_to_create)
228 repo, self.db_repo_name, refs_to_create)
231 data = {
229 data = {
232 'more': False,
230 'more': False,
233 'results': res
231 'results': res
234 }
232 }
235 return data
233 return data
236
234
237 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
235 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
238 format_ref_id = get_format_ref_id(repo)
236 format_ref_id = get_format_ref_id(repo)
239
237
240 result = []
238 result = []
241 for title, refs, ref_type in refs_to_create:
239 for title, refs, ref_type in refs_to_create:
242 if refs:
240 if refs:
243 result.append({
241 result.append({
244 'text': title,
242 'text': title,
245 'children': self._create_reference_items(
243 'children': self._create_reference_items(
246 repo, full_repo_name, refs, ref_type,
244 repo, full_repo_name, refs, ref_type,
247 format_ref_id),
245 format_ref_id),
248 })
246 })
249 return result
247 return result
250
248
251 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
249 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
252 result = []
250 result = []
253 is_svn = h.is_svn(repo)
251 is_svn = h.is_svn(repo)
254 for ref_name, raw_id in refs.items():
252 for ref_name, raw_id in refs.items():
255 files_url = self._create_files_url(
253 files_url = self._create_files_url(
256 repo, full_repo_name, ref_name, raw_id, is_svn)
254 repo, full_repo_name, ref_name, raw_id, is_svn)
257 result.append({
255 result.append({
258 'text': ref_name,
256 'text': ref_name,
259 'id': format_ref_id(ref_name, raw_id),
257 'id': format_ref_id(ref_name, raw_id),
260 'raw_id': raw_id,
258 'raw_id': raw_id,
261 'type': ref_type,
259 'type': ref_type,
262 'files_url': files_url,
260 'files_url': files_url,
263 'idx': 0,
261 'idx': 0,
264 })
262 })
265 return result
263 return result
266
264
267 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
265 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
268 use_commit_id = '/' in ref_name or is_svn
266 use_commit_id = '/' in ref_name or is_svn
269 return h.route_path(
267 return h.route_path(
270 'repo_files',
268 'repo_files',
271 repo_name=full_repo_name,
269 repo_name=full_repo_name,
272 f_path=ref_name if is_svn else '',
270 f_path=ref_name if is_svn else '',
273 commit_id=raw_id if use_commit_id else ref_name,
271 commit_id=raw_id if use_commit_id else ref_name,
274 _query=dict(at=ref_name))
272 _query=dict(at=ref_name))
@@ -1,48 +1,46 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from rhodecode.apps._base import BaseReferencesView
21 from rhodecode.apps._base import BaseReferencesView
24 from rhodecode.lib import ext_json
22 from rhodecode.lib import ext_json
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
23 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26 from rhodecode.model.scm import ScmModel
24 from rhodecode.model.scm import ScmModel
27
25
28 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
29
27
30
28
31 class RepoTagsView(BaseReferencesView):
29 class RepoTagsView(BaseReferencesView):
32
30
33 @LoginRequired()
31 @LoginRequired()
34 @HasRepoPermissionAnyDecorator(
32 @HasRepoPermissionAnyDecorator(
35 'repository.read', 'repository.write', 'repository.admin')
33 'repository.read', 'repository.write', 'repository.admin')
36 def tags(self):
34 def tags(self):
37 c = self.load_default_context()
35 c = self.load_default_context()
38 self._prepare_and_set_clone_url(c)
36 self._prepare_and_set_clone_url(c)
39 c.rhodecode_repo = self.rhodecode_vcs_repo
37 c.rhodecode_repo = self.rhodecode_vcs_repo
40 c.repository_forks = ScmModel().get_forks(self.db_repo)
38 c.repository_forks = ScmModel().get_forks(self.db_repo)
41
39
42 ref_items = self.rhodecode_vcs_repo.tags.items()
40 ref_items = self.rhodecode_vcs_repo.tags.items()
43 data = self.load_refs_context(
41 data = self.load_refs_context(
44 ref_items=ref_items, partials_template='tags/tags_data.mako')
42 ref_items=ref_items, partials_template='tags/tags_data.mako')
45
43
46 c.has_references = bool(data)
44 c.has_references = bool(data)
47 c.data = ext_json.str_json(data)
45 c.data = ext_json.str_json(data)
48 return self._get_template_context(c)
46 return self._get_template_context(c)
@@ -1,62 +1,60 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
18 from rhodecode.apps._base import ADMIN_PREFIX
21
19
22
20
23 def includeme(config):
21 def includeme(config):
24 from rhodecode.apps.search.views import (
22 from rhodecode.apps.search.views import (
25 SearchView, SearchRepoView, SearchRepoGroupView)
23 SearchView, SearchRepoView, SearchRepoGroupView)
26
24
27 config.add_route(
25 config.add_route(
28 name='search',
26 name='search',
29 pattern=ADMIN_PREFIX + '/search')
27 pattern=ADMIN_PREFIX + '/search')
30 config.add_view(
28 config.add_view(
31 SearchView,
29 SearchView,
32 attr='search',
30 attr='search',
33 route_name='search', request_method='GET',
31 route_name='search', request_method='GET',
34 renderer='rhodecode:templates/search/search.mako')
32 renderer='rhodecode:templates/search/search.mako')
35
33
36 config.add_route(
34 config.add_route(
37 name='search_repo',
35 name='search_repo',
38 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
36 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
39 config.add_view(
37 config.add_view(
40 SearchRepoView,
38 SearchRepoView,
41 attr='search_repo',
39 attr='search_repo',
42 route_name='search_repo', request_method='GET',
40 route_name='search_repo', request_method='GET',
43 renderer='rhodecode:templates/search/search.mako')
41 renderer='rhodecode:templates/search/search.mako')
44
42
45 config.add_route(
43 config.add_route(
46 name='search_repo_alt',
44 name='search_repo_alt',
47 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
45 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
48 config.add_view(
46 config.add_view(
49 SearchRepoView,
47 SearchRepoView,
50 attr='search_repo',
48 attr='search_repo',
51 route_name='search_repo_alt', request_method='GET',
49 route_name='search_repo_alt', request_method='GET',
52 renderer='rhodecode:templates/search/search.mako')
50 renderer='rhodecode:templates/search/search.mako')
53
51
54 config.add_route(
52 config.add_route(
55 name='search_repo_group',
53 name='search_repo_group',
56 pattern='/{repo_group_name:.*?[^/]}/_search',
54 pattern='/{repo_group_name:.*?[^/]}/_search',
57 repo_group_route=True)
55 repo_group_route=True)
58 config.add_view(
56 config.add_view(
59 SearchRepoGroupView,
57 SearchRepoGroupView,
60 attr='search_repo_group',
58 attr='search_repo_group',
61 route_name='search_repo_group', request_method='GET',
59 route_name='search_repo_group', request_method='GET',
62 renderer='rhodecode:templates/search/search.mako')
60 renderer='rhodecode:templates/search/search.mako')
@@ -1,202 +1,201 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import os
19 import os
21
20
22 import mock
21 import mock
23 import pytest
22 import pytest
24 from whoosh import query
23 from whoosh import query
25
24
26 from rhodecode.tests import (
25 from rhodecode.tests import (
27 TestController, route_path_generator, HG_REPO,
26 TestController, route_path_generator, HG_REPO,
28 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
29 from rhodecode.tests.utils import AssertResponse
28 from rhodecode.tests.utils import AssertResponse
30
29
31
30
32 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
33 from rhodecode.apps._base import ADMIN_PREFIX
32 from rhodecode.apps._base import ADMIN_PREFIX
34 url_defs = {
33 url_defs = {
35 'search':
34 'search':
36 ADMIN_PREFIX + '/search',
35 ADMIN_PREFIX + '/search',
37 'search_repo':
36 'search_repo':
38 '/{repo_name}/search',
37 '/{repo_name}/search',
39 }
38 }
40 return route_path_generator(url_defs, name=name, params=params, **kwargs)
39 return route_path_generator(url_defs, name=name, params=params, **kwargs)
41
40
42
41
43 class TestSearchController(TestController):
42 class TestSearchController(TestController):
44
43
45 def test_index(self):
44 def test_index(self):
46 self.log_user()
45 self.log_user()
47 response = self.app.get(route_path('search'))
46 response = self.app.get(route_path('search'))
48 assert_response = response.assert_response()
47 assert_response = response.assert_response()
49 assert_response.one_element_exists('input#q')
48 assert_response.one_element_exists('input#q')
50
49
51 def test_search_files_empty_search(self):
50 def test_search_files_empty_search(self):
52 if os.path.isdir(self.index_location):
51 if os.path.isdir(self.index_location):
53 pytest.skip('skipped due to existing index')
52 pytest.skip('skipped due to existing index')
54 else:
53 else:
55 self.log_user()
54 self.log_user()
56 response = self.app.get(route_path('search'),
55 response = self.app.get(route_path('search'),
57 {'q': HG_REPO})
56 {'q': HG_REPO})
58 response.mustcontain('There is no index to search in. '
57 response.mustcontain('There is no index to search in. '
59 'Please run whoosh indexer')
58 'Please run whoosh indexer')
60
59
61 def test_search_validation(self):
60 def test_search_validation(self):
62 self.log_user()
61 self.log_user()
63 response = self.app.get(route_path('search'),
62 response = self.app.get(route_path('search'),
64 {'q': query, 'type': 'content', 'page_limit': 1000})
63 {'q': query, 'type': 'content', 'page_limit': 1000})
65
64
66 response.mustcontain(
65 response.mustcontain(
67 'page_limit - 1000 is greater than maximum value 500')
66 'page_limit - 1000 is greater than maximum value 500')
68
67
69 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
68 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
70 ('todo', 23, [
69 ('todo', 23, [
71 'vcs/backends/hg/inmemory.py',
70 'vcs/backends/hg/inmemory.py',
72 'vcs/tests/test_git.py']),
71 'vcs/tests/test_git.py']),
73 ('extension:rst installation', 6, [
72 ('extension:rst installation', 6, [
74 'docs/index.rst',
73 'docs/index.rst',
75 'docs/installation.rst']),
74 'docs/installation.rst']),
76 ('def repo', 87, [
75 ('def repo', 87, [
77 'vcs/tests/test_git.py',
76 'vcs/tests/test_git.py',
78 'vcs/tests/test_changesets.py']),
77 'vcs/tests/test_changesets.py']),
79 ('repository:%s def test' % HG_REPO, 18, [
78 ('repository:%s def test' % HG_REPO, 18, [
80 'vcs/tests/test_git.py',
79 'vcs/tests/test_git.py',
81 'vcs/tests/test_changesets.py']),
80 'vcs/tests/test_changesets.py']),
82 ('"def main"', 9, [
81 ('"def main"', 9, [
83 'vcs/__init__.py',
82 'vcs/__init__.py',
84 'vcs/tests/__init__.py',
83 'vcs/tests/__init__.py',
85 'vcs/utils/progressbar.py']),
84 'vcs/utils/progressbar.py']),
86 ('owner:test_admin', 358, [
85 ('owner:test_admin', 358, [
87 'vcs/tests/base.py',
86 'vcs/tests/base.py',
88 'MANIFEST.in',
87 'MANIFEST.in',
89 'vcs/utils/termcolors.py',
88 'vcs/utils/termcolors.py',
90 'docs/theme/ADC/static/documentation.png']),
89 'docs/theme/ADC/static/documentation.png']),
91 ('owner:test_admin def main', 72, [
90 ('owner:test_admin def main', 72, [
92 'vcs/__init__.py',
91 'vcs/__init__.py',
93 'vcs/tests/test_utils_filesize.py',
92 'vcs/tests/test_utils_filesize.py',
94 'vcs/tests/test_cli.py']),
93 'vcs/tests/test_cli.py']),
95 ('owner:michał test', 0, []),
94 ('owner:michał test', 0, []),
96 ])
95 ])
97 def test_search_files(self, query, expected_hits, expected_paths):
96 def test_search_files(self, query, expected_hits, expected_paths):
98 self.log_user()
97 self.log_user()
99 response = self.app.get(route_path('search'),
98 response = self.app.get(route_path('search'),
100 {'q': query, 'type': 'content', 'page_limit': 500})
99 {'q': query, 'type': 'content', 'page_limit': 500})
101
100
102 response.mustcontain('%s results' % expected_hits)
101 response.mustcontain('%s results' % expected_hits)
103 for path in expected_paths:
102 for path in expected_paths:
104 response.mustcontain(path)
103 response.mustcontain(path)
105
104
106 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
105 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
107 ('bother to ask where to fetch repo during tests', 3, [
106 ('bother to ask where to fetch repo during tests', 3, [
108 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
107 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
109 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
108 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
110 ('svn', '98')]),
109 ('svn', '98')]),
111 ('michał', 0, []),
110 ('michał', 0, []),
112 ('changed:tests/utils.py', 36, [
111 ('changed:tests/utils.py', 36, [
113 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
112 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
114 ('changed:vcs/utils/archivers.py', 11, [
113 ('changed:vcs/utils/archivers.py', 11, [
115 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
114 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
116 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
115 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
117 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
116 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
118 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
117 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
119 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
118 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
120 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
119 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
121 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
120 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
122 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
121 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
123 ('added:README.rst', 3, [
122 ('added:README.rst', 3, [
124 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
123 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
125 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
124 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
126 ('svn', '8')]),
125 ('svn', '8')]),
127 ('changed:lazy.py', 15, [
126 ('changed:lazy.py', 15, [
128 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
127 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
129 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
128 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
130 ('svn', '82'),
129 ('svn', '82'),
131 ('svn', '262'),
130 ('svn', '262'),
132 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
131 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
133 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
132 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
134 ]),
133 ]),
135 ('author:marcin@python-blog.com '
134 ('author:marcin@python-blog.com '
136 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
135 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
137 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
136 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
138 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
137 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
139 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
138 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
140 ('b986218b', 1, [
139 ('b986218b', 1, [
141 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
140 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
142 ])
141 ])
143 def test_search_commit_messages(
142 def test_search_commit_messages(
144 self, query, expected_hits, expected_commits, enabled_backends):
143 self, query, expected_hits, expected_commits, enabled_backends):
145 self.log_user()
144 self.log_user()
146 response = self.app.get(route_path('search'),
145 response = self.app.get(route_path('search'),
147 {'q': query, 'type': 'commit', 'page_limit': 500})
146 {'q': query, 'type': 'commit', 'page_limit': 500})
148
147
149 response.mustcontain('%s results' % expected_hits)
148 response.mustcontain('%s results' % expected_hits)
150 for backend, commit_id in expected_commits:
149 for backend, commit_id in expected_commits:
151 if backend in enabled_backends:
150 if backend in enabled_backends:
152 response.mustcontain(commit_id)
151 response.mustcontain(commit_id)
153
152
154 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
153 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
155 ('readme.rst', 3, []),
154 ('readme.rst', 3, []),
156 ('test*', 75, []),
155 ('test*', 75, []),
157 ('*model*', 1, []),
156 ('*model*', 1, []),
158 ('extension:rst', 48, []),
157 ('extension:rst', 48, []),
159 ('extension:rst api', 24, []),
158 ('extension:rst api', 24, []),
160 ])
159 ])
161 def test_search_file_paths(self, query, expected_hits, expected_paths):
160 def test_search_file_paths(self, query, expected_hits, expected_paths):
162 self.log_user()
161 self.log_user()
163 response = self.app.get(route_path('search'),
162 response = self.app.get(route_path('search'),
164 {'q': query, 'type': 'path', 'page_limit': 500})
163 {'q': query, 'type': 'path', 'page_limit': 500})
165
164
166 response.mustcontain('%s results' % expected_hits)
165 response.mustcontain('%s results' % expected_hits)
167 for path in expected_paths:
166 for path in expected_paths:
168 response.mustcontain(path)
167 response.mustcontain(path)
169
168
170 def test_search_commit_message_specific_repo(self, backend):
169 def test_search_commit_message_specific_repo(self, backend):
171 self.log_user()
170 self.log_user()
172 response = self.app.get(
171 response = self.app.get(
173 route_path('search_repo',repo_name=backend.repo_name),
172 route_path('search_repo',repo_name=backend.repo_name),
174 {'q': 'bother to ask where to fetch repo during tests',
173 {'q': 'bother to ask where to fetch repo during tests',
175 'type': 'commit'})
174 'type': 'commit'})
176
175
177 response.mustcontain('1 results')
176 response.mustcontain('1 results')
178
177
179 def test_filters_are_not_applied_for_admin_user(self):
178 def test_filters_are_not_applied_for_admin_user(self):
180 self.log_user()
179 self.log_user()
181 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
180 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
182
181
183 self.app.get(route_path('search'),
182 self.app.get(route_path('search'),
184 {'q': 'test query', 'type': 'commit'})
183 {'q': 'test query', 'type': 'commit'})
185 assert search_mock.call_count == 1
184 assert search_mock.call_count == 1
186 _, kwargs = search_mock.call_args
185 _, kwargs = search_mock.call_args
187 assert kwargs['filter'] is None
186 assert kwargs['filter'] is None
188
187
189 def test_filters_are_applied_for_normal_user(self, enabled_backends):
188 def test_filters_are_applied_for_normal_user(self, enabled_backends):
190 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
189 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
191 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
190 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
192 self.app.get(route_path('search'),
191 self.app.get(route_path('search'),
193 {'q': 'test query', 'type': 'commit'})
192 {'q': 'test query', 'type': 'commit'})
194 assert search_mock.call_count == 1
193 assert search_mock.call_count == 1
195 _, kwargs = search_mock.call_args
194 _, kwargs = search_mock.call_args
196 assert isinstance(kwargs['filter'], query.Or)
195 assert isinstance(kwargs['filter'], query.Or)
197 expected_repositories = [
196 expected_repositories = [
198 'vcs_test_{}'.format(b) for b in enabled_backends]
197 f'vcs_test_{b}' for b in enabled_backends]
199 queried_repositories = [
198 queried_repositories = [
200 name for type_, name in kwargs['filter'].all_terms()]
199 name for type_, name in kwargs['filter'].all_terms()]
201 for repository in expected_repositories:
200 for repository in expected_repositories:
202 assert repository in queried_repositories
201 assert repository in queried_repositories
@@ -1,168 +1,166 b''
1
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22 import urllib.request
20 import urllib.request
23 import urllib.parse
21 import urllib.parse
24 import urllib.error
22 import urllib.error
25
23
26 from webhelpers2.html.tools import update_params
24 from webhelpers2.html.tools import update_params
27
25
28 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
29 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
31 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.helpers import Page
32 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
33 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.model import validation_schema
32 from rhodecode.model import validation_schema
35 from rhodecode.model.validation_schema.schemas import search_schema
33 from rhodecode.model.validation_schema.schemas import search_schema
36
34
37 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
38
36
39
37
40 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
41 searcher = searcher_from_config(request.registry.settings)
39 searcher = searcher_from_config(request.registry.settings)
42 formatted_results = []
40 formatted_results = []
43 execution_time = ''
41 execution_time = ''
44
42
45 schema = search_schema.SearchParamsSchema()
43 schema = search_schema.SearchParamsSchema()
46 search_tags = []
44 search_tags = []
47 search_params = {}
45 search_params = {}
48 errors = []
46 errors = []
49
47
50 try:
48 try:
51 search_params = schema.deserialize(
49 search_params = schema.deserialize(
52 dict(
50 dict(
53 search_query=request.GET.get('q'),
51 search_query=request.GET.get('q'),
54 search_type=request.GET.get('type'),
52 search_type=request.GET.get('type'),
55 search_sort=request.GET.get('sort'),
53 search_sort=request.GET.get('sort'),
56 search_max_lines=request.GET.get('max_lines'),
54 search_max_lines=request.GET.get('max_lines'),
57 page_limit=request.GET.get('page_limit'),
55 page_limit=request.GET.get('page_limit'),
58 requested_page=request.GET.get('page'),
56 requested_page=request.GET.get('page'),
59 )
57 )
60 )
58 )
61 except validation_schema.Invalid as e:
59 except validation_schema.Invalid as e:
62 errors = e.children
60 errors = e.children
63
61
64 def url_generator(page_num):
62 def url_generator(page_num):
65
63
66 query_params = {
64 query_params = {
67 'page': page_num,
65 'page': page_num,
68 'q': safe_str(search_query),
66 'q': safe_str(search_query),
69 'type': safe_str(search_type),
67 'type': safe_str(search_type),
70 'max_lines': search_max_lines,
68 'max_lines': search_max_lines,
71 'sort': search_sort
69 'sort': search_sort
72 }
70 }
73
71
74 return '?' + urllib.parse.urlencode(query_params)
72 return '?' + urllib.parse.urlencode(query_params)
75
73
76 c = tmpl_context
74 c = tmpl_context
77 search_query = search_params.get('search_query')
75 search_query = search_params.get('search_query')
78 search_type = search_params.get('search_type')
76 search_type = search_params.get('search_type')
79 search_sort = search_params.get('search_sort')
77 search_sort = search_params.get('search_sort')
80 search_max_lines = search_params.get('search_max_lines')
78 search_max_lines = search_params.get('search_max_lines')
81 if search_params.get('search_query'):
79 if search_params.get('search_query'):
82 page_limit = search_params['page_limit']
80 page_limit = search_params['page_limit']
83 requested_page = search_params['requested_page']
81 requested_page = search_params['requested_page']
84
82
85 try:
83 try:
86 search_result = searcher.search(
84 search_result = searcher.search(
87 search_query, search_type, c.auth_user, repo_name, repo_group_name,
85 search_query, search_type, c.auth_user, repo_name, repo_group_name,
88 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
86 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
89
87
90 formatted_results = Page(
88 formatted_results = Page(
91 search_result['results'], page=requested_page,
89 search_result['results'], page=requested_page,
92 item_count=search_result['count'],
90 item_count=search_result['count'],
93 items_per_page=page_limit, url_maker=url_generator)
91 items_per_page=page_limit, url_maker=url_generator)
94 finally:
92 finally:
95 searcher.cleanup()
93 searcher.cleanup()
96
94
97 search_tags = searcher.extract_search_tags(search_query)
95 search_tags = searcher.extract_search_tags(search_query)
98
96
99 if not search_result['error']:
97 if not search_result['error']:
100 execution_time = '%s results (%.4f seconds)' % (
98 execution_time = '{} results ({:.4f} seconds)'.format(
101 search_result['count'],
99 search_result['count'],
102 search_result['runtime'])
100 search_result['runtime'])
103 elif not errors:
101 elif not errors:
104 node = schema['search_query']
102 node = schema['search_query']
105 errors = [
103 errors = [
106 validation_schema.Invalid(node, search_result['error'])]
104 validation_schema.Invalid(node, search_result['error'])]
107
105
108 c.perm_user = c.auth_user
106 c.perm_user = c.auth_user
109 c.repo_name = repo_name
107 c.repo_name = repo_name
110 c.repo_group_name = repo_group_name
108 c.repo_group_name = repo_group_name
111 c.errors = errors
109 c.errors = errors
112 c.formatted_results = formatted_results
110 c.formatted_results = formatted_results
113 c.runtime = execution_time
111 c.runtime = execution_time
114 c.cur_query = search_query
112 c.cur_query = search_query
115 c.search_type = search_type
113 c.search_type = search_type
116 c.searcher = searcher
114 c.searcher = searcher
117 c.search_tags = search_tags
115 c.search_tags = search_tags
118
116
119 direction, sort_field = searcher.get_sort(search_type, search_sort)
117 direction, sort_field = searcher.get_sort(search_type, search_sort)
120 sort_definition = searcher.sort_def(search_type, direction, sort_field)
118 sort_definition = searcher.sort_def(search_type, direction, sort_field)
121 c.sort = ''
119 c.sort = ''
122 c.sort_tag = None
120 c.sort_tag = None
123 c.sort_tag_dir = direction
121 c.sort_tag_dir = direction
124 if sort_definition:
122 if sort_definition:
125 c.sort = '{}:{}'.format(direction, sort_field)
123 c.sort = f'{direction}:{sort_field}'
126 c.sort_tag = sort_field
124 c.sort_tag = sort_field
127
125
128
126
129 class SearchView(BaseAppView):
127 class SearchView(BaseAppView):
130 def load_default_context(self):
128 def load_default_context(self):
131 c = self._get_local_tmpl_context()
129 c = self._get_local_tmpl_context()
132 return c
130 return c
133
131
134 @LoginRequired()
132 @LoginRequired()
135 def search(self):
133 def search(self):
136 c = self.load_default_context()
134 c = self.load_default_context()
137 perform_search(self.request, c)
135 perform_search(self.request, c)
138 return self._get_template_context(c)
136 return self._get_template_context(c)
139
137
140
138
141 class SearchRepoView(RepoAppView):
139 class SearchRepoView(RepoAppView):
142 def load_default_context(self):
140 def load_default_context(self):
143 c = self._get_local_tmpl_context()
141 c = self._get_local_tmpl_context()
144 c.active = 'search'
142 c.active = 'search'
145 return c
143 return c
146
144
147 @LoginRequired()
145 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
146 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
147 'repository.read', 'repository.write', 'repository.admin')
150 def search_repo(self):
148 def search_repo(self):
151 c = self.load_default_context()
149 c = self.load_default_context()
152 perform_search(self.request, c, repo_name=self.db_repo_name)
150 perform_search(self.request, c, repo_name=self.db_repo_name)
153 return self._get_template_context(c)
151 return self._get_template_context(c)
154
152
155
153
156 class SearchRepoGroupView(RepoGroupAppView):
154 class SearchRepoGroupView(RepoGroupAppView):
157 def load_default_context(self):
155 def load_default_context(self):
158 c = self._get_local_tmpl_context()
156 c = self._get_local_tmpl_context()
159 c.active = 'search'
157 c.active = 'search'
160 return c
158 return c
161
159
162 @LoginRequired()
160 @LoginRequired()
163 @HasRepoGroupPermissionAnyDecorator(
161 @HasRepoGroupPermissionAnyDecorator(
164 'group.read', 'group.write', 'group.admin')
162 'group.read', 'group.write', 'group.admin')
165 def search_repo_group(self):
163 def search_repo_group(self):
166 c = self.load_default_context()
164 c = self.load_default_context()
167 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
165 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
168 return self._get_template_context(c)
166 return self._get_template_context(c)
@@ -1,61 +1,59 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from . import config_keys
21 from . import config_keys
24 from .events import SshKeyFileChangeEvent
22 from .events import SshKeyFileChangeEvent
25 from .subscribers import generate_ssh_authorized_keys_file_subscriber
23 from .subscribers import generate_ssh_authorized_keys_file_subscriber
26
24
27 from rhodecode.config.settings_maker import SettingsMaker
25 from rhodecode.config.settings_maker import SettingsMaker
28
26
29 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
30
28
31
29
32 def _sanitize_settings_and_apply_defaults(settings):
30 def _sanitize_settings_and_apply_defaults(settings):
33 """
31 """
34 Set defaults, convert to python types and validate settings.
32 Set defaults, convert to python types and validate settings.
35 """
33 """
36 settings_maker = SettingsMaker(settings)
34 settings_maker = SettingsMaker(settings)
37
35
38 settings_maker.make_setting(config_keys.generate_authorized_keyfile, False, parser='bool')
36 settings_maker.make_setting(config_keys.generate_authorized_keyfile, False, parser='bool')
39 settings_maker.make_setting(config_keys.wrapper_allow_shell, False, parser='bool')
37 settings_maker.make_setting(config_keys.wrapper_allow_shell, False, parser='bool')
40 settings_maker.make_setting(config_keys.enable_debug_logging, False, parser='bool')
38 settings_maker.make_setting(config_keys.enable_debug_logging, False, parser='bool')
41 settings_maker.make_setting(config_keys.ssh_key_generator_enabled, True, parser='bool')
39 settings_maker.make_setting(config_keys.ssh_key_generator_enabled, True, parser='bool')
42
40
43 settings_maker.make_setting(config_keys.authorized_keys_file_path, '~/.ssh/authorized_keys_rhodecode')
41 settings_maker.make_setting(config_keys.authorized_keys_file_path, '~/.ssh/authorized_keys_rhodecode')
44 settings_maker.make_setting(config_keys.wrapper_cmd, '')
42 settings_maker.make_setting(config_keys.wrapper_cmd, '')
45 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
43 settings_maker.make_setting(config_keys.authorized_keys_line_ssh_opts, '')
46
44
47 settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg')
45 settings_maker.make_setting(config_keys.ssh_hg_bin, '~/.rccontrol/vcsserver-1/profile/bin/hg')
48 settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git')
46 settings_maker.make_setting(config_keys.ssh_git_bin, '~/.rccontrol/vcsserver-1/profile/bin/git')
49 settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve')
47 settings_maker.make_setting(config_keys.ssh_svn_bin, '~/.rccontrol/vcsserver-1/profile/bin/svnserve')
50
48
51 settings_maker.env_expand()
49 settings_maker.env_expand()
52
50
53
51
54 def includeme(config):
52 def includeme(config):
55 settings = config.registry.settings
53 settings = config.registry.settings
56 _sanitize_settings_and_apply_defaults(settings)
54 _sanitize_settings_and_apply_defaults(settings)
57
55
58 # if we have enable generation of file, subscribe to event
56 # if we have enable generation of file, subscribe to event
59 if settings[config_keys.generate_authorized_keyfile]:
57 if settings[config_keys.generate_authorized_keyfile]:
60 config.add_subscriber(
58 config.add_subscriber(
61 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
59 generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
@@ -1,34 +1,32 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 # Definition of setting keys used to configure this module. Defined here to
20 # Definition of setting keys used to configure this module. Defined here to
23 # avoid repetition of keys throughout the module.
21 # avoid repetition of keys throughout the module.
24 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
22 generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
25 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
23 authorized_keys_file_path = 'ssh.authorized_keys_file_path'
26 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
24 authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
27 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
25 ssh_key_generator_enabled = 'ssh.enable_ui_key_generator'
28 wrapper_cmd = 'ssh.wrapper_cmd'
26 wrapper_cmd = 'ssh.wrapper_cmd'
29 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
27 wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
30 enable_debug_logging = 'ssh.enable_debug_logging'
28 enable_debug_logging = 'ssh.enable_debug_logging'
31
29
32 ssh_hg_bin = 'ssh.executable.hg'
30 ssh_hg_bin = 'ssh.executable.hg'
33 ssh_git_bin = 'ssh.executable.git'
31 ssh_git_bin = 'ssh.executable.git'
34 ssh_svn_bin = 'ssh.executable.svn'
32 ssh_svn_bin = 'ssh.executable.svn'
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,163 +1,161 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import sys
20 import sys
23 import logging
21 import logging
24
22
25 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
23 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.ext_json import sjson as json
24 from rhodecode.lib.ext_json import sjson as json
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
25 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 from rhodecode.model.scm import ScmModel
26 from rhodecode.model.scm import ScmModel
29
27
30 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
31
29
32
30
33 class VcsServer(object):
31 class VcsServer(object):
34 repo_user_agent = None # set in child classes
32 repo_user_agent = None # set in child classes
35 _path = None # set executable path for hg/git/svn binary
33 _path = None # set executable path for hg/git/svn binary
36 backend = None # set in child classes
34 backend = None # set in child classes
37 tunnel = None # subprocess handling tunnel
35 tunnel = None # subprocess handling tunnel
38 write_perms = ['repository.admin', 'repository.write']
36 write_perms = ['repository.admin', 'repository.write']
39 read_perms = ['repository.read', 'repository.admin', 'repository.write']
37 read_perms = ['repository.read', 'repository.admin', 'repository.write']
40
38
41 def __init__(self, user, user_permissions, config, env):
39 def __init__(self, user, user_permissions, config, env):
42 self.user = user
40 self.user = user
43 self.user_permissions = user_permissions
41 self.user_permissions = user_permissions
44 self.config = config
42 self.config = config
45 self.env = env
43 self.env = env
46 self.stdin = sys.stdin
44 self.stdin = sys.stdin
47
45
48 self.repo_name = None
46 self.repo_name = None
49 self.repo_mode = None
47 self.repo_mode = None
50 self.store = ''
48 self.store = ''
51 self.ini_path = ''
49 self.ini_path = ''
52
50
53 def _invalidate_cache(self, repo_name):
51 def _invalidate_cache(self, repo_name):
54 """
52 """
55 Set's cache for this repository for invalidation on next access
53 Set's cache for this repository for invalidation on next access
56
54
57 :param repo_name: full repo name, also a cache key
55 :param repo_name: full repo name, also a cache key
58 """
56 """
59 ScmModel().mark_for_invalidation(repo_name)
57 ScmModel().mark_for_invalidation(repo_name)
60
58
61 def has_write_perm(self):
59 def has_write_perm(self):
62 permission = self.user_permissions.get(self.repo_name)
60 permission = self.user_permissions.get(self.repo_name)
63 if permission in ['repository.write', 'repository.admin']:
61 if permission in ['repository.write', 'repository.admin']:
64 return True
62 return True
65
63
66 return False
64 return False
67
65
68 def _check_permissions(self, action):
66 def _check_permissions(self, action):
69 permission = self.user_permissions.get(self.repo_name)
67 permission = self.user_permissions.get(self.repo_name)
70 log.debug('permission for %s on %s are: %s',
68 log.debug('permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
69 self.user, self.repo_name, permission)
72
70
73 if not permission:
71 if not permission:
74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
72 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 self.user, self.repo_name)
73 self.user, self.repo_name)
76 return -2
74 return -2
77
75
78 if action == 'pull':
76 if action == 'pull':
79 if permission in self.read_perms:
77 if permission in self.read_perms:
80 log.info(
78 log.info(
81 'READ Permissions for User "%s" detected to repo "%s"!',
79 'READ Permissions for User "%s" detected to repo "%s"!',
82 self.user, self.repo_name)
80 self.user, self.repo_name)
83 return 0
81 return 0
84 else:
82 else:
85 if permission in self.write_perms:
83 if permission in self.write_perms:
86 log.info(
84 log.info(
87 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
85 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
88 self.user, self.repo_name)
86 self.user, self.repo_name)
89 return 0
87 return 0
90
88
91 log.error('Cannot properly fetch or verify user `%s` permissions. '
89 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 'Permissions: %s, vcs action: %s',
90 'Permissions: %s, vcs action: %s',
93 self.user, permission, action)
91 self.user, permission, action)
94 return -2
92 return -2
95
93
96 def update_environment(self, action, extras=None):
94 def update_environment(self, action, extras=None):
97
95
98 scm_data = {
96 scm_data = {
99 'ip': os.environ['SSH_CLIENT'].split()[0],
97 'ip': os.environ['SSH_CLIENT'].split()[0],
100 'username': self.user.username,
98 'username': self.user.username,
101 'user_id': self.user.user_id,
99 'user_id': self.user.user_id,
102 'action': action,
100 'action': action,
103 'repository': self.repo_name,
101 'repository': self.repo_name,
104 'scm': self.backend,
102 'scm': self.backend,
105 'config': self.ini_path,
103 'config': self.ini_path,
106 'repo_store': self.store,
104 'repo_store': self.store,
107 'make_lock': None,
105 'make_lock': None,
108 'locked_by': [None, None],
106 'locked_by': [None, None],
109 'server_url': None,
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 'hooks': ['push', 'pull'],
109 'hooks': ['push', 'pull'],
112 'hooks_module': 'rhodecode.lib.hooks_daemon',
110 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 'is_shadow_repo': False,
111 'is_shadow_repo': False,
114 'detect_force_push': False,
112 'detect_force_push': False,
115 'check_branch_perms': False,
113 'check_branch_perms': False,
116
114
117 'SSH': True,
115 'SSH': True,
118 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
116 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 }
117 }
120 if extras:
118 if extras:
121 scm_data.update(extras)
119 scm_data.update(extras)
122 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
120 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123
121
124 def get_root_store(self):
122 def get_root_store(self):
125 root_store = self.store
123 root_store = self.store
126 if not root_store.endswith('/'):
124 if not root_store.endswith('/'):
127 # always append trailing slash
125 # always append trailing slash
128 root_store = root_store + '/'
126 root_store = root_store + '/'
129 return root_store
127 return root_store
130
128
131 def _handle_tunnel(self, extras):
129 def _handle_tunnel(self, extras):
132 # pre-auth
130 # pre-auth
133 action = 'pull'
131 action = 'pull'
134 exit_code = self._check_permissions(action)
132 exit_code = self._check_permissions(action)
135 if exit_code:
133 if exit_code:
136 return exit_code, False
134 return exit_code, False
137
135
138 req = self.env['request']
136 req = self.env['request']
139 server_url = req.host_url + req.script_name
137 server_url = req.host_url + req.script_name
140 extras['server_url'] = server_url
138 extras['server_url'] = server_url
141
139
142 log.debug('Using %s binaries from path %s', self.backend, self._path)
140 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 exit_code = self.tunnel.run(extras)
141 exit_code = self.tunnel.run(extras)
144
142
145 return exit_code, action == "push"
143 return exit_code, action == "push"
146
144
147 def run(self, tunnel_extras=None):
145 def run(self, tunnel_extras=None):
148 tunnel_extras = tunnel_extras or {}
146 tunnel_extras = tunnel_extras or {}
149 extras = {}
147 extras = {}
150 extras.update(tunnel_extras)
148 extras.update(tunnel_extras)
151
149
152 callback_daemon, extras = prepare_callback_daemon(
150 callback_daemon, extras = prepare_callback_daemon(
153 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
151 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 host=vcs_settings.HOOKS_HOST,
152 host=vcs_settings.HOOKS_HOST,
155 use_direct_calls=False)
153 use_direct_calls=False)
156
154
157 with callback_daemon:
155 with callback_daemon:
158 try:
156 try:
159 return self._handle_tunnel(extras)
157 return self._handle_tunnel(extras)
160 finally:
158 finally:
161 log.debug('Running cleanup with cache invalidation')
159 log.debug('Running cleanup with cache invalidation')
162 if self.repo_name:
160 if self.repo_name:
163 self._invalidate_cache(self.repo_name)
161 self._invalidate_cache(self.repo_name)
@@ -1,75 +1,73 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import sys
20 import sys
23 import logging
21 import logging
24
22
25 from .base import VcsServer
23 from .base import VcsServer
26
24
27 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
28
26
29
27
30 class GitTunnelWrapper(object):
28 class GitTunnelWrapper(object):
31 process = None
29 process = None
32
30
33 def __init__(self, server):
31 def __init__(self, server):
34 self.server = server
32 self.server = server
35 self.stdin = sys.stdin
33 self.stdin = sys.stdin
36 self.stdout = sys.stdout
34 self.stdout = sys.stdout
37
35
38 def create_hooks_env(self):
36 def create_hooks_env(self):
39 pass
37 pass
40
38
41 def command(self):
39 def command(self):
42 root = self.server.get_root_store()
40 root = self.server.get_root_store()
43 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
41 command = "cd {root}; {git_path} {mode} '{root}{repo_name}'".format(
44 root=root, git_path=self.server.git_path,
42 root=root, git_path=self.server.git_path,
45 mode=self.server.repo_mode, repo_name=self.server.repo_name)
43 mode=self.server.repo_mode, repo_name=self.server.repo_name)
46 log.debug("Final CMD: %s", command)
44 log.debug("Final CMD: %s", command)
47 return command
45 return command
48
46
49 def run(self, extras):
47 def run(self, extras):
50 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
48 action = "push" if self.server.repo_mode == "receive-pack" else "pull"
51 exit_code = self.server._check_permissions(action)
49 exit_code = self.server._check_permissions(action)
52 if exit_code:
50 if exit_code:
53 return exit_code
51 return exit_code
54
52
55 self.server.update_environment(action=action, extras=extras)
53 self.server.update_environment(action=action, extras=extras)
56 self.create_hooks_env()
54 self.create_hooks_env()
57 return os.system(self.command())
55 return os.system(self.command())
58
56
59
57
60 class GitServer(VcsServer):
58 class GitServer(VcsServer):
61 backend = 'git'
59 backend = 'git'
62 repo_user_agent = 'git'
60 repo_user_agent = 'git'
63
61
64 def __init__(self, store, ini_path, repo_name, repo_mode,
62 def __init__(self, store, ini_path, repo_name, repo_mode,
65 user, user_permissions, config, env):
63 user, user_permissions, config, env):
66 super(GitServer, self).\
64 super().\
67 __init__(user, user_permissions, config, env)
65 __init__(user, user_permissions, config, env)
68
66
69 self.store = store
67 self.store = store
70 self.ini_path = ini_path
68 self.ini_path = ini_path
71 self.repo_name = repo_name
69 self.repo_name = repo_name
72 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
70 self._path = self.git_path = config.get('app:main', 'ssh.executable.git')
73
71
74 self.repo_mode = repo_mode
72 self.repo_mode = repo_mode
75 self.tunnel = GitTunnelWrapper(server=self)
73 self.tunnel = GitTunnelWrapper(server=self)
@@ -1,148 +1,146 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import sys
20 import sys
23 import logging
21 import logging
24 import tempfile
22 import tempfile
25 import textwrap
23 import textwrap
26 import collections
24 import collections
27 from .base import VcsServer
25 from .base import VcsServer
28 from rhodecode.model.db import RhodeCodeUi
26 from rhodecode.model.db import RhodeCodeUi
29 from rhodecode.model.settings import VcsSettingsModel
27 from rhodecode.model.settings import VcsSettingsModel
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class MercurialTunnelWrapper(object):
32 class MercurialTunnelWrapper(object):
35 process = None
33 process = None
36
34
37 def __init__(self, server):
35 def __init__(self, server):
38 self.server = server
36 self.server = server
39 self.stdin = sys.stdin
37 self.stdin = sys.stdin
40 self.stdout = sys.stdout
38 self.stdout = sys.stdout
41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
39 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
42
40
43 def create_hooks_env(self):
41 def create_hooks_env(self):
44 repo_name = self.server.repo_name
42 repo_name = self.server.repo_name
45 hg_flags = self.server.config_to_hgrc(repo_name)
43 hg_flags = self.server.config_to_hgrc(repo_name)
46
44
47 content = textwrap.dedent(
45 content = textwrap.dedent(
48 '''
46 '''
49 # RhodeCode SSH hooks version=2.0.0
47 # RhodeCode SSH hooks version=2.0.0
50 {custom}
48 {custom}
51 '''
49 '''
52 ).format(custom='\n'.join(hg_flags))
50 ).format(custom='\n'.join(hg_flags))
53
51
54 root = self.server.get_root_store()
52 root = self.server.get_root_store()
55 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
53 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
56 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
54 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
57
55
58 # cleanup custom hgrc file
56 # cleanup custom hgrc file
59 if os.path.isfile(hgrc_custom):
57 if os.path.isfile(hgrc_custom):
60 with open(hgrc_custom, 'wb') as f:
58 with open(hgrc_custom, 'wb') as f:
61 f.write('')
59 f.write('')
62 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
60 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
63
61
64 # write temp
62 # write temp
65 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
63 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
66 hooks_env_file.write(content)
64 hooks_env_file.write(content)
67
65
68 return self.hooks_env_path
66 return self.hooks_env_path
69
67
70 def remove_configs(self):
68 def remove_configs(self):
71 os.remove(self.hooks_env_path)
69 os.remove(self.hooks_env_path)
72
70
73 def command(self, hgrc_path):
71 def command(self, hgrc_path):
74 root = self.server.get_root_store()
72 root = self.server.get_root_store()
75
73
76 command = (
74 command = (
77 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
75 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
78 "serve --stdio".format(
76 "serve --stdio".format(
79 root=root, hg_path=self.server.hg_path,
77 root=root, hg_path=self.server.hg_path,
80 repo_name=self.server.repo_name, hgrc=hgrc_path))
78 repo_name=self.server.repo_name, hgrc=hgrc_path))
81 log.debug("Final CMD: %s", command)
79 log.debug("Final CMD: %s", command)
82 return command
80 return command
83
81
84 def run(self, extras):
82 def run(self, extras):
85 # at this point we cannot tell, we do further ACL checks
83 # at this point we cannot tell, we do further ACL checks
86 # inside the hooks
84 # inside the hooks
87 action = '?'
85 action = '?'
88 # permissions are check via `pre_push_ssh_auth` hook
86 # permissions are check via `pre_push_ssh_auth` hook
89 self.server.update_environment(action=action, extras=extras)
87 self.server.update_environment(action=action, extras=extras)
90 custom_hgrc_file = self.create_hooks_env()
88 custom_hgrc_file = self.create_hooks_env()
91
89
92 try:
90 try:
93 return os.system(self.command(custom_hgrc_file))
91 return os.system(self.command(custom_hgrc_file))
94 finally:
92 finally:
95 self.remove_configs()
93 self.remove_configs()
96
94
97
95
98 class MercurialServer(VcsServer):
96 class MercurialServer(VcsServer):
99 backend = 'hg'
97 backend = 'hg'
100 repo_user_agent = 'mercurial'
98 repo_user_agent = 'mercurial'
101 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
99 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
102
100
103 def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
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 self.store = store
104 self.store = store
107 self.ini_path = ini_path
105 self.ini_path = ini_path
108 self.repo_name = repo_name
106 self.repo_name = repo_name
109 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
107 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
110 self.tunnel = MercurialTunnelWrapper(server=self)
108 self.tunnel = MercurialTunnelWrapper(server=self)
111
109
112 def config_to_hgrc(self, repo_name):
110 def config_to_hgrc(self, repo_name):
113 ui_sections = collections.defaultdict(list)
111 ui_sections = collections.defaultdict(list)
114 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
112 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
115
113
116 # write default hooks
114 # write default hooks
117 default_hooks = [
115 default_hooks = [
118 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
116 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
119 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
117 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
120 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
118 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
121
119
122 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
120 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
123 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
121 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
124 ]
122 ]
125
123
126 for k, v in default_hooks:
124 for k, v in default_hooks:
127 ui_sections['hooks'].append((k, v))
125 ui_sections['hooks'].append((k, v))
128
126
129 for entry in ui:
127 for entry in ui:
130 if not entry.active:
128 if not entry.active:
131 continue
129 continue
132 sec = entry.section
130 sec = entry.section
133 key = entry.key
131 key = entry.key
134
132
135 if sec in self.cli_flags:
133 if sec in self.cli_flags:
136 # we want only custom hooks, so we skip builtins
134 # we want only custom hooks, so we skip builtins
137 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
135 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
138 continue
136 continue
139
137
140 ui_sections[sec].append([key, entry.value])
138 ui_sections[sec].append([key, entry.value])
141
139
142 flags = []
140 flags = []
143 for _sec, key_val in ui_sections.items():
141 for _sec, key_val in ui_sections.items():
144 flags.append(' ')
142 flags.append(' ')
145 flags.append('[{}]'.format(_sec))
143 flags.append(f'[{_sec}]')
146 for key, val in key_val:
144 for key, val in key_val:
147 flags.append('{}= {}'.format(key, val))
145 flags.append(f'{key}= {val}')
148 return flags
146 return flags
@@ -1,258 +1,256 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import re
20 import re
23 import sys
21 import sys
24 import logging
22 import logging
25 import signal
23 import signal
26 import tempfile
24 import tempfile
27 from subprocess import Popen, PIPE
25 from subprocess import Popen, PIPE
28 import urllib.parse
26 import urllib.parse
29
27
30 from .base import VcsServer
28 from .base import VcsServer
31
29
32 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
33
31
34
32
35 class SubversionTunnelWrapper(object):
33 class SubversionTunnelWrapper(object):
36 process = None
34 process = None
37
35
38 def __init__(self, server):
36 def __init__(self, server):
39 self.server = server
37 self.server = server
40 self.timeout = 30
38 self.timeout = 30
41 self.stdin = sys.stdin
39 self.stdin = sys.stdin
42 self.stdout = sys.stdout
40 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
41 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
42 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
43
46 self.read_only = True # flag that we set to make the hooks readonly
44 self.read_only = True # flag that we set to make the hooks readonly
47
45
48 def create_svn_config(self):
46 def create_svn_config(self):
49 content = (
47 content = (
50 '[general]\n'
48 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
49 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
50 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
51 config_file.write(content)
54
52
55 def create_hooks_env(self):
53 def create_hooks_env(self):
56 content = (
54 content = (
57 '[default]\n'
55 '[default]\n'
58 'LANG = en_US.UTF-8\n')
56 'LANG = en_US.UTF-8\n')
59 if self.read_only:
57 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
58 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
59 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
60 hooks_env_file.write(content)
63
61
64 def remove_configs(self):
62 def remove_configs(self):
65 os.remove(self.svn_conf_path)
63 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
64 os.remove(self.hooks_env_path)
67
65
68 def command(self):
66 def command(self):
69 root = self.server.get_root_store()
67 root = self.server.get_root_store()
70 username = self.server.user.username
68 username = self.server.user.username
71
69
72 command = [
70 command = [
73 self.server.svn_path, '-t',
71 self.server.svn_path, '-t',
74 '--config-file', self.svn_conf_path,
72 '--config-file', self.svn_conf_path,
75 '--tunnel-user', username,
73 '--tunnel-user', username,
76 '-r', root]
74 '-r', root]
77 log.debug("Final CMD: %s", ' '.join(command))
75 log.debug("Final CMD: %s", ' '.join(command))
78 return command
76 return command
79
77
80 def start(self):
78 def start(self):
81 command = self.command()
79 command = self.command()
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
83
81
84 def sync(self):
82 def sync(self):
85 while self.process.poll() is None:
83 while self.process.poll() is None:
86 next_byte = self.stdin.read(1)
84 next_byte = self.stdin.read(1)
87 if not next_byte:
85 if not next_byte:
88 break
86 break
89 self.process.stdin.write(next_byte)
87 self.process.stdin.write(next_byte)
90 self.remove_configs()
88 self.remove_configs()
91
89
92 @property
90 @property
93 def return_code(self):
91 def return_code(self):
94 return self.process.returncode
92 return self.process.returncode
95
93
96 def get_first_client_response(self):
94 def get_first_client_response(self):
97 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.signal(signal.SIGALRM, self.interrupt)
98 signal.alarm(self.timeout)
96 signal.alarm(self.timeout)
99 first_response = self._read_first_client_response()
97 first_response = self._read_first_client_response()
100 signal.alarm(0)
98 signal.alarm(0)
101 return (self._parse_first_client_response(first_response)
99 return (self._parse_first_client_response(first_response)
102 if first_response else None)
100 if first_response else None)
103
101
104 def patch_first_client_response(self, response, **kwargs):
102 def patch_first_client_response(self, response, **kwargs):
105 self.create_hooks_env()
103 self.create_hooks_env()
106 data = response.copy()
104 data = response.copy()
107 data.update(kwargs)
105 data.update(kwargs)
108 data['url'] = self._svn_string(data['url'])
106 data['url'] = self._svn_string(data['url'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
110 data['client'] = data['client'] or ''
108 data['client'] = data['client'] or ''
111 buffer_ = (
109 buffer_ = (
112 "( {version} ( {capabilities} ) {url}{ra_client}"
110 "( {version} ( {capabilities} ) {url}{ra_client}"
113 "( {client}) ) ".format(**data))
111 "( {client}) ) ".format(**data))
114 self.process.stdin.write(buffer_)
112 self.process.stdin.write(buffer_)
115
113
116 def fail(self, message):
114 def fail(self, message):
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
115 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
118 message=self._svn_string(message)))
116 message=self._svn_string(message)))
119 self.remove_configs()
117 self.remove_configs()
120 self.process.kill()
118 self.process.kill()
121 return 1
119 return 1
122
120
123 def interrupt(self, signum, frame):
121 def interrupt(self, signum, frame):
124 self.fail("Exited by timeout")
122 self.fail("Exited by timeout")
125
123
126 def _svn_string(self, str_):
124 def _svn_string(self, str_):
127 if not str_:
125 if not str_:
128 return ''
126 return ''
129 return '{length}:{string} '.format(length=len(str_), string=str_)
127 return f'{len(str_)}:{str_} '
130
128
131 def _read_first_client_response(self):
129 def _read_first_client_response(self):
132 buffer_ = ""
130 buffer_ = ""
133 brackets_stack = []
131 brackets_stack = []
134 while True:
132 while True:
135 next_byte = self.stdin.read(1)
133 next_byte = self.stdin.read(1)
136 buffer_ += next_byte
134 buffer_ += next_byte
137 if next_byte == "(":
135 if next_byte == "(":
138 brackets_stack.append(next_byte)
136 brackets_stack.append(next_byte)
139 elif next_byte == ")":
137 elif next_byte == ")":
140 brackets_stack.pop()
138 brackets_stack.pop()
141 elif next_byte == " " and not brackets_stack:
139 elif next_byte == " " and not brackets_stack:
142 break
140 break
143
141
144 return buffer_
142 return buffer_
145
143
146 def _parse_first_client_response(self, buffer_):
144 def _parse_first_client_response(self, buffer_):
147 """
145 """
148 According to the Subversion RA protocol, the first request
146 According to the Subversion RA protocol, the first request
149 should look like:
147 should look like:
150
148
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( version:number ( cap:word ... ) url:string ? ra-client:string
152 ( ? client:string ) )
150 ( ? client:string ) )
153
151
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
152 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
155 """
153 """
156 version_re = r'(?P<version>\d+)'
154 version_re = r'(?P<version>\d+)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
161 regex = re.compile(
159 regex = re.compile(
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
163 r'\(\s{client}\)\s\)\s*$'.format(
161 r'\(\s{client}\)\s\)\s*$'.format(
164 version=version_re, capabilities=capabilities_re,
162 version=version_re, capabilities=capabilities_re,
165 url=url_re, ra_client=ra_client_re, client=client_re))
163 url=url_re, ra_client=ra_client_re, client=client_re))
166 matcher = regex.match(buffer_)
164 matcher = regex.match(buffer_)
167
165
168 return matcher.groupdict() if matcher else None
166 return matcher.groupdict() if matcher else None
169
167
170 def _match_repo_name(self, url):
168 def _match_repo_name(self, url):
171 """
169 """
172 Given an server url, try to match it against ALL known repository names.
170 Given an server url, try to match it against ALL known repository names.
173 This handles a tricky SVN case for SSH and subdir commits.
171 This handles a tricky SVN case for SSH and subdir commits.
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
172 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
175 result in the url with this subdir added.
173 result in the url with this subdir added.
176 """
174 """
177 # case 1 direct match, we don't do any "heavy" lookups
175 # case 1 direct match, we don't do any "heavy" lookups
178 if url in self.server.user_permissions:
176 if url in self.server.user_permissions:
179 return url
177 return url
180
178
181 log.debug('Extracting repository name from subdir path %s', url)
179 log.debug('Extracting repository name from subdir path %s', url)
182 # case 2 we check all permissions, and match closes possible case...
180 # case 2 we check all permissions, and match closes possible case...
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
181 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
184 # to assume that it will have the repo name as prefix, we ensure the prefix
182 # to assume that it will have the repo name as prefix, we ensure the prefix
185 # for similar repositories isn't matched by adding a /
183 # for similar repositories isn't matched by adding a /
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
184 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
187 for repo_name in self.server.user_permissions:
185 for repo_name in self.server.user_permissions:
188 repo_name_prefix = repo_name + '/'
186 repo_name_prefix = repo_name + '/'
189 if url.startswith(repo_name_prefix):
187 if url.startswith(repo_name_prefix):
190 log.debug('Found prefix %s match, returning proper repository name',
188 log.debug('Found prefix %s match, returning proper repository name',
191 repo_name_prefix)
189 repo_name_prefix)
192 return repo_name
190 return repo_name
193
191
194 return
192 return
195
193
196 def run(self, extras):
194 def run(self, extras):
197 action = 'pull'
195 action = 'pull'
198 self.create_svn_config()
196 self.create_svn_config()
199 self.start()
197 self.start()
200
198
201 first_response = self.get_first_client_response()
199 first_response = self.get_first_client_response()
202 if not first_response:
200 if not first_response:
203 return self.fail("Repository name cannot be extracted")
201 return self.fail("Repository name cannot be extracted")
204
202
205 url_parts = urllib.parse.urlparse(first_response['url'])
203 url_parts = urllib.parse.urlparse(first_response['url'])
206
204
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
205 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
208
206
209 exit_code = self.server._check_permissions(action)
207 exit_code = self.server._check_permissions(action)
210 if exit_code:
208 if exit_code:
211 return exit_code
209 return exit_code
212
210
213 # set the readonly flag to False if we have proper permissions
211 # set the readonly flag to False if we have proper permissions
214 if self.server.has_write_perm():
212 if self.server.has_write_perm():
215 self.read_only = False
213 self.read_only = False
216 self.server.update_environment(action=action, extras=extras)
214 self.server.update_environment(action=action, extras=extras)
217
215
218 self.patch_first_client_response(first_response)
216 self.patch_first_client_response(first_response)
219 self.sync()
217 self.sync()
220 return self.return_code
218 return self.return_code
221
219
222
220
223 class SubversionServer(VcsServer):
221 class SubversionServer(VcsServer):
224 backend = 'svn'
222 backend = 'svn'
225 repo_user_agent = 'svn'
223 repo_user_agent = 'svn'
226
224
227 def __init__(self, store, ini_path, repo_name,
225 def __init__(self, store, ini_path, repo_name,
228 user, user_permissions, config, env):
226 user, user_permissions, config, env):
229 super(SubversionServer, self)\
227 super()\
230 .__init__(user, user_permissions, config, env)
228 .__init__(user, user_permissions, config, env)
231 self.store = store
229 self.store = store
232 self.ini_path = ini_path
230 self.ini_path = ini_path
233 # NOTE(dan): repo_name at this point is empty,
231 # NOTE(dan): repo_name at this point is empty,
234 # this is set later in .run() based from parsed input stream
232 # this is set later in .run() based from parsed input stream
235 self.repo_name = repo_name
233 self.repo_name = repo_name
236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
234 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
237
235
238 self.tunnel = SubversionTunnelWrapper(server=self)
236 self.tunnel = SubversionTunnelWrapper(server=self)
239
237
240 def _handle_tunnel(self, extras):
238 def _handle_tunnel(self, extras):
241
239
242 # pre-auth
240 # pre-auth
243 action = 'pull'
241 action = 'pull'
244 # Special case for SVN, we extract repo name at later stage
242 # Special case for SVN, we extract repo name at later stage
245 # exit_code = self._check_permissions(action)
243 # exit_code = self._check_permissions(action)
246 # if exit_code:
244 # if exit_code:
247 # return exit_code, False
245 # return exit_code, False
248
246
249 req = self.env['request']
247 req = self.env['request']
250 server_url = req.host_url + req.script_name
248 server_url = req.host_url + req.script_name
251 extras['server_url'] = server_url
249 extras['server_url'] = server_url
252
250
253 log.debug('Using %s binaries from path %s', self.backend, self._path)
251 log.debug('Using %s binaries from path %s', self.backend, self._path)
254 exit_code = self.tunnel.run(extras)
252 exit_code = self.tunnel.run(extras)
255
253
256 return exit_code, action == "push"
254 return exit_code, action == "push"
257
255
258
256
@@ -1,81 +1,79 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import sys
20 import sys
23 import logging
21 import logging
24
22
25 import click
23 import click
26
24
27 from pyramid.paster import setup_logging
25 from pyramid.paster import setup_logging
28
26
29 from rhodecode.lib.pyramid_utils import bootstrap
27 from rhodecode.lib.pyramid_utils import bootstrap
30 from .backends import SshWrapper
28 from .backends import SshWrapper
31
29
32 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
33
31
34
32
35 def setup_custom_logging(ini_path, debug):
33 def setup_custom_logging(ini_path, debug):
36 if debug:
34 if debug:
37 # enabled rhodecode.ini controlled logging setup
35 # enabled rhodecode.ini controlled logging setup
38 setup_logging(ini_path)
36 setup_logging(ini_path)
39 else:
37 else:
40 # configure logging in a mode that doesn't print anything.
38 # configure logging in a mode that doesn't print anything.
41 # in case of regularly configured logging it gets printed out back
39 # in case of regularly configured logging it gets printed out back
42 # to the client doing an SSH command.
40 # to the client doing an SSH command.
43 logger = logging.getLogger('')
41 logger = logging.getLogger('')
44 null = logging.NullHandler()
42 null = logging.NullHandler()
45 # add the handler to the root logger
43 # add the handler to the root logger
46 logger.handlers = [null]
44 logger.handlers = [null]
47
45
48
46
49 @click.command()
47 @click.command()
50 @click.argument('ini_path', type=click.Path(exists=True))
48 @click.argument('ini_path', type=click.Path(exists=True))
51 @click.option(
49 @click.option(
52 '--mode', '-m', required=False, default='auto',
50 '--mode', '-m', required=False, default='auto',
53 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
51 type=click.Choice(['auto', 'vcs', 'git', 'hg', 'svn', 'test']),
54 help='mode of operation')
52 help='mode of operation')
55 @click.option('--user', help='Username for which the command will be executed')
53 @click.option('--user', help='Username for which the command will be executed')
56 @click.option('--user-id', help='User ID for which the command will be executed')
54 @click.option('--user-id', help='User ID for which the command will be executed')
57 @click.option('--key-id', help='ID of the key from the database')
55 @click.option('--key-id', help='ID of the key from the database')
58 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
56 @click.option('--shell', '-s', is_flag=True, help='Allow Shell')
59 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
57 @click.option('--debug', is_flag=True, help='Enabled detailed output logging')
60 def main(ini_path, mode, user, user_id, key_id, shell, debug):
58 def main(ini_path, mode, user, user_id, key_id, shell, debug):
61 setup_custom_logging(ini_path, debug)
59 setup_custom_logging(ini_path, debug)
62
60
63 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
61 command = os.environ.get('SSH_ORIGINAL_COMMAND', '')
64 if not command and mode not in ['test']:
62 if not command and mode not in ['test']:
65 raise ValueError(
63 raise ValueError(
66 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
64 'Unable to fetch SSH_ORIGINAL_COMMAND from environment.'
67 'Please make sure this is set and available during execution '
65 'Please make sure this is set and available during execution '
68 'of this script.')
66 'of this script.')
69 connection_info = os.environ.get('SSH_CONNECTION', '')
67 connection_info = os.environ.get('SSH_CONNECTION', '')
70
68
71 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
69 with bootstrap(ini_path, env={'RC_CMD_SSH_WRAPPER': '1'}) as env:
72 try:
70 try:
73 ssh_wrapper = SshWrapper(
71 ssh_wrapper = SshWrapper(
74 command, connection_info, mode,
72 command, connection_info, mode,
75 user, user_id, key_id, shell, ini_path, env)
73 user, user_id, key_id, shell, ini_path, env)
76 except Exception:
74 except Exception:
77 log.exception('Failed to execute SshWrapper')
75 log.exception('Failed to execute SshWrapper')
78 sys.exit(-5)
76 sys.exit(-5)
79
77
80 return_code = ssh_wrapper.wrap()
78 return_code = ssh_wrapper.wrap()
81 sys.exit(return_code)
79 sys.exit(return_code)
@@ -1,36 +1,34 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from .utils import generate_ssh_authorized_keys_file
22 from .utils import generate_ssh_authorized_keys_file
25
23
26
24
27 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
28
26
29
27
30 def generate_ssh_authorized_keys_file_subscriber(event):
28 def generate_ssh_authorized_keys_file_subscriber(event):
31 """
29 """
32 Subscriber to the `SshKeyFileChangeEvent`. This triggers the
30 Subscriber to the `SshKeyFileChangeEvent`. This triggers the
33 automatic generation of authorized_keys file on any change in
31 automatic generation of authorized_keys file on any change in
34 ssh keys management
32 ssh keys management
35 """
33 """
36 generate_ssh_authorized_keys_file(event.request.registry)
34 generate_ssh_authorized_keys_file(event.request.registry)
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,70 +1,68 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import pytest
20 import pytest
23 import configparser
21 import configparser
24
22
25 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
23 from rhodecode.apps.ssh_support.lib.ssh_wrapper import SshWrapper
26 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
27
25
28
26
29 @pytest.fixture()
27 @pytest.fixture()
30 def dummy_conf_file(tmpdir):
28 def dummy_conf_file(tmpdir):
31 conf = configparser.ConfigParser()
29 conf = configparser.ConfigParser()
32 conf.add_section('app:main')
30 conf.add_section('app:main')
33 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
31 conf.set('app:main', 'ssh.executable.hg', '/usr/bin/hg')
34 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
32 conf.set('app:main', 'ssh.executable.git', '/usr/bin/git')
35 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
33 conf.set('app:main', 'ssh.executable.svn', '/usr/bin/svnserve')
36
34
37 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
35 f_path = os.path.join(str(tmpdir), 'ssh_wrapper_test.ini')
38 with open(f_path, 'wt') as f:
36 with open(f_path, 'wt') as f:
39 conf.write(f)
37 conf.write(f)
40
38
41 return os.path.join(f_path)
39 return os.path.join(f_path)
42
40
43
41
44 def plain_dummy_env():
42 def plain_dummy_env():
45 return {
43 return {
46 'request':
44 'request':
47 AttributeDict(host_url='http://localhost', script_name='/')
45 AttributeDict(host_url='http://localhost', script_name='/')
48 }
46 }
49
47
50
48
51 @pytest.fixture()
49 @pytest.fixture()
52 def dummy_env():
50 def dummy_env():
53 return plain_dummy_env()
51 return plain_dummy_env()
54
52
55
53
56 def plain_dummy_user():
54 def plain_dummy_user():
57 return AttributeDict(username='test_user')
55 return AttributeDict(username='test_user')
58
56
59
57
60 @pytest.fixture()
58 @pytest.fixture()
61 def dummy_user():
59 def dummy_user():
62 return plain_dummy_user()
60 return plain_dummy_user()
63
61
64
62
65 @pytest.fixture()
63 @pytest.fixture()
66 def ssh_wrapper(app, dummy_conf_file, dummy_env):
64 def ssh_wrapper(app, dummy_conf_file, dummy_env):
67 conn_info = '127.0.0.1 22 10.0.0.1 443'
65 conn_info = '127.0.0.1 22 10.0.0.1 443'
68 return SshWrapper(
66 return SshWrapper(
69 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
67 'random command', conn_info, 'auto', 'admin', '1', key_id='1',
70 shell=False, ini_path=dummy_conf_file, env=dummy_env)
68 shell=False, ini_path=dummy_conf_file, env=dummy_env)
@@ -1,155 +1,153 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22
20
23 import mock
21 import mock
24 import pytest
22 import pytest
25
23
26 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
24 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
27 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
28 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
29
27
30 class GitServerCreator(object):
28 class GitServerCreator(object):
31 root = '/tmp/repo/path/'
29 root = '/tmp/repo/path/'
32 git_path = '/usr/local/bin/git'
30 git_path = '/usr/local/bin/git'
33 config_data = {
31 config_data = {
34 'app:main': {
32 'app:main': {
35 'ssh.executable.git': git_path,
33 'ssh.executable.git': git_path,
36 'vcs.hooks.protocol': 'http',
34 'vcs.hooks.protocol': 'http',
37 }
35 }
38 }
36 }
39 repo_name = 'test_git'
37 repo_name = 'test_git'
40 repo_mode = 'receive-pack'
38 repo_mode = 'receive-pack'
41 user = plain_dummy_user()
39 user = plain_dummy_user()
42
40
43 def __init__(self):
41 def __init__(self):
44 def config_get(part, key):
42 def config_get(part, key):
45 return self.config_data.get(part, {}).get(key)
43 return self.config_data.get(part, {}).get(key)
46 self.config_mock = mock.Mock()
44 self.config_mock = mock.Mock()
47 self.config_mock.get = mock.Mock(side_effect=config_get)
45 self.config_mock.get = mock.Mock(side_effect=config_get)
48
46
49 def create(self, **kwargs):
47 def create(self, **kwargs):
50 parameters = {
48 parameters = {
51 'store': self.root,
49 'store': self.root,
52 'ini_path': '',
50 'ini_path': '',
53 'user': self.user,
51 'user': self.user,
54 'repo_name': self.repo_name,
52 'repo_name': self.repo_name,
55 'repo_mode': self.repo_mode,
53 'repo_mode': self.repo_mode,
56 'user_permissions': {
54 'user_permissions': {
57 self.repo_name: 'repository.admin'
55 self.repo_name: 'repository.admin'
58 },
56 },
59 'config': self.config_mock,
57 'config': self.config_mock,
60 'env': plain_dummy_env()
58 'env': plain_dummy_env()
61 }
59 }
62 parameters.update(kwargs)
60 parameters.update(kwargs)
63 server = GitServer(**parameters)
61 server = GitServer(**parameters)
64 return server
62 return server
65
63
66
64
67 @pytest.fixture()
65 @pytest.fixture()
68 def git_server(app):
66 def git_server(app):
69 return GitServerCreator()
67 return GitServerCreator()
70
68
71
69
72 class TestGitServer(object):
70 class TestGitServer(object):
73
71
74 def test_command(self, git_server):
72 def test_command(self, git_server):
75 server = git_server.create()
73 server = git_server.create()
76 expected_command = (
74 expected_command = (
77 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
75 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
78 root=git_server.root, git_path=git_server.git_path,
76 root=git_server.root, git_path=git_server.git_path,
79 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
77 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
80 )
78 )
81 assert expected_command == server.tunnel.command()
79 assert expected_command == server.tunnel.command()
82
80
83 @pytest.mark.parametrize('permissions, action, code', [
81 @pytest.mark.parametrize('permissions, action, code', [
84 ({}, 'pull', -2),
82 ({}, 'pull', -2),
85 ({'test_git': 'repository.read'}, 'pull', 0),
83 ({'test_git': 'repository.read'}, 'pull', 0),
86 ({'test_git': 'repository.read'}, 'push', -2),
84 ({'test_git': 'repository.read'}, 'push', -2),
87 ({'test_git': 'repository.write'}, 'push', 0),
85 ({'test_git': 'repository.write'}, 'push', 0),
88 ({'test_git': 'repository.admin'}, 'push', 0),
86 ({'test_git': 'repository.admin'}, 'push', 0),
89
87
90 ])
88 ])
91 def test_permission_checks(self, git_server, permissions, action, code):
89 def test_permission_checks(self, git_server, permissions, action, code):
92 server = git_server.create(user_permissions=permissions)
90 server = git_server.create(user_permissions=permissions)
93 result = server._check_permissions(action)
91 result = server._check_permissions(action)
94 assert result is code
92 assert result is code
95
93
96 @pytest.mark.parametrize('permissions, value', [
94 @pytest.mark.parametrize('permissions, value', [
97 ({}, False),
95 ({}, False),
98 ({'test_git': 'repository.read'}, False),
96 ({'test_git': 'repository.read'}, False),
99 ({'test_git': 'repository.write'}, True),
97 ({'test_git': 'repository.write'}, True),
100 ({'test_git': 'repository.admin'}, True),
98 ({'test_git': 'repository.admin'}, True),
101
99
102 ])
100 ])
103 def test_has_write_permissions(self, git_server, permissions, value):
101 def test_has_write_permissions(self, git_server, permissions, value):
104 server = git_server.create(user_permissions=permissions)
102 server = git_server.create(user_permissions=permissions)
105 result = server.has_write_perm()
103 result = server.has_write_perm()
106 assert result is value
104 assert result is value
107
105
108 def test_run_returns_executes_command(self, git_server):
106 def test_run_returns_executes_command(self, git_server):
109 server = git_server.create()
107 server = git_server.create()
110 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
108 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
111
109
112 os.environ['SSH_CLIENT'] = '127.0.0.1'
110 os.environ['SSH_CLIENT'] = '127.0.0.1'
113 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
111 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
114 _patch.return_value = 0
112 _patch.return_value = 0
115 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
113 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
116 exit_code = server.run()
114 exit_code = server.run()
117
115
118 assert exit_code == (0, False)
116 assert exit_code == (0, False)
119
117
120 @pytest.mark.parametrize(
118 @pytest.mark.parametrize(
121 'repo_mode, action', [
119 'repo_mode, action', [
122 ['receive-pack', 'push'],
120 ['receive-pack', 'push'],
123 ['upload-pack', 'pull']
121 ['upload-pack', 'pull']
124 ])
122 ])
125 def test_update_environment(self, git_server, repo_mode, action):
123 def test_update_environment(self, git_server, repo_mode, action):
126 server = git_server.create(repo_mode=repo_mode)
124 server = git_server.create(repo_mode=repo_mode)
127 store = server.store
125 store = server.store
128
126
129 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
127 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
130 with mock.patch('os.putenv') as putenv_mock:
128 with mock.patch('os.putenv') as putenv_mock:
131 server.update_environment(action)
129 server.update_environment(action)
132
130
133 expected_data = {
131 expected_data = {
134 'username': git_server.user.username,
132 'username': git_server.user.username,
135 'user_id': git_server.user.user_id,
133 'user_id': git_server.user.user_id,
136 'scm': 'git',
134 'scm': 'git',
137 'repository': git_server.repo_name,
135 'repository': git_server.repo_name,
138 'make_lock': None,
136 'make_lock': None,
139 'action': action,
137 'action': action,
140 'ip': '10.10.10.10',
138 'ip': '10.10.10.10',
141 'locked_by': [None, None],
139 'locked_by': [None, None],
142 'config': '',
140 'config': '',
143 'repo_store': store,
141 'repo_store': store,
144 'server_url': None,
142 'server_url': None,
145 'hooks': ['push', 'pull'],
143 'hooks': ['push', 'pull'],
146 'is_shadow_repo': False,
144 'is_shadow_repo': False,
147 'hooks_module': 'rhodecode.lib.hooks_daemon',
145 'hooks_module': 'rhodecode.lib.hooks_daemon',
148 'check_branch_perms': False,
146 'check_branch_perms': False,
149 'detect_force_push': False,
147 'detect_force_push': False,
150 'user_agent': u'git/ssh-user-agent',
148 'user_agent': u'git/ssh-user-agent',
151 'SSH': True,
149 'SSH': True,
152 'SSH_PERMISSIONS': 'repository.admin',
150 'SSH_PERMISSIONS': 'repository.admin',
153 }
151 }
154 args, kwargs = putenv_mock.call_args
152 args, kwargs = putenv_mock.call_args
155 assert json.loads(args[1]) == expected_data
153 assert json.loads(args[1]) == expected_data
@@ -1,120 +1,118 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import mock
20 import mock
23 import pytest
21 import pytest
24
22
25 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
23 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialServer
26 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
24 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
27
25
28
26
29 class MercurialServerCreator(object):
27 class MercurialServerCreator(object):
30 root = '/tmp/repo/path/'
28 root = '/tmp/repo/path/'
31 hg_path = '/usr/local/bin/hg'
29 hg_path = '/usr/local/bin/hg'
32
30
33 config_data = {
31 config_data = {
34 'app:main': {
32 'app:main': {
35 'ssh.executable.hg': hg_path,
33 'ssh.executable.hg': hg_path,
36 'vcs.hooks.protocol': 'http',
34 'vcs.hooks.protocol': 'http',
37 }
35 }
38 }
36 }
39 repo_name = 'test_hg'
37 repo_name = 'test_hg'
40 user = plain_dummy_user()
38 user = plain_dummy_user()
41
39
42 def __init__(self):
40 def __init__(self):
43 def config_get(part, key):
41 def config_get(part, key):
44 return self.config_data.get(part, {}).get(key)
42 return self.config_data.get(part, {}).get(key)
45 self.config_mock = mock.Mock()
43 self.config_mock = mock.Mock()
46 self.config_mock.get = mock.Mock(side_effect=config_get)
44 self.config_mock.get = mock.Mock(side_effect=config_get)
47
45
48 def create(self, **kwargs):
46 def create(self, **kwargs):
49 parameters = {
47 parameters = {
50 'store': self.root,
48 'store': self.root,
51 'ini_path': '',
49 'ini_path': '',
52 'user': self.user,
50 'user': self.user,
53 'repo_name': self.repo_name,
51 'repo_name': self.repo_name,
54 'user_permissions': {
52 'user_permissions': {
55 'test_hg': 'repository.admin'
53 'test_hg': 'repository.admin'
56 },
54 },
57 'config': self.config_mock,
55 'config': self.config_mock,
58 'env': plain_dummy_env()
56 'env': plain_dummy_env()
59 }
57 }
60 parameters.update(kwargs)
58 parameters.update(kwargs)
61 server = MercurialServer(**parameters)
59 server = MercurialServer(**parameters)
62 return server
60 return server
63
61
64
62
65 @pytest.fixture()
63 @pytest.fixture()
66 def hg_server(app):
64 def hg_server(app):
67 return MercurialServerCreator()
65 return MercurialServerCreator()
68
66
69
67
70 class TestMercurialServer(object):
68 class TestMercurialServer(object):
71
69
72 def test_command(self, hg_server, tmpdir):
70 def test_command(self, hg_server, tmpdir):
73 server = hg_server.create()
71 server = hg_server.create()
74 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
72 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
75 expected_command = (
73 expected_command = (
76 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
74 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
77 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
75 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
78 repo_name=hg_server.repo_name)
76 repo_name=hg_server.repo_name)
79 )
77 )
80 server_command = server.tunnel.command(custom_hgrc)
78 server_command = server.tunnel.command(custom_hgrc)
81 assert expected_command == server_command
79 assert expected_command == server_command
82
80
83 @pytest.mark.parametrize('permissions, action, code', [
81 @pytest.mark.parametrize('permissions, action, code', [
84 ({}, 'pull', -2),
82 ({}, 'pull', -2),
85 ({'test_hg': 'repository.read'}, 'pull', 0),
83 ({'test_hg': 'repository.read'}, 'pull', 0),
86 ({'test_hg': 'repository.read'}, 'push', -2),
84 ({'test_hg': 'repository.read'}, 'push', -2),
87 ({'test_hg': 'repository.write'}, 'push', 0),
85 ({'test_hg': 'repository.write'}, 'push', 0),
88 ({'test_hg': 'repository.admin'}, 'push', 0),
86 ({'test_hg': 'repository.admin'}, 'push', 0),
89
87
90 ])
88 ])
91 def test_permission_checks(self, hg_server, permissions, action, code):
89 def test_permission_checks(self, hg_server, permissions, action, code):
92 server = hg_server.create(user_permissions=permissions)
90 server = hg_server.create(user_permissions=permissions)
93 result = server._check_permissions(action)
91 result = server._check_permissions(action)
94 assert result is code
92 assert result is code
95
93
96 @pytest.mark.parametrize('permissions, value', [
94 @pytest.mark.parametrize('permissions, value', [
97 ({}, False),
95 ({}, False),
98 ({'test_hg': 'repository.read'}, False),
96 ({'test_hg': 'repository.read'}, False),
99 ({'test_hg': 'repository.write'}, True),
97 ({'test_hg': 'repository.write'}, True),
100 ({'test_hg': 'repository.admin'}, True),
98 ({'test_hg': 'repository.admin'}, True),
101
99
102 ])
100 ])
103 def test_has_write_permissions(self, hg_server, permissions, value):
101 def test_has_write_permissions(self, hg_server, permissions, value):
104 server = hg_server.create(user_permissions=permissions)
102 server = hg_server.create(user_permissions=permissions)
105 result = server.has_write_perm()
103 result = server.has_write_perm()
106 assert result is value
104 assert result is value
107
105
108 def test_run_returns_executes_command(self, hg_server):
106 def test_run_returns_executes_command(self, hg_server):
109 server = hg_server.create()
107 server = hg_server.create()
110 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
108 from rhodecode.apps.ssh_support.lib.backends.hg import MercurialTunnelWrapper
111 os.environ['SSH_CLIENT'] = '127.0.0.1'
109 os.environ['SSH_CLIENT'] = '127.0.0.1'
112 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
110 with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
113 _patch.return_value = 0
111 _patch.return_value = 0
114 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
112 with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
115 exit_code = server.run()
113 exit_code = server.run()
116
114
117 assert exit_code == (0, False)
115 assert exit_code == (0, False)
118
116
119
117
120
118
@@ -1,207 +1,205 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
18 import os
21 import mock
19 import mock
22 import pytest
20 import pytest
23
21
24 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
22 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer
25 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
23 from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user
26
24
27
25
28 class SubversionServerCreator(object):
26 class SubversionServerCreator(object):
29 root = '/tmp/repo/path/'
27 root = '/tmp/repo/path/'
30 svn_path = '/usr/local/bin/svnserve'
28 svn_path = '/usr/local/bin/svnserve'
31 config_data = {
29 config_data = {
32 'app:main': {
30 'app:main': {
33 'ssh.executable.svn': svn_path,
31 'ssh.executable.svn': svn_path,
34 'vcs.hooks.protocol': 'http',
32 'vcs.hooks.protocol': 'http',
35 }
33 }
36 }
34 }
37 repo_name = 'test-svn'
35 repo_name = 'test-svn'
38 user = plain_dummy_user()
36 user = plain_dummy_user()
39
37
40 def __init__(self):
38 def __init__(self):
41 def config_get(part, key):
39 def config_get(part, key):
42 return self.config_data.get(part, {}).get(key)
40 return self.config_data.get(part, {}).get(key)
43 self.config_mock = mock.Mock()
41 self.config_mock = mock.Mock()
44 self.config_mock.get = mock.Mock(side_effect=config_get)
42 self.config_mock.get = mock.Mock(side_effect=config_get)
45
43
46 def create(self, **kwargs):
44 def create(self, **kwargs):
47 parameters = {
45 parameters = {
48 'store': self.root,
46 'store': self.root,
49 'repo_name': self.repo_name,
47 'repo_name': self.repo_name,
50 'ini_path': '',
48 'ini_path': '',
51 'user': self.user,
49 'user': self.user,
52 'user_permissions': {
50 'user_permissions': {
53 self.repo_name: 'repository.admin'
51 self.repo_name: 'repository.admin'
54 },
52 },
55 'config': self.config_mock,
53 'config': self.config_mock,
56 'env': plain_dummy_env()
54 'env': plain_dummy_env()
57 }
55 }
58
56
59 parameters.update(kwargs)
57 parameters.update(kwargs)
60 server = SubversionServer(**parameters)
58 server = SubversionServer(**parameters)
61 return server
59 return server
62
60
63
61
64 @pytest.fixture()
62 @pytest.fixture()
65 def svn_server(app):
63 def svn_server(app):
66 return SubversionServerCreator()
64 return SubversionServerCreator()
67
65
68
66
69 class TestSubversionServer(object):
67 class TestSubversionServer(object):
70 def test_command(self, svn_server):
68 def test_command(self, svn_server):
71 server = svn_server.create()
69 server = svn_server.create()
72 expected_command = [
70 expected_command = [
73 svn_server.svn_path, '-t',
71 svn_server.svn_path, '-t',
74 '--config-file', server.tunnel.svn_conf_path,
72 '--config-file', server.tunnel.svn_conf_path,
75 '--tunnel-user', svn_server.user.username,
73 '--tunnel-user', svn_server.user.username,
76 '-r', svn_server.root
74 '-r', svn_server.root
77 ]
75 ]
78
76
79 assert expected_command == server.tunnel.command()
77 assert expected_command == server.tunnel.command()
80
78
81 @pytest.mark.parametrize('permissions, action, code', [
79 @pytest.mark.parametrize('permissions, action, code', [
82 ({}, 'pull', -2),
80 ({}, 'pull', -2),
83 ({'test-svn': 'repository.read'}, 'pull', 0),
81 ({'test-svn': 'repository.read'}, 'pull', 0),
84 ({'test-svn': 'repository.read'}, 'push', -2),
82 ({'test-svn': 'repository.read'}, 'push', -2),
85 ({'test-svn': 'repository.write'}, 'push', 0),
83 ({'test-svn': 'repository.write'}, 'push', 0),
86 ({'test-svn': 'repository.admin'}, 'push', 0),
84 ({'test-svn': 'repository.admin'}, 'push', 0),
87
85
88 ])
86 ])
89 def test_permission_checks(self, svn_server, permissions, action, code):
87 def test_permission_checks(self, svn_server, permissions, action, code):
90 server = svn_server.create(user_permissions=permissions)
88 server = svn_server.create(user_permissions=permissions)
91 result = server._check_permissions(action)
89 result = server._check_permissions(action)
92 assert result is code
90 assert result is code
93
91
94 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
92 @pytest.mark.parametrize('permissions, access_paths, expected_match', [
95 # not matched repository name
93 # not matched repository name
96 ({
94 ({
97 'test-svn': ''
95 'test-svn': ''
98 }, ['test-svn-1', 'test-svn-1/subpath'],
96 }, ['test-svn-1', 'test-svn-1/subpath'],
99 None),
97 None),
100
98
101 # exact match
99 # exact match
102 ({
100 ({
103 'test-svn': ''
101 'test-svn': ''
104 },
102 },
105 ['test-svn'],
103 ['test-svn'],
106 'test-svn'),
104 'test-svn'),
107
105
108 # subdir commits
106 # subdir commits
109 ({
107 ({
110 'test-svn': ''
108 'test-svn': ''
111 },
109 },
112 ['test-svn/foo',
110 ['test-svn/foo',
113 'test-svn/foo/test-svn',
111 'test-svn/foo/test-svn',
114 'test-svn/trunk/development.txt',
112 'test-svn/trunk/development.txt',
115 ],
113 ],
116 'test-svn'),
114 'test-svn'),
117
115
118 # subgroups + similar patterns
116 # subgroups + similar patterns
119 ({
117 ({
120 'test-svn': '',
118 'test-svn': '',
121 'test-svn-1': '',
119 'test-svn-1': '',
122 'test-svn-subgroup/test-svn': '',
120 'test-svn-subgroup/test-svn': '',
123
121
124 },
122 },
125 ['test-svn-1',
123 ['test-svn-1',
126 'test-svn-1/foo/test-svn',
124 'test-svn-1/foo/test-svn',
127 'test-svn-1/test-svn',
125 'test-svn-1/test-svn',
128 ],
126 ],
129 'test-svn-1'),
127 'test-svn-1'),
130
128
131 # subgroups + similar patterns
129 # subgroups + similar patterns
132 ({
130 ({
133 'test-svn-1': '',
131 'test-svn-1': '',
134 'test-svn-10': '',
132 'test-svn-10': '',
135 'test-svn-100': '',
133 'test-svn-100': '',
136 },
134 },
137 ['test-svn-10',
135 ['test-svn-10',
138 'test-svn-10/foo/test-svn',
136 'test-svn-10/foo/test-svn',
139 'test-svn-10/test-svn',
137 'test-svn-10/test-svn',
140 ],
138 ],
141 'test-svn-10'),
139 'test-svn-10'),
142
140
143 # subgroups + similar patterns
141 # subgroups + similar patterns
144 ({
142 ({
145 'name': '',
143 'name': '',
146 'nameContains': '',
144 'nameContains': '',
147 'nameContainsThis': '',
145 'nameContainsThis': '',
148 },
146 },
149 ['nameContains',
147 ['nameContains',
150 'nameContains/This',
148 'nameContains/This',
151 'nameContains/This/test-svn',
149 'nameContains/This/test-svn',
152 ],
150 ],
153 'nameContains'),
151 'nameContains'),
154
152
155 # subgroups + similar patterns
153 # subgroups + similar patterns
156 ({
154 ({
157 'test-svn': '',
155 'test-svn': '',
158 'test-svn-1': '',
156 'test-svn-1': '',
159 'test-svn-subgroup/test-svn': '',
157 'test-svn-subgroup/test-svn': '',
160
158
161 },
159 },
162 ['test-svn-subgroup/test-svn',
160 ['test-svn-subgroup/test-svn',
163 'test-svn-subgroup/test-svn/foo/test-svn',
161 'test-svn-subgroup/test-svn/foo/test-svn',
164 'test-svn-subgroup/test-svn/trunk/example.txt',
162 'test-svn-subgroup/test-svn/trunk/example.txt',
165 ],
163 ],
166 'test-svn-subgroup/test-svn'),
164 'test-svn-subgroup/test-svn'),
167 ])
165 ])
168 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
166 def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match):
169 server = svn_server.create(user_permissions=permissions)
167 server = svn_server.create(user_permissions=permissions)
170 for path in access_paths:
168 for path in access_paths:
171 repo_name = server.tunnel._match_repo_name(path)
169 repo_name = server.tunnel._match_repo_name(path)
172 assert repo_name == expected_match
170 assert repo_name == expected_match
173
171
174 def test_run_returns_executes_command(self, svn_server):
172 def test_run_returns_executes_command(self, svn_server):
175 server = svn_server.create()
173 server = svn_server.create()
176 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
174 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
177 os.environ['SSH_CLIENT'] = '127.0.0.1'
175 os.environ['SSH_CLIENT'] = '127.0.0.1'
178 with mock.patch.object(
176 with mock.patch.object(
179 SubversionTunnelWrapper, 'get_first_client_response',
177 SubversionTunnelWrapper, 'get_first_client_response',
180 return_value={'url': 'http://server/test-svn'}):
178 return_value={'url': 'http://server/test-svn'}):
181 with mock.patch.object(
179 with mock.patch.object(
182 SubversionTunnelWrapper, 'patch_first_client_response',
180 SubversionTunnelWrapper, 'patch_first_client_response',
183 return_value=0):
181 return_value=0):
184 with mock.patch.object(
182 with mock.patch.object(
185 SubversionTunnelWrapper, 'sync',
183 SubversionTunnelWrapper, 'sync',
186 return_value=0):
184 return_value=0):
187 with mock.patch.object(
185 with mock.patch.object(
188 SubversionTunnelWrapper, 'command',
186 SubversionTunnelWrapper, 'command',
189 return_value=['date']):
187 return_value=['date']):
190
188
191 exit_code = server.run()
189 exit_code = server.run()
192 # SVN has this differently configured, and we get in our mock env
190 # SVN has this differently configured, and we get in our mock env
193 # None as return code
191 # None as return code
194 assert exit_code == (None, False)
192 assert exit_code == (None, False)
195
193
196 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
194 def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
197 server = svn_server.create()
195 server = svn_server.create()
198 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
196 from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
199 with mock.patch.object(
197 with mock.patch.object(
200 SubversionTunnelWrapper, 'command',
198 SubversionTunnelWrapper, 'command',
201 return_value=['date']):
199 return_value=['date']):
202 with mock.patch.object(
200 with mock.patch.object(
203 SubversionTunnelWrapper, 'get_first_client_response',
201 SubversionTunnelWrapper, 'get_first_client_response',
204 return_value=None):
202 return_value=None):
205 exit_code = server.run()
203 exit_code = server.run()
206
204
207 assert exit_code == (1, False)
205 assert exit_code == (1, False)
@@ -1,71 +1,69 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import pytest
20 import pytest
23 import mock
21 import mock
24
22
25 from rhodecode.apps.ssh_support import utils
23 from rhodecode.apps.ssh_support import utils
26 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
27
25
28
26
29 class TestSshKeyFileGeneration(object):
27 class TestSshKeyFileGeneration(object):
30 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
28 @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
31 @pytest.mark.parametrize('allow_shell', [True, False])
29 @pytest.mark.parametrize('allow_shell', [True, False])
32 @pytest.mark.parametrize('debug', [True, False])
30 @pytest.mark.parametrize('debug', [True, False])
33 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
31 @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
34 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, debug, ssh_opts):
32 def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, debug, ssh_opts):
35
33
36 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
34 authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
37
35
38 def keys():
36 def keys():
39 return [
37 return [
40 AttributeDict({'user': AttributeDict(username='admin'),
38 AttributeDict({'user': AttributeDict(username='admin'),
41 'ssh_key_data': 'ssh-rsa ADMIN_KEY'}),
39 'ssh_key_data': 'ssh-rsa ADMIN_KEY'}),
42 AttributeDict({'user': AttributeDict(username='user'),
40 AttributeDict({'user': AttributeDict(username='user'),
43 'ssh_key_data': 'ssh-rsa USER_KEY'}),
41 'ssh_key_data': 'ssh-rsa USER_KEY'}),
44 ]
42 ]
45 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
43 with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
46 return_value=keys()):
44 return_value=keys()):
47 with mock.patch.dict('rhodecode.CONFIG', {'__file__': '/tmp/file.ini'}):
45 with mock.patch.dict('rhodecode.CONFIG', {'__file__': '/tmp/file.ini'}):
48 utils._generate_ssh_authorized_keys_file(
46 utils._generate_ssh_authorized_keys_file(
49 authorized_keys_file_path, ssh_wrapper_cmd,
47 authorized_keys_file_path, ssh_wrapper_cmd,
50 allow_shell, ssh_opts, debug
48 allow_shell, ssh_opts, debug
51 )
49 )
52
50
53 assert os.path.isfile(authorized_keys_file_path)
51 assert os.path.isfile(authorized_keys_file_path)
54 with open(authorized_keys_file_path) as f:
52 with open(authorized_keys_file_path) as f:
55 content = f.read()
53 content = f.read()
56
54
57 assert 'command="/tmp/sshwrapper.py' in content
55 assert 'command="/tmp/sshwrapper.py' in content
58 assert 'This file is managed by RhodeCode, ' \
56 assert 'This file is managed by RhodeCode, ' \
59 'please do not edit it manually.' in content
57 'please do not edit it manually.' in content
60
58
61 if allow_shell:
59 if allow_shell:
62 assert '--shell' in content
60 assert '--shell' in content
63
61
64 if debug:
62 if debug:
65 assert '--debug' in content
63 assert '--debug' in content
66
64
67 assert '--user' in content
65 assert '--user' in content
68 assert '--user-id' in content
66 assert '--user-id' in content
69
67
70 if ssh_opts:
68 if ssh_opts:
71 assert ssh_opts in content
69 assert ssh_opts in content
@@ -1,54 +1,52 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import pytest
19 import pytest
22
20
23
21
24 class TestSSHWrapper(object):
22 class TestSSHWrapper(object):
25
23
26 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
24 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
27 with pytest.raises(Exception) as exc_info:
25 with pytest.raises(Exception) as exc_info:
28 ssh_wrapper.serve(
26 ssh_wrapper.serve(
29 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
27 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
30 permissions={}, branch_permissions={})
28 permissions={}, branch_permissions={})
31 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
29 assert str(exc_info.value) == 'Unrecognised VCS: microsoft-tfs'
32
30
33 def test_parse_config(self, ssh_wrapper):
31 def test_parse_config(self, ssh_wrapper):
34 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
32 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
35 assert config
33 assert config
36
34
37 def test_get_connection_info(self, ssh_wrapper):
35 def test_get_connection_info(self, ssh_wrapper):
38 conn_info = ssh_wrapper.get_connection_info()
36 conn_info = ssh_wrapper.get_connection_info()
39 assert {'client_ip': '127.0.0.1',
37 assert {'client_ip': '127.0.0.1',
40 'client_port': '22',
38 'client_port': '22',
41 'server_ip': '10.0.0.1',
39 'server_ip': '10.0.0.1',
42 'server_port': '443'} == conn_info
40 'server_port': '443'} == conn_info
43
41
44 @pytest.mark.parametrize('command, vcs', [
42 @pytest.mark.parametrize('command, vcs', [
45 ('xxx', None),
43 ('xxx', None),
46 ('svnserve -t', 'svn'),
44 ('svnserve -t', 'svn'),
47 ('hg -R repo serve --stdio', 'hg'),
45 ('hg -R repo serve --stdio', 'hg'),
48 ('git-receive-pack \'repo.git\'', 'git'),
46 ('git-receive-pack \'repo.git\'', 'git'),
49
47
50 ])
48 ])
51 def test_get_repo_details(self, ssh_wrapper, command, vcs):
49 def test_get_repo_details(self, ssh_wrapper, command, vcs):
52 ssh_wrapper.command = command
50 ssh_wrapper.command = command
53 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
51 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
54 assert vcs_type == vcs
52 assert vcs_type == vcs
@@ -1,138 +1,136 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import os
19 import os
22 import stat
20 import stat
23 import logging
21 import logging
24 import tempfile
22 import tempfile
25 import datetime
23 import datetime
26
24
27 from . import config_keys
25 from . import config_keys
28 from rhodecode.model.db import true, joinedload, User, UserSshKeys
26 from rhodecode.model.db import true, joinedload, User, UserSshKeys
29
27
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33 HEADER = \
31 HEADER = \
34 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
32 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
35 "# Current entries: {}, create date: UTC:{}.\n"
33 "# Current entries: {}, create date: UTC:{}.\n"
36
34
37 # Default SSH options for authorized_keys file, can be override via .ini
35 # Default SSH options for authorized_keys file, can be override via .ini
38 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
36 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
39
37
40
38
41 def get_all_active_keys():
39 def get_all_active_keys():
42 result = UserSshKeys.query() \
40 result = UserSshKeys.query() \
43 .join(User) \
41 .join(User) \
44 .filter(User != User.get_default_user()) \
42 .filter(User != User.get_default_user()) \
45 .filter(User.active == true()) \
43 .filter(User.active == true()) \
46 .all()
44 .all()
47 return result
45 return result
48
46
49
47
50 def _generate_ssh_authorized_keys_file(
48 def _generate_ssh_authorized_keys_file(
51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
49 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52 import rhodecode
50 import rhodecode
53
51
54 authorized_keys_file_path = os.path.abspath(
52 authorized_keys_file_path = os.path.abspath(
55 os.path.expanduser(authorized_keys_file_path))
53 os.path.expanduser(authorized_keys_file_path))
56 tmp_file_dir = os.path.dirname(authorized_keys_file_path)
54 tmp_file_dir = os.path.dirname(authorized_keys_file_path)
57
55
58 if not os.path.exists(tmp_file_dir):
56 if not os.path.exists(tmp_file_dir):
59 log.debug('SSH authorized_keys file dir does not exist, creating one now...')
57 log.debug('SSH authorized_keys file dir does not exist, creating one now...')
60 os.makedirs(tmp_file_dir)
58 os.makedirs(tmp_file_dir)
61
59
62 all_active_keys = get_all_active_keys()
60 all_active_keys = get_all_active_keys()
63
61
64 if allow_shell:
62 if allow_shell:
65 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
63 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
66 if debug:
64 if debug:
67 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
65 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
68
66
69 if not os.path.isfile(authorized_keys_file_path):
67 if not os.path.isfile(authorized_keys_file_path):
70 log.debug('Creating file at %s', authorized_keys_file_path)
68 log.debug('Creating file at %s', authorized_keys_file_path)
71 with open(authorized_keys_file_path, 'w'):
69 with open(authorized_keys_file_path, 'w'):
72 # create a file with write access
70 # create a file with write access
73 pass
71 pass
74
72
75 if not os.access(authorized_keys_file_path, os.R_OK):
73 if not os.access(authorized_keys_file_path, os.R_OK):
76 raise OSError('Access to file {} is without read access'.format(
74 raise OSError('Access to file {} is without read access'.format(
77 authorized_keys_file_path))
75 authorized_keys_file_path))
78
76
79 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
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 fd, tmp_authorized_keys = tempfile.mkstemp(
79 fd, tmp_authorized_keys = tempfile.mkstemp(
82 '.authorized_keys_write_operation',
80 '.authorized_keys_write_operation',
83 dir=tmp_file_dir)
81 dir=tmp_file_dir)
84
82
85 now = datetime.datetime.utcnow().isoformat()
83 now = datetime.datetime.utcnow().isoformat()
86 keys_file = os.fdopen(fd, 'wt')
84 keys_file = os.fdopen(fd, 'wt')
87 keys_file.write(HEADER.format(len(all_active_keys), now))
85 keys_file.write(HEADER.format(len(all_active_keys), now))
88 ini_path = rhodecode.CONFIG['__file__']
86 ini_path = rhodecode.CONFIG['__file__']
89
87
90 for user_key in all_active_keys:
88 for user_key in all_active_keys:
91 username = user_key.user.username
89 username = user_key.user.username
92 user_id = user_key.user.user_id
90 user_id = user_key.user.user_id
93 # replace all newline from ends and inside
91 # replace all newline from ends and inside
94 safe_key_data = user_key.ssh_key_data\
92 safe_key_data = user_key.ssh_key_data\
95 .strip()\
93 .strip()\
96 .replace('\n', ' ') \
94 .replace('\n', ' ') \
97 .replace('\t', ' ') \
95 .replace('\t', ' ') \
98 .replace('\r', ' ')
96 .replace('\r', ' ')
99
97
100 line = line_tmpl.format(
98 line = line_tmpl.format(
101 ssh_opts=ssh_opts or SSH_OPTS,
99 ssh_opts=ssh_opts or SSH_OPTS,
102 wrapper_command=ssh_wrapper_cmd,
100 wrapper_command=ssh_wrapper_cmd,
103 ini_path=ini_path,
101 ini_path=ini_path,
104 user_id=user_id,
102 user_id=user_id,
105 user=username,
103 user=username,
106 user_key_id=user_key.ssh_key_id,
104 user_key_id=user_key.ssh_key_id,
107 key=safe_key_data)
105 key=safe_key_data)
108
106
109 keys_file.write(line)
107 keys_file.write(line)
110 log.debug('addkey: Key added for user: `%s`', username)
108 log.debug('addkey: Key added for user: `%s`', username)
111 keys_file.close()
109 keys_file.close()
112
110
113 # Explicitly setting read-only permissions to authorized_keys
111 # Explicitly setting read-only permissions to authorized_keys
114 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
112 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
115 # Rename is atomic operation
113 # Rename is atomic operation
116 os.rename(tmp_authorized_keys, authorized_keys_file_path)
114 os.rename(tmp_authorized_keys, authorized_keys_file_path)
117
115
118
116
119 def generate_ssh_authorized_keys_file(registry):
117 def generate_ssh_authorized_keys_file(registry):
120 log.info('Generating new authorized key file')
118 log.info('Generating new authorized key file')
121
119
122 authorized_keys_file_path = registry.settings.get(
120 authorized_keys_file_path = registry.settings.get(
123 config_keys.authorized_keys_file_path)
121 config_keys.authorized_keys_file_path)
124
122
125 ssh_wrapper_cmd = registry.settings.get(
123 ssh_wrapper_cmd = registry.settings.get(
126 config_keys.wrapper_cmd)
124 config_keys.wrapper_cmd)
127 allow_shell = registry.settings.get(
125 allow_shell = registry.settings.get(
128 config_keys.wrapper_allow_shell)
126 config_keys.wrapper_allow_shell)
129 ssh_opts = registry.settings.get(
127 ssh_opts = registry.settings.get(
130 config_keys.authorized_keys_line_ssh_opts)
128 config_keys.authorized_keys_line_ssh_opts)
131 debug = registry.settings.get(
129 debug = registry.settings.get(
132 config_keys.enable_debug_logging)
130 config_keys.enable_debug_logging)
133
131
134 _generate_ssh_authorized_keys_file(
132 _generate_ssh_authorized_keys_file(
135 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
133 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
136 debug)
134 debug)
137
135
138 return 0
136 return 0
@@ -1,90 +1,88 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
18 import os
21 import logging
19 import logging
22
20
23 # Do not use `from rhodecode import events` here, it will be overridden by the
21 # Do not use `from rhodecode import events` here, it will be overridden by the
24 # events module in this package due to pythons import mechanism.
22 # events module in this package due to pythons import mechanism.
25 from rhodecode.events import RepoGroupEvent
23 from rhodecode.events import RepoGroupEvent
26 from rhodecode.subscribers import AsyncSubprocessSubscriber
24 from rhodecode.subscribers import AsyncSubprocessSubscriber
27 from rhodecode.config.settings_maker import SettingsMaker
25 from rhodecode.config.settings_maker import SettingsMaker
28
26
29 from .events import ModDavSvnConfigChange
27 from .events import ModDavSvnConfigChange
30 from .subscribers import generate_config_subscriber
28 from .subscribers import generate_config_subscriber
31 from . import config_keys
29 from . import config_keys
32
30
33
31
34 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
35
33
36
34
37 def _sanitize_settings_and_apply_defaults(settings):
35 def _sanitize_settings_and_apply_defaults(settings):
38 """
36 """
39 Set defaults, convert to python types and validate settings.
37 Set defaults, convert to python types and validate settings.
40 """
38 """
41 settings_maker = SettingsMaker(settings)
39 settings_maker = SettingsMaker(settings)
42 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
40 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
43 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
41 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
44 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
42 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
45 settings_maker.make_setting(config_keys.config_file_path, '')
43 settings_maker.make_setting(config_keys.config_file_path, '')
46 settings_maker.make_setting(config_keys.location_root, '/')
44 settings_maker.make_setting(config_keys.location_root, '/')
47 settings_maker.make_setting(config_keys.reload_command, '')
45 settings_maker.make_setting(config_keys.reload_command, '')
48 settings_maker.make_setting(config_keys.template, '')
46 settings_maker.make_setting(config_keys.template, '')
49
47
50 settings_maker.env_expand()
48 settings_maker.env_expand()
51
49
52 # Convert negative timeout values to zero.
50 # Convert negative timeout values to zero.
53 if settings[config_keys.reload_timeout] < 0:
51 if settings[config_keys.reload_timeout] < 0:
54 settings[config_keys.reload_timeout] = 0
52 settings[config_keys.reload_timeout] = 0
55
53
56 # Append path separator to location root.
54 # Append path separator to location root.
57 settings[config_keys.location_root] = _append_path_sep(
55 settings[config_keys.location_root] = _append_path_sep(
58 settings[config_keys.location_root])
56 settings[config_keys.location_root])
59
57
60 # Validate settings.
58 # Validate settings.
61 if settings[config_keys.generate_config]:
59 if settings[config_keys.generate_config]:
62 assert len(settings[config_keys.config_file_path]) > 0
60 assert len(settings[config_keys.config_file_path]) > 0
63
61
64
62
65 def _append_path_sep(path):
63 def _append_path_sep(path):
66 """
64 """
67 Append the path separator if missing.
65 Append the path separator if missing.
68 """
66 """
69 if isinstance(path, str) and not path.endswith(os.path.sep):
67 if isinstance(path, str) and not path.endswith(os.path.sep):
70 path += os.path.sep
68 path += os.path.sep
71 return path
69 return path
72
70
73
71
74 def includeme(config):
72 def includeme(config):
75 settings = config.registry.settings
73 settings = config.registry.settings
76 _sanitize_settings_and_apply_defaults(settings)
74 _sanitize_settings_and_apply_defaults(settings)
77
75
78 if settings[config_keys.generate_config]:
76 if settings[config_keys.generate_config]:
79 # Add subscriber to generate the Apache mod dav svn configuration on
77 # Add subscriber to generate the Apache mod dav svn configuration on
80 # repository group events.
78 # repository group events.
81 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
79 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
82
80
83 # If a reload command is set add a subscriber to execute it on
81 # If a reload command is set add a subscriber to execute it on
84 # configuration changes.
82 # configuration changes.
85 reload_cmd = settings[config_keys.reload_command]
83 reload_cmd = settings[config_keys.reload_command]
86 if reload_cmd:
84 if reload_cmd:
87 reload_timeout = settings[config_keys.reload_timeout] or None
85 reload_timeout = settings[config_keys.reload_timeout] or None
88 reload_subscriber = AsyncSubprocessSubscriber(
86 reload_subscriber = AsyncSubprocessSubscriber(
89 cmd=reload_cmd, timeout=reload_timeout)
87 cmd=reload_cmd, timeout=reload_timeout)
90 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
88 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
@@ -1,30 +1,28 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 # Definition of setting keys used to configure this module. Defined here to
20 # Definition of setting keys used to configure this module. Defined here to
23 # avoid repetition of keys throughout the module.
21 # avoid repetition of keys throughout the module.
24 config_file_path = 'svn.proxy.config_file_path'
22 config_file_path = 'svn.proxy.config_file_path'
25 generate_config = 'svn.proxy.generate_config'
23 generate_config = 'svn.proxy.generate_config'
26 list_parent_path = 'svn.proxy.list_parent_path'
24 list_parent_path = 'svn.proxy.list_parent_path'
27 location_root = 'svn.proxy.location_root'
25 location_root = 'svn.proxy.location_root'
28 reload_command = 'svn.proxy.reload_cmd'
26 reload_command = 'svn.proxy.reload_cmd'
29 reload_timeout = 'svn.proxy.reload_timeout'
27 reload_timeout = 'svn.proxy.reload_timeout'
30 template = 'svn.proxy.config_template'
28 template = 'svn.proxy.config_template'
@@ -1,40 +1,38 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23
21
24 from .utils import generate_mod_dav_svn_config
22 from .utils import generate_mod_dav_svn_config
25
23
26
24
27 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
28
26
29
27
30 def generate_config_subscriber(event):
28 def generate_config_subscriber(event):
31 """
29 """
32 Subscriber to the `rhodcode.events.RepoGroupEvent`. This triggers the
30 Subscriber to the `rhodcode.events.RepoGroupEvent`. This triggers the
33 automatic generation of mod_dav_svn config file on repository group
31 automatic generation of mod_dav_svn config file on repository group
34 changes.
32 changes.
35 """
33 """
36 try:
34 try:
37 generate_mod_dav_svn_config(event.request.registry)
35 generate_mod_dav_svn_config(event.request.registry)
38 except Exception:
36 except Exception:
39 log.exception(
37 log.exception(
40 'Exception while generating subversion mod_dav_svn configuration.')
38 'Exception while generating subversion mod_dav_svn configuration.')
@@ -1,121 +1,119 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import re
19 import re
22 import os
20 import os
23 import mock
21 import mock
24 import pytest
22 import pytest
25
23
26 from rhodecode.apps.svn_support import utils
24 from rhodecode.apps.svn_support import utils
27
25
28
26
29 @pytest.mark.usefixtures('config_stub')
27 @pytest.mark.usefixtures('config_stub')
30 class TestModDavSvnConfig(object):
28 class TestModDavSvnConfig(object):
31
29
32 @classmethod
30 @classmethod
33 def setup_class(cls):
31 def setup_class(cls):
34 cls.location_root = u'/location/root/çµäö'
32 cls.location_root = '/location/root/çµäö'
35 cls.parent_path_root = u'/parent/path/çµäö'
33 cls.parent_path_root = '/parent/path/çµäö'
36 cls.realm = u'Dummy Realm (äöüçµ)'
34 cls.realm = 'Dummy Realm (äöüçµ)'
37
35
38 @classmethod
36 @classmethod
39 def get_repo_group_mocks(cls, count=1):
37 def get_repo_group_mocks(cls, count=1):
40 repo_groups = []
38 repo_groups = []
41 for num in range(0, count):
39 for num in range(0, count):
42 full_path = f'/path/to/RepöGröúp-°µ {num}'
40 full_path = f'/path/to/RepöGröúp-°µ {num}'
43 repo_group_mock = mock.MagicMock()
41 repo_group_mock = mock.MagicMock()
44 repo_group_mock.full_path = full_path
42 repo_group_mock.full_path = full_path
45 repo_group_mock.full_path_splitted = full_path.split('/')
43 repo_group_mock.full_path_splitted = full_path.split('/')
46 repo_groups.append(repo_group_mock)
44 repo_groups.append(repo_group_mock)
47 return repo_groups
45 return repo_groups
48
46
49 def assert_root_location_directive(self, config):
47 def assert_root_location_directive(self, config):
50 pattern = u'<Location "{location}">'.format(
48 pattern = '<Location "{location}">'.format(
51 location=self.location_root)
49 location=self.location_root)
52 assert len(re.findall(pattern, config)) == 1
50 assert len(re.findall(pattern, config)) == 1
53
51
54 def assert_group_location_directive(self, config, group_path):
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 location=self.location_root, group_path=group_path)
54 location=self.location_root, group_path=group_path)
57 assert len(re.findall(pattern, config)) == 1
55 assert len(re.findall(pattern, config)) == 1
58
56
59 def test_render_mod_dav_svn_config(self):
57 def test_render_mod_dav_svn_config(self):
60 repo_groups = self.get_repo_group_mocks(count=10)
58 repo_groups = self.get_repo_group_mocks(count=10)
61 generated_config = utils._render_mod_dav_svn_config(
59 generated_config = utils._render_mod_dav_svn_config(
62 parent_path_root=self.parent_path_root,
60 parent_path_root=self.parent_path_root,
63 list_parent_path=True,
61 list_parent_path=True,
64 location_root=self.location_root,
62 location_root=self.location_root,
65 repo_groups=repo_groups,
63 repo_groups=repo_groups,
66 realm=self.realm,
64 realm=self.realm,
67 use_ssl=True,
65 use_ssl=True,
68 template=''
66 template=''
69 )
67 )
70 # Assert that one location directive exists for each repository group.
68 # Assert that one location directive exists for each repository group.
71 for group in repo_groups:
69 for group in repo_groups:
72 self.assert_group_location_directive(
70 self.assert_group_location_directive(
73 generated_config, group.full_path)
71 generated_config, group.full_path)
74
72
75 # Assert that the root location directive exists.
73 # Assert that the root location directive exists.
76 self.assert_root_location_directive(generated_config)
74 self.assert_root_location_directive(generated_config)
77
75
78 def test_render_mod_dav_svn_config_with_alternative_template(self, tmpdir):
76 def test_render_mod_dav_svn_config_with_alternative_template(self, tmpdir):
79 repo_groups = self.get_repo_group_mocks(count=10)
77 repo_groups = self.get_repo_group_mocks(count=10)
80 test_file_path = os.path.join(str(tmpdir), 'example.mako')
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 f.write('TEST_EXAMPLE\n')
80 f.write('TEST_EXAMPLE\n')
83
81
84 generated_config = utils._render_mod_dav_svn_config(
82 generated_config = utils._render_mod_dav_svn_config(
85 parent_path_root=self.parent_path_root,
83 parent_path_root=self.parent_path_root,
86 list_parent_path=True,
84 list_parent_path=True,
87 location_root=self.location_root,
85 location_root=self.location_root,
88 repo_groups=repo_groups,
86 repo_groups=repo_groups,
89 realm=self.realm,
87 realm=self.realm,
90 use_ssl=True,
88 use_ssl=True,
91 template=test_file_path
89 template=test_file_path
92 )
90 )
93 assert 'TEST_EXAMPLE' in generated_config
91 assert 'TEST_EXAMPLE' in generated_config
94
92
95 @pytest.mark.parametrize('list_parent_path', [True, False])
93 @pytest.mark.parametrize('list_parent_path', [True, False])
96 @pytest.mark.parametrize('use_ssl', [True, False])
94 @pytest.mark.parametrize('use_ssl', [True, False])
97 def test_list_parent_path(self, list_parent_path, use_ssl):
95 def test_list_parent_path(self, list_parent_path, use_ssl):
98 generated_config = utils._render_mod_dav_svn_config(
96 generated_config = utils._render_mod_dav_svn_config(
99 parent_path_root=self.parent_path_root,
97 parent_path_root=self.parent_path_root,
100 list_parent_path=list_parent_path,
98 list_parent_path=list_parent_path,
101 location_root=self.location_root,
99 location_root=self.location_root,
102 repo_groups=self.get_repo_group_mocks(count=10),
100 repo_groups=self.get_repo_group_mocks(count=10),
103 realm=self.realm,
101 realm=self.realm,
104 use_ssl=use_ssl,
102 use_ssl=use_ssl,
105 template=''
103 template=''
106 )
104 )
107
105
108 # Assert that correct configuration directive is present.
106 # Assert that correct configuration directive is present.
109 if list_parent_path:
107 if list_parent_path:
110 assert not re.search(r'SVNListParentPath\s+Off', generated_config)
108 assert not re.search(r'SVNListParentPath\s+Off', generated_config)
111 assert re.search(r'SVNListParentPath\s+On', generated_config)
109 assert re.search(r'SVNListParentPath\s+On', generated_config)
112 else:
110 else:
113 assert re.search(r'SVNListParentPath\s+Off', generated_config)
111 assert re.search(r'SVNListParentPath\s+Off', generated_config)
114 assert not re.search(r'SVNListParentPath\s+On', generated_config)
112 assert not re.search(r'SVNListParentPath\s+On', generated_config)
115
113
116 if use_ssl:
114 if use_ssl:
117 assert 'RequestHeader edit Destination ^https: http: early' \
115 assert 'RequestHeader edit Destination ^https: http: early' \
118 in generated_config
116 in generated_config
119 else:
117 else:
120 assert '#RequestHeader edit Destination ^https: http: early' \
118 assert '#RequestHeader edit Destination ^https: http: early' \
121 in generated_config
119 in generated_config
@@ -1,99 +1,97 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import codecs
19 import codecs
22 import logging
20 import logging
23 import os
21 import os
24 from pyramid.renderers import render
22 from pyramid.renderers import render
25
23
26 from rhodecode.events import trigger
24 from rhodecode.events import trigger
27 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path
25 from rhodecode.lib.utils import get_rhodecode_realm, get_rhodecode_base_path
28 from rhodecode.lib.utils2 import str2bool
26 from rhodecode.lib.utils2 import str2bool
29 from rhodecode.model.db import RepoGroup
27 from rhodecode.model.db import RepoGroup
30
28
31 from . import config_keys
29 from . import config_keys
32 from .events import ModDavSvnConfigChange
30 from .events import ModDavSvnConfigChange
33
31
34
32
35 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
36
34
37
35
38 def write_mod_dav_svn_config(settings):
36 def write_mod_dav_svn_config(settings):
39 use_ssl = str2bool(settings['force_https'])
37 use_ssl = str2bool(settings['force_https'])
40 file_path = settings[config_keys.config_file_path]
38 file_path = settings[config_keys.config_file_path]
41 config = _render_mod_dav_svn_config(
39 config = _render_mod_dav_svn_config(
42 use_ssl=use_ssl,
40 use_ssl=use_ssl,
43 parent_path_root=get_rhodecode_base_path(),
41 parent_path_root=get_rhodecode_base_path(),
44 list_parent_path=settings[config_keys.list_parent_path],
42 list_parent_path=settings[config_keys.list_parent_path],
45 location_root=settings[config_keys.location_root],
43 location_root=settings[config_keys.location_root],
46 repo_groups=RepoGroup.get_all_repo_groups(),
44 repo_groups=RepoGroup.get_all_repo_groups(),
47 realm=get_rhodecode_realm(), template=settings[config_keys.template])
45 realm=get_rhodecode_realm(), template=settings[config_keys.template])
48 _write_mod_dav_svn_config(config, file_path)
46 _write_mod_dav_svn_config(config, file_path)
49 return file_path
47 return file_path
50
48
51
49
52 def generate_mod_dav_svn_config(registry):
50 def generate_mod_dav_svn_config(registry):
53 """
51 """
54 Generate the configuration file for use with subversion's mod_dav_svn
52 Generate the configuration file for use with subversion's mod_dav_svn
55 module. The configuration has to contain a <Location> block for each
53 module. The configuration has to contain a <Location> block for each
56 available repository group because the mod_dav_svn module does not support
54 available repository group because the mod_dav_svn module does not support
57 repositories organized in sub folders.
55 repositories organized in sub folders.
58 """
56 """
59 settings = registry.settings
57 settings = registry.settings
60 file_path = write_mod_dav_svn_config(settings)
58 file_path = write_mod_dav_svn_config(settings)
61
59
62 # Trigger an event on mod dav svn configuration change.
60 # Trigger an event on mod dav svn configuration change.
63 trigger(ModDavSvnConfigChange(), registry)
61 trigger(ModDavSvnConfigChange(), registry)
64 return file_path
62 return file_path
65
63
66
64
67 def _render_mod_dav_svn_config(
65 def _render_mod_dav_svn_config(
68 parent_path_root, list_parent_path, location_root, repo_groups, realm,
66 parent_path_root, list_parent_path, location_root, repo_groups, realm,
69 use_ssl, template):
67 use_ssl, template):
70 """
68 """
71 Render mod_dav_svn configuration to string.
69 Render mod_dav_svn configuration to string.
72 """
70 """
73 repo_group_paths = []
71 repo_group_paths = []
74 for repo_group in repo_groups:
72 for repo_group in repo_groups:
75 group_path = repo_group.full_path_splitted
73 group_path = repo_group.full_path_splitted
76 location = os.path.join(location_root, *group_path)
74 location = os.path.join(location_root, *group_path)
77 parent_path = os.path.join(parent_path_root, *group_path)
75 parent_path = os.path.join(parent_path_root, *group_path)
78 repo_group_paths.append((location, parent_path))
76 repo_group_paths.append((location, parent_path))
79
77
80 context = {
78 context = {
81 'location_root': location_root,
79 'location_root': location_root,
82 'parent_path_root': parent_path_root,
80 'parent_path_root': parent_path_root,
83 'repo_group_paths': repo_group_paths,
81 'repo_group_paths': repo_group_paths,
84 'svn_list_parent_path': list_parent_path,
82 'svn_list_parent_path': list_parent_path,
85 'rhodecode_realm': realm,
83 'rhodecode_realm': realm,
86 'use_https': use_ssl,
84 'use_https': use_ssl,
87 }
85 }
88 template = template or \
86 template = template or \
89 'rhodecode:apps/svn_support/templates/mod-dav-svn.conf.mako'
87 'rhodecode:apps/svn_support/templates/mod-dav-svn.conf.mako'
90 # Render the configuration template to string.
88 # Render the configuration template to string.
91 return render(template, context)
89 return render(template, context)
92
90
93
91
94 def _write_mod_dav_svn_config(config, filepath):
92 def _write_mod_dav_svn_config(config, filepath):
95 """
93 """
96 Write mod_dav_svn config to file.
94 Write mod_dav_svn config to file.
97 """
95 """
98 with codecs.open(filepath, 'w', encoding='utf-8') as f:
96 with codecs.open(filepath, 'w', encoding='utf-8') as f:
99 f.write(config)
97 f.write(config)
@@ -1,161 +1,159 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 from rhodecode.apps._base.navigation import NavigationRegistry
20 from rhodecode.apps._base.navigation import NavigationRegistry
23 from rhodecode.apps._base import ADMIN_PREFIX
21 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.lib.utils2 import str2bool
22 from rhodecode.lib.utils2 import str2bool
25
23
26
24
27 def admin_routes(config):
25 def admin_routes(config):
28 """
26 """
29 User groups /_admin prefixed routes
27 User groups /_admin prefixed routes
30 """
28 """
31 from rhodecode.apps.user_group.views import UserGroupsView
29 from rhodecode.apps.user_group.views import UserGroupsView
32
30
33 config.add_route(
31 config.add_route(
34 name='user_group_members_data',
32 name='user_group_members_data',
35 pattern='/user_groups/{user_group_id:\d+}/members',
33 pattern='/user_groups/{user_group_id:\d+}/members',
36 user_group_route=True)
34 user_group_route=True)
37 config.add_view(
35 config.add_view(
38 UserGroupsView,
36 UserGroupsView,
39 attr='user_group_members',
37 attr='user_group_members',
40 route_name='user_group_members_data', request_method='GET',
38 route_name='user_group_members_data', request_method='GET',
41 renderer='json_ext', xhr=True)
39 renderer='json_ext', xhr=True)
42
40
43 # user groups perms
41 # user groups perms
44 config.add_route(
42 config.add_route(
45 name='edit_user_group_perms_summary',
43 name='edit_user_group_perms_summary',
46 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary',
44 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary',
47 user_group_route=True)
45 user_group_route=True)
48 config.add_view(
46 config.add_view(
49 UserGroupsView,
47 UserGroupsView,
50 attr='user_group_perms_summary',
48 attr='user_group_perms_summary',
51 route_name='edit_user_group_perms_summary', request_method='GET',
49 route_name='edit_user_group_perms_summary', request_method='GET',
52 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
50 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
53
51
54 config.add_route(
52 config.add_route(
55 name='edit_user_group_perms_summary_json',
53 name='edit_user_group_perms_summary_json',
56 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary/json',
54 pattern='/user_groups/{user_group_id:\d+}/edit/permissions_summary/json',
57 user_group_route=True)
55 user_group_route=True)
58 config.add_view(
56 config.add_view(
59 UserGroupsView,
57 UserGroupsView,
60 attr='user_group_perms_summary_json',
58 attr='user_group_perms_summary_json',
61 route_name='edit_user_group_perms_summary_json', request_method='GET',
59 route_name='edit_user_group_perms_summary_json', request_method='GET',
62 renderer='json_ext')
60 renderer='json_ext')
63
61
64 # user groups edit
62 # user groups edit
65 config.add_route(
63 config.add_route(
66 name='edit_user_group',
64 name='edit_user_group',
67 pattern='/user_groups/{user_group_id:\d+}/edit',
65 pattern='/user_groups/{user_group_id:\d+}/edit',
68 user_group_route=True)
66 user_group_route=True)
69 config.add_view(
67 config.add_view(
70 UserGroupsView,
68 UserGroupsView,
71 attr='user_group_edit',
69 attr='user_group_edit',
72 route_name='edit_user_group', request_method='GET',
70 route_name='edit_user_group', request_method='GET',
73 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
71 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
74
72
75 # user groups update
73 # user groups update
76 config.add_route(
74 config.add_route(
77 name='user_groups_update',
75 name='user_groups_update',
78 pattern='/user_groups/{user_group_id:\d+}/update',
76 pattern='/user_groups/{user_group_id:\d+}/update',
79 user_group_route=True)
77 user_group_route=True)
80 config.add_view(
78 config.add_view(
81 UserGroupsView,
79 UserGroupsView,
82 attr='user_group_update',
80 attr='user_group_update',
83 route_name='user_groups_update', request_method='POST',
81 route_name='user_groups_update', request_method='POST',
84 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
82 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
85
83
86 config.add_route(
84 config.add_route(
87 name='edit_user_group_global_perms',
85 name='edit_user_group_global_perms',
88 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions',
86 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions',
89 user_group_route=True)
87 user_group_route=True)
90 config.add_view(
88 config.add_view(
91 UserGroupsView,
89 UserGroupsView,
92 attr='user_group_global_perms_edit',
90 attr='user_group_global_perms_edit',
93 route_name='edit_user_group_global_perms', request_method='GET',
91 route_name='edit_user_group_global_perms', request_method='GET',
94 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
92 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
95
93
96 config.add_route(
94 config.add_route(
97 name='edit_user_group_global_perms_update',
95 name='edit_user_group_global_perms_update',
98 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions/update',
96 pattern='/user_groups/{user_group_id:\d+}/edit/global_permissions/update',
99 user_group_route=True)
97 user_group_route=True)
100 config.add_view(
98 config.add_view(
101 UserGroupsView,
99 UserGroupsView,
102 attr='user_group_global_perms_update',
100 attr='user_group_global_perms_update',
103 route_name='edit_user_group_global_perms_update', request_method='POST',
101 route_name='edit_user_group_global_perms_update', request_method='POST',
104 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
102 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
105
103
106 config.add_route(
104 config.add_route(
107 name='edit_user_group_perms',
105 name='edit_user_group_perms',
108 pattern='/user_groups/{user_group_id:\d+}/edit/permissions',
106 pattern='/user_groups/{user_group_id:\d+}/edit/permissions',
109 user_group_route=True)
107 user_group_route=True)
110 config.add_view(
108 config.add_view(
111 UserGroupsView,
109 UserGroupsView,
112 attr='user_group_edit_perms',
110 attr='user_group_edit_perms',
113 route_name='edit_user_group_perms', request_method='GET',
111 route_name='edit_user_group_perms', request_method='GET',
114 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
112 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
115
113
116 config.add_route(
114 config.add_route(
117 name='edit_user_group_perms_update',
115 name='edit_user_group_perms_update',
118 pattern='/user_groups/{user_group_id:\d+}/edit/permissions/update',
116 pattern='/user_groups/{user_group_id:\d+}/edit/permissions/update',
119 user_group_route=True)
117 user_group_route=True)
120 config.add_view(
118 config.add_view(
121 UserGroupsView,
119 UserGroupsView,
122 attr='user_group_update_perms',
120 attr='user_group_update_perms',
123 route_name='edit_user_group_perms_update', request_method='POST',
121 route_name='edit_user_group_perms_update', request_method='POST',
124 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
122 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
125
123
126 config.add_route(
124 config.add_route(
127 name='edit_user_group_advanced',
125 name='edit_user_group_advanced',
128 pattern='/user_groups/{user_group_id:\d+}/edit/advanced',
126 pattern='/user_groups/{user_group_id:\d+}/edit/advanced',
129 user_group_route=True)
127 user_group_route=True)
130 config.add_view(
128 config.add_view(
131 UserGroupsView,
129 UserGroupsView,
132 attr='user_group_edit_advanced',
130 attr='user_group_edit_advanced',
133 route_name='edit_user_group_advanced', request_method='GET',
131 route_name='edit_user_group_advanced', request_method='GET',
134 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
132 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
135
133
136 config.add_route(
134 config.add_route(
137 name='edit_user_group_advanced_sync',
135 name='edit_user_group_advanced_sync',
138 pattern='/user_groups/{user_group_id:\d+}/edit/advanced/sync',
136 pattern='/user_groups/{user_group_id:\d+}/edit/advanced/sync',
139 user_group_route=True)
137 user_group_route=True)
140 config.add_view(
138 config.add_view(
141 UserGroupsView,
139 UserGroupsView,
142 attr='user_group_edit_advanced_set_synchronization',
140 attr='user_group_edit_advanced_set_synchronization',
143 route_name='edit_user_group_advanced_sync', request_method='POST',
141 route_name='edit_user_group_advanced_sync', request_method='POST',
144 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
142 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
145
143
146 # user groups delete
144 # user groups delete
147 config.add_route(
145 config.add_route(
148 name='user_groups_delete',
146 name='user_groups_delete',
149 pattern='/user_groups/{user_group_id:\d+}/delete',
147 pattern='/user_groups/{user_group_id:\d+}/delete',
150 user_group_route=True)
148 user_group_route=True)
151 config.add_view(
149 config.add_view(
152 UserGroupsView,
150 UserGroupsView,
153 attr='user_group_delete',
151 attr='user_group_delete',
154 route_name='user_groups_delete', request_method='POST',
152 route_name='user_groups_delete', request_method='POST',
155 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
153 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
156
154
157
155
158 def includeme(config):
156 def includeme(config):
159 # main admin routes
157 # main admin routes
160 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
158 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
161
159
@@ -1,81 +1,80 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import pytest
19 import pytest
21
20
22 from rhodecode.tests.utils import permission_update_data_generator
21 from rhodecode.tests.utils import permission_update_data_generator
23
22
24
23
25 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
26 import urllib.request
25 import urllib.request
27 import urllib.parse
26 import urllib.parse
28 import urllib.error
27 import urllib.error
29 from rhodecode.apps._base import ADMIN_PREFIX
28 from rhodecode.apps._base import ADMIN_PREFIX
30
29
31 base_url = {
30 base_url = {
32 'edit_user_group_perms':
31 'edit_user_group_perms':
33 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
32 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
34 'edit_user_group_perms_update':
33 'edit_user_group_perms_update':
35 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
34 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
36 }[name].format(**kwargs)
35 }[name].format(**kwargs)
37
36
38 if params:
37 if params:
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
39 return base_url
41
40
42
41
43 @pytest.mark.usefixtures("app")
42 @pytest.mark.usefixtures("app")
44 class TestUserGroupPermissionsView(object):
43 class TestUserGroupPermissionsView(object):
45
44
46 def test_edit_perms_view(self, user_util, autologin_user):
45 def test_edit_perms_view(self, user_util, autologin_user):
47 user_group = user_util.create_user_group()
46 user_group = user_util.create_user_group()
48 self.app.get(
47 self.app.get(
49 route_path('edit_user_group_perms',
48 route_path('edit_user_group_perms',
50 user_group_id=user_group.users_group_id), status=200)
49 user_group_id=user_group.users_group_id), status=200)
51
50
52 def test_update_permissions(self, csrf_token, user_util):
51 def test_update_permissions(self, csrf_token, user_util):
53 user_group = user_util.create_user_group()
52 user_group = user_util.create_user_group()
54 user_group_id = user_group.users_group_id
53 user_group_id = user_group.users_group_id
55 user = user_util.create_user()
54 user = user_util.create_user()
56 user_id = user.user_id
55 user_id = user.user_id
57 username = user.username
56 username = user.username
58
57
59 # grant new
58 # grant new
60 form_data = permission_update_data_generator(
59 form_data = permission_update_data_generator(
61 csrf_token,
60 csrf_token,
62 default='usergroup.write',
61 default='usergroup.write',
63 grant=[(user_id, 'usergroup.write', username, 'user')])
62 grant=[(user_id, 'usergroup.write', username, 'user')])
64
63
65 response = self.app.post(
64 response = self.app.post(
66 route_path('edit_user_group_perms_update',
65 route_path('edit_user_group_perms_update',
67 user_group_id=user_group_id), form_data).follow()
66 user_group_id=user_group_id), form_data).follow()
68
67
69 assert 'User Group permissions updated' in response
68 assert 'User Group permissions updated' in response
70
69
71 # revoke given
70 # revoke given
72 form_data = permission_update_data_generator(
71 form_data = permission_update_data_generator(
73 csrf_token,
72 csrf_token,
74 default='usergroup.read',
73 default='usergroup.read',
75 revoke=[(user_id, 'user')])
74 revoke=[(user_id, 'user')])
76
75
77 response = self.app.post(
76 response = self.app.post(
78 route_path('edit_user_group_perms_update',
77 route_path('edit_user_group_perms_update',
79 user_group_id=user_group_id), form_data).follow()
78 user_group_id=user_group_id), form_data).follow()
80
79
81 assert 'User Group permissions updated' in response
80 assert 'User Group permissions updated' in response
@@ -1,514 +1,512 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 import peppercorn
21 import peppercorn
24 import formencode
22 import formencode
25 import formencode.htmlfill
23 import formencode.htmlfill
26 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
27
25
28 from pyramid.response import Response
26 from pyramid.response import Response
29 from pyramid.renderers import render
27 from pyramid.renderers import render
30
28
31 from rhodecode import events
29 from rhodecode import events
32 from rhodecode.lib.exceptions import (
30 from rhodecode.lib.exceptions import (
33 RepoGroupAssignmentError, UserGroupAssignedException)
31 RepoGroupAssignmentError, UserGroupAssignedException)
34 from rhodecode.model.forms import (
32 from rhodecode.model.forms import (
35 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
33 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
36 UserPermissionsForm)
34 UserPermissionsForm)
37 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.permission import PermissionModel
38
36
39 from rhodecode.apps._base import UserGroupAppView
37 from rhodecode.apps._base import UserGroupAppView
40 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
41 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
39 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
42 from rhodecode.lib import helpers as h, audit_logger
40 from rhodecode.lib import helpers as h, audit_logger
43 from rhodecode.lib.utils2 import str2bool, safe_int
41 from rhodecode.lib.utils2 import str2bool, safe_int
44 from rhodecode.model.db import User, UserGroup
42 from rhodecode.model.db import User, UserGroup
45 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
46 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
47
45
48 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
49
47
50
48
51 class UserGroupsView(UserGroupAppView):
49 class UserGroupsView(UserGroupAppView):
52
50
53 def load_default_context(self):
51 def load_default_context(self):
54 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
55
53
56 PermissionModel().set_global_permission_choices(
54 PermissionModel().set_global_permission_choices(
57 c, gettext_translator=self.request.translate)
55 c, gettext_translator=self.request.translate)
58
56
59 return c
57 return c
60
58
61 @LoginRequired()
59 @LoginRequired()
62 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
60 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
63 def user_group_members(self):
61 def user_group_members(self):
64 """
62 """
65 Return members of given user group
63 Return members of given user group
66 """
64 """
67 self.load_default_context()
65 self.load_default_context()
68 user_group = self.db_user_group
66 user_group = self.db_user_group
69 group_members_obj = sorted((x.user for x in user_group.members),
67 group_members_obj = sorted((x.user for x in user_group.members),
70 key=lambda u: u.username.lower())
68 key=lambda u: u.username.lower())
71
69
72 group_members = [
70 group_members = [
73 {
71 {
74 'id': user.user_id,
72 'id': user.user_id,
75 'first_name': user.first_name,
73 'first_name': user.first_name,
76 'last_name': user.last_name,
74 'last_name': user.last_name,
77 'username': user.username,
75 'username': user.username,
78 'icon_link': h.gravatar_url(user.email, 30, request=self.request),
76 'icon_link': h.gravatar_url(user.email, 30, request=self.request),
79 'value_display': h.person(user.email),
77 'value_display': h.person(user.email),
80 'value': user.username,
78 'value': user.username,
81 'value_type': 'user',
79 'value_type': 'user',
82 'active': user.active,
80 'active': user.active,
83 }
81 }
84 for user in group_members_obj
82 for user in group_members_obj
85 ]
83 ]
86
84
87 return {
85 return {
88 'members': group_members
86 'members': group_members
89 }
87 }
90
88
91 @LoginRequired()
89 @LoginRequired()
92 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
90 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
93 def user_group_perms_summary(self):
91 def user_group_perms_summary(self):
94 c = self.load_default_context()
92 c = self.load_default_context()
95 c.user_group = self.db_user_group
93 c.user_group = self.db_user_group
96 c.active = 'perms_summary'
94 c.active = 'perms_summary'
97 c.permissions = UserGroupModel().get_perms_summary(
95 c.permissions = UserGroupModel().get_perms_summary(
98 c.user_group.users_group_id)
96 c.user_group.users_group_id)
99 return self._get_template_context(c)
97 return self._get_template_context(c)
100
98
101 @LoginRequired()
99 @LoginRequired()
102 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
100 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
103 def user_group_perms_summary_json(self):
101 def user_group_perms_summary_json(self):
104 self.load_default_context()
102 self.load_default_context()
105 user_group = self.db_user_group
103 user_group = self.db_user_group
106 return UserGroupModel().get_perms_summary(user_group.users_group_id)
104 return UserGroupModel().get_perms_summary(user_group.users_group_id)
107
105
108 def _revoke_perms_on_yourself(self, form_result):
106 def _revoke_perms_on_yourself(self, form_result):
109 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
107 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
110 form_result['perm_updates'])
108 form_result['perm_updates'])
111 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
109 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
112 form_result['perm_additions'])
110 form_result['perm_additions'])
113 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
111 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
114 form_result['perm_deletions'])
112 form_result['perm_deletions'])
115 admin_perm = 'usergroup.admin'
113 admin_perm = 'usergroup.admin'
116 if _updates and _updates[0][1] != admin_perm or \
114 if _updates and _updates[0][1] != admin_perm or \
117 _additions and _additions[0][1] != admin_perm or \
115 _additions and _additions[0][1] != admin_perm or \
118 _deletions and _deletions[0][1] != admin_perm:
116 _deletions and _deletions[0][1] != admin_perm:
119 return True
117 return True
120 return False
118 return False
121
119
122 @LoginRequired()
120 @LoginRequired()
123 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
121 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
124 @CSRFRequired()
122 @CSRFRequired()
125 def user_group_update(self):
123 def user_group_update(self):
126 _ = self.request.translate
124 _ = self.request.translate
127
125
128 user_group = self.db_user_group
126 user_group = self.db_user_group
129 user_group_id = user_group.users_group_id
127 user_group_id = user_group.users_group_id
130
128
131 old_user_group_name = self.db_user_group_name
129 old_user_group_name = self.db_user_group_name
132 new_user_group_name = old_user_group_name
130 new_user_group_name = old_user_group_name
133
131
134 c = self.load_default_context()
132 c = self.load_default_context()
135 c.user_group = user_group
133 c.user_group = user_group
136 c.group_members_obj = [x.user for x in c.user_group.members]
134 c.group_members_obj = [x.user for x in c.user_group.members]
137 c.group_members_obj.sort(key=lambda u: u.username.lower())
135 c.group_members_obj.sort(key=lambda u: u.username.lower())
138 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
136 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
139 c.active = 'settings'
137 c.active = 'settings'
140
138
141 users_group_form = UserGroupForm(
139 users_group_form = UserGroupForm(
142 self.request.translate, edit=True,
140 self.request.translate, edit=True,
143 old_data=c.user_group.get_dict(), allow_disabled=True)()
141 old_data=c.user_group.get_dict(), allow_disabled=True)()
144
142
145 old_values = c.user_group.get_api_data()
143 old_values = c.user_group.get_api_data()
146
144
147 try:
145 try:
148 form_result = users_group_form.to_python(self.request.POST)
146 form_result = users_group_form.to_python(self.request.POST)
149 pstruct = peppercorn.parse(self.request.POST.items())
147 pstruct = peppercorn.parse(self.request.POST.items())
150 form_result['users_group_members'] = pstruct['user_group_members']
148 form_result['users_group_members'] = pstruct['user_group_members']
151
149
152 user_group, added_members, removed_members = \
150 user_group, added_members, removed_members = \
153 UserGroupModel().update(c.user_group, form_result)
151 UserGroupModel().update(c.user_group, form_result)
154 new_user_group_name = form_result['users_group_name']
152 new_user_group_name = form_result['users_group_name']
155
153
156 for user_id in added_members:
154 for user_id in added_members:
157 user = User.get(user_id)
155 user = User.get(user_id)
158 user_data = user.get_api_data()
156 user_data = user.get_api_data()
159 audit_logger.store_web(
157 audit_logger.store_web(
160 'user_group.edit.member.add',
158 'user_group.edit.member.add',
161 action_data={'user': user_data, 'old_data': old_values},
159 action_data={'user': user_data, 'old_data': old_values},
162 user=self._rhodecode_user)
160 user=self._rhodecode_user)
163
161
164 for user_id in removed_members:
162 for user_id in removed_members:
165 user = User.get(user_id)
163 user = User.get(user_id)
166 user_data = user.get_api_data()
164 user_data = user.get_api_data()
167 audit_logger.store_web(
165 audit_logger.store_web(
168 'user_group.edit.member.delete',
166 'user_group.edit.member.delete',
169 action_data={'user': user_data, 'old_data': old_values},
167 action_data={'user': user_data, 'old_data': old_values},
170 user=self._rhodecode_user)
168 user=self._rhodecode_user)
171
169
172 audit_logger.store_web(
170 audit_logger.store_web(
173 'user_group.edit', action_data={'old_data': old_values},
171 'user_group.edit', action_data={'old_data': old_values},
174 user=self._rhodecode_user)
172 user=self._rhodecode_user)
175
173
176 h.flash(_('Updated user group %s') % new_user_group_name,
174 h.flash(_('Updated user group %s') % new_user_group_name,
177 category='success')
175 category='success')
178
176
179 affected_user_ids = []
177 affected_user_ids = []
180 for user_id in added_members + removed_members:
178 for user_id in added_members + removed_members:
181 affected_user_ids.append(user_id)
179 affected_user_ids.append(user_id)
182
180
183 name_changed = old_user_group_name != new_user_group_name
181 name_changed = old_user_group_name != new_user_group_name
184 if name_changed:
182 if name_changed:
185 owner = User.get_by_username(form_result['user'])
183 owner = User.get_by_username(form_result['user'])
186 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
184 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
187 affected_user_ids.append(self._rhodecode_user.user_id)
185 affected_user_ids.append(self._rhodecode_user.user_id)
188 affected_user_ids.append(owner_id)
186 affected_user_ids.append(owner_id)
189
187
190 PermissionModel().trigger_permission_flush(affected_user_ids)
188 PermissionModel().trigger_permission_flush(affected_user_ids)
191
189
192 Session().commit()
190 Session().commit()
193 except formencode.Invalid as errors:
191 except formencode.Invalid as errors:
194 defaults = errors.value
192 defaults = errors.value
195 e = errors.error_dict or {}
193 e = errors.error_dict or {}
196
194
197 data = render(
195 data = render(
198 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
196 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
199 self._get_template_context(c), self.request)
197 self._get_template_context(c), self.request)
200 html = formencode.htmlfill.render(
198 html = formencode.htmlfill.render(
201 data,
199 data,
202 defaults=defaults,
200 defaults=defaults,
203 errors=e,
201 errors=e,
204 prefix_error=False,
202 prefix_error=False,
205 encoding="UTF-8",
203 encoding="UTF-8",
206 force_defaults=False
204 force_defaults=False
207 )
205 )
208 return Response(html)
206 return Response(html)
209
207
210 except Exception:
208 except Exception:
211 log.exception("Exception during update of user group")
209 log.exception("Exception during update of user group")
212 h.flash(_('Error occurred during update of user group %s')
210 h.flash(_('Error occurred during update of user group %s')
213 % new_user_group_name, category='error')
211 % new_user_group_name, category='error')
214
212
215 raise HTTPFound(
213 raise HTTPFound(
216 h.route_path('edit_user_group', user_group_id=user_group_id))
214 h.route_path('edit_user_group', user_group_id=user_group_id))
217
215
218 @LoginRequired()
216 @LoginRequired()
219 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
217 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
220 @CSRFRequired()
218 @CSRFRequired()
221 def user_group_delete(self):
219 def user_group_delete(self):
222 _ = self.request.translate
220 _ = self.request.translate
223 user_group = self.db_user_group
221 user_group = self.db_user_group
224
222
225 self.load_default_context()
223 self.load_default_context()
226 force = str2bool(self.request.POST.get('force'))
224 force = str2bool(self.request.POST.get('force'))
227
225
228 old_values = user_group.get_api_data()
226 old_values = user_group.get_api_data()
229 try:
227 try:
230 UserGroupModel().delete(user_group, force=force)
228 UserGroupModel().delete(user_group, force=force)
231 audit_logger.store_web(
229 audit_logger.store_web(
232 'user.delete', action_data={'old_data': old_values},
230 'user.delete', action_data={'old_data': old_values},
233 user=self._rhodecode_user)
231 user=self._rhodecode_user)
234 Session().commit()
232 Session().commit()
235 h.flash(_('Successfully deleted user group'), category='success')
233 h.flash(_('Successfully deleted user group'), category='success')
236 except UserGroupAssignedException as e:
234 except UserGroupAssignedException as e:
237 h.flash(str(e), category='error')
235 h.flash(str(e), category='error')
238 except Exception:
236 except Exception:
239 log.exception("Exception during deletion of user group")
237 log.exception("Exception during deletion of user group")
240 h.flash(_('An error occurred during deletion of user group'),
238 h.flash(_('An error occurred during deletion of user group'),
241 category='error')
239 category='error')
242 raise HTTPFound(h.route_path('user_groups'))
240 raise HTTPFound(h.route_path('user_groups'))
243
241
244 @LoginRequired()
242 @LoginRequired()
245 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
243 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
246 def user_group_edit(self):
244 def user_group_edit(self):
247 user_group = self.db_user_group
245 user_group = self.db_user_group
248
246
249 c = self.load_default_context()
247 c = self.load_default_context()
250 c.user_group = user_group
248 c.user_group = user_group
251 c.group_members_obj = [x.user for x in c.user_group.members]
249 c.group_members_obj = [x.user for x in c.user_group.members]
252 c.group_members_obj.sort(key=lambda u: u.username.lower())
250 c.group_members_obj.sort(key=lambda u: u.username.lower())
253 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
251 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
254
252
255 c.active = 'settings'
253 c.active = 'settings'
256
254
257 defaults = user_group.get_dict()
255 defaults = user_group.get_dict()
258 # fill owner
256 # fill owner
259 if user_group.user:
257 if user_group.user:
260 defaults.update({'user': user_group.user.username})
258 defaults.update({'user': user_group.user.username})
261 else:
259 else:
262 replacement_user = User.get_first_super_admin().username
260 replacement_user = User.get_first_super_admin().username
263 defaults.update({'user': replacement_user})
261 defaults.update({'user': replacement_user})
264
262
265 data = render(
263 data = render(
266 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
264 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
267 self._get_template_context(c), self.request)
265 self._get_template_context(c), self.request)
268 html = formencode.htmlfill.render(
266 html = formencode.htmlfill.render(
269 data,
267 data,
270 defaults=defaults,
268 defaults=defaults,
271 encoding="UTF-8",
269 encoding="UTF-8",
272 force_defaults=False
270 force_defaults=False
273 )
271 )
274 return Response(html)
272 return Response(html)
275
273
276 @LoginRequired()
274 @LoginRequired()
277 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
275 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
278 def user_group_edit_perms(self):
276 def user_group_edit_perms(self):
279 user_group = self.db_user_group
277 user_group = self.db_user_group
280 c = self.load_default_context()
278 c = self.load_default_context()
281 c.user_group = user_group
279 c.user_group = user_group
282 c.active = 'perms'
280 c.active = 'perms'
283
281
284 defaults = {}
282 defaults = {}
285 # fill user group users
283 # fill user group users
286 for p in c.user_group.user_user_group_to_perm:
284 for p in c.user_group.user_user_group_to_perm:
287 defaults.update({'u_perm_%s' % p.user.user_id:
285 defaults.update({'u_perm_%s' % p.user.user_id:
288 p.permission.permission_name})
286 p.permission.permission_name})
289
287
290 for p in c.user_group.user_group_user_group_to_perm:
288 for p in c.user_group.user_group_user_group_to_perm:
291 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
289 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
292 p.permission.permission_name})
290 p.permission.permission_name})
293
291
294 data = render(
292 data = render(
295 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
293 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
296 self._get_template_context(c), self.request)
294 self._get_template_context(c), self.request)
297 html = formencode.htmlfill.render(
295 html = formencode.htmlfill.render(
298 data,
296 data,
299 defaults=defaults,
297 defaults=defaults,
300 encoding="UTF-8",
298 encoding="UTF-8",
301 force_defaults=False
299 force_defaults=False
302 )
300 )
303 return Response(html)
301 return Response(html)
304
302
305 @LoginRequired()
303 @LoginRequired()
306 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
307 @CSRFRequired()
305 @CSRFRequired()
308 def user_group_update_perms(self):
306 def user_group_update_perms(self):
309 """
307 """
310 grant permission for given user group
308 grant permission for given user group
311 """
309 """
312 _ = self.request.translate
310 _ = self.request.translate
313
311
314 user_group = self.db_user_group
312 user_group = self.db_user_group
315 user_group_id = user_group.users_group_id
313 user_group_id = user_group.users_group_id
316 c = self.load_default_context()
314 c = self.load_default_context()
317 c.user_group = user_group
315 c.user_group = user_group
318 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
316 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
319
317
320 if not self._rhodecode_user.is_admin:
318 if not self._rhodecode_user.is_admin:
321 if self._revoke_perms_on_yourself(form):
319 if self._revoke_perms_on_yourself(form):
322 msg = _('Cannot change permission for yourself as admin')
320 msg = _('Cannot change permission for yourself as admin')
323 h.flash(msg, category='warning')
321 h.flash(msg, category='warning')
324 raise HTTPFound(
322 raise HTTPFound(
325 h.route_path('edit_user_group_perms',
323 h.route_path('edit_user_group_perms',
326 user_group_id=user_group_id))
324 user_group_id=user_group_id))
327
325
328 try:
326 try:
329 changes = UserGroupModel().update_permissions(
327 changes = UserGroupModel().update_permissions(
330 user_group,
328 user_group,
331 form['perm_additions'], form['perm_updates'],
329 form['perm_additions'], form['perm_updates'],
332 form['perm_deletions'])
330 form['perm_deletions'])
333
331
334 except RepoGroupAssignmentError:
332 except RepoGroupAssignmentError:
335 h.flash(_('Target group cannot be the same'), category='error')
333 h.flash(_('Target group cannot be the same'), category='error')
336 raise HTTPFound(
334 raise HTTPFound(
337 h.route_path('edit_user_group_perms',
335 h.route_path('edit_user_group_perms',
338 user_group_id=user_group_id))
336 user_group_id=user_group_id))
339
337
340 action_data = {
338 action_data = {
341 'added': changes['added'],
339 'added': changes['added'],
342 'updated': changes['updated'],
340 'updated': changes['updated'],
343 'deleted': changes['deleted'],
341 'deleted': changes['deleted'],
344 }
342 }
345 audit_logger.store_web(
343 audit_logger.store_web(
346 'user_group.edit.permissions', action_data=action_data,
344 'user_group.edit.permissions', action_data=action_data,
347 user=self._rhodecode_user)
345 user=self._rhodecode_user)
348
346
349 Session().commit()
347 Session().commit()
350 h.flash(_('User Group permissions updated'), category='success')
348 h.flash(_('User Group permissions updated'), category='success')
351
349
352 affected_user_ids = []
350 affected_user_ids = []
353 for change in changes['added'] + changes['updated'] + changes['deleted']:
351 for change in changes['added'] + changes['updated'] + changes['deleted']:
354 if change['type'] == 'user':
352 if change['type'] == 'user':
355 affected_user_ids.append(change['id'])
353 affected_user_ids.append(change['id'])
356 if change['type'] == 'user_group':
354 if change['type'] == 'user_group':
357 user_group = UserGroup.get(safe_int(change['id']))
355 user_group = UserGroup.get(safe_int(change['id']))
358 if user_group:
356 if user_group:
359 group_members_ids = [x.user_id for x in user_group.members]
357 group_members_ids = [x.user_id for x in user_group.members]
360 affected_user_ids.extend(group_members_ids)
358 affected_user_ids.extend(group_members_ids)
361
359
362 PermissionModel().trigger_permission_flush(affected_user_ids)
360 PermissionModel().trigger_permission_flush(affected_user_ids)
363
361
364 raise HTTPFound(
362 raise HTTPFound(
365 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
363 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
366
364
367 @LoginRequired()
365 @LoginRequired()
368 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
366 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
369 def user_group_global_perms_edit(self):
367 def user_group_global_perms_edit(self):
370 user_group = self.db_user_group
368 user_group = self.db_user_group
371 c = self.load_default_context()
369 c = self.load_default_context()
372 c.user_group = user_group
370 c.user_group = user_group
373 c.active = 'global_perms'
371 c.active = 'global_perms'
374
372
375 c.default_user = User.get_default_user()
373 c.default_user = User.get_default_user()
376 defaults = c.user_group.get_dict()
374 defaults = c.user_group.get_dict()
377 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
375 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
378 defaults.update(c.user_group.get_default_perms())
376 defaults.update(c.user_group.get_default_perms())
379
377
380 data = render(
378 data = render(
381 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
379 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
382 self._get_template_context(c), self.request)
380 self._get_template_context(c), self.request)
383 html = formencode.htmlfill.render(
381 html = formencode.htmlfill.render(
384 data,
382 data,
385 defaults=defaults,
383 defaults=defaults,
386 encoding="UTF-8",
384 encoding="UTF-8",
387 force_defaults=False
385 force_defaults=False
388 )
386 )
389 return Response(html)
387 return Response(html)
390
388
391 @LoginRequired()
389 @LoginRequired()
392 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
390 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
393 @CSRFRequired()
391 @CSRFRequired()
394 def user_group_global_perms_update(self):
392 def user_group_global_perms_update(self):
395 _ = self.request.translate
393 _ = self.request.translate
396 user_group = self.db_user_group
394 user_group = self.db_user_group
397 user_group_id = self.db_user_group.users_group_id
395 user_group_id = self.db_user_group.users_group_id
398
396
399 c = self.load_default_context()
397 c = self.load_default_context()
400 c.user_group = user_group
398 c.user_group = user_group
401 c.active = 'global_perms'
399 c.active = 'global_perms'
402
400
403 try:
401 try:
404 # first stage that verifies the checkbox
402 # first stage that verifies the checkbox
405 _form = UserIndividualPermissionsForm(self.request.translate)
403 _form = UserIndividualPermissionsForm(self.request.translate)
406 form_result = _form.to_python(dict(self.request.POST))
404 form_result = _form.to_python(dict(self.request.POST))
407 inherit_perms = form_result['inherit_default_permissions']
405 inherit_perms = form_result['inherit_default_permissions']
408 user_group.inherit_default_permissions = inherit_perms
406 user_group.inherit_default_permissions = inherit_perms
409 Session().add(user_group)
407 Session().add(user_group)
410
408
411 if not inherit_perms:
409 if not inherit_perms:
412 # only update the individual ones if we un check the flag
410 # only update the individual ones if we un check the flag
413 _form = UserPermissionsForm(
411 _form = UserPermissionsForm(
414 self.request.translate,
412 self.request.translate,
415 [x[0] for x in c.repo_create_choices],
413 [x[0] for x in c.repo_create_choices],
416 [x[0] for x in c.repo_create_on_write_choices],
414 [x[0] for x in c.repo_create_on_write_choices],
417 [x[0] for x in c.repo_group_create_choices],
415 [x[0] for x in c.repo_group_create_choices],
418 [x[0] for x in c.user_group_create_choices],
416 [x[0] for x in c.user_group_create_choices],
419 [x[0] for x in c.fork_choices],
417 [x[0] for x in c.fork_choices],
420 [x[0] for x in c.inherit_default_permission_choices])()
418 [x[0] for x in c.inherit_default_permission_choices])()
421
419
422 form_result = _form.to_python(dict(self.request.POST))
420 form_result = _form.to_python(dict(self.request.POST))
423 form_result.update(
421 form_result.update(
424 {'perm_user_group_id': user_group.users_group_id})
422 {'perm_user_group_id': user_group.users_group_id})
425
423
426 PermissionModel().update_user_group_permissions(form_result)
424 PermissionModel().update_user_group_permissions(form_result)
427
425
428 Session().commit()
426 Session().commit()
429 h.flash(_('User Group global permissions updated successfully'),
427 h.flash(_('User Group global permissions updated successfully'),
430 category='success')
428 category='success')
431
429
432 except formencode.Invalid as errors:
430 except formencode.Invalid as errors:
433 defaults = errors.value
431 defaults = errors.value
434
432
435 data = render(
433 data = render(
436 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
434 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
437 self._get_template_context(c), self.request)
435 self._get_template_context(c), self.request)
438 html = formencode.htmlfill.render(
436 html = formencode.htmlfill.render(
439 data,
437 data,
440 defaults=defaults,
438 defaults=defaults,
441 errors=errors.error_dict or {},
439 errors=errors.error_dict or {},
442 prefix_error=False,
440 prefix_error=False,
443 encoding="UTF-8",
441 encoding="UTF-8",
444 force_defaults=False
442 force_defaults=False
445 )
443 )
446 return Response(html)
444 return Response(html)
447 except Exception:
445 except Exception:
448 log.exception("Exception during permissions saving")
446 log.exception("Exception during permissions saving")
449 h.flash(_('An error occurred during permissions saving'),
447 h.flash(_('An error occurred during permissions saving'),
450 category='error')
448 category='error')
451
449
452 raise HTTPFound(
450 raise HTTPFound(
453 h.route_path('edit_user_group_global_perms',
451 h.route_path('edit_user_group_global_perms',
454 user_group_id=user_group_id))
452 user_group_id=user_group_id))
455
453
456 @LoginRequired()
454 @LoginRequired()
457 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
455 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
458 def user_group_edit_advanced(self):
456 def user_group_edit_advanced(self):
459 user_group = self.db_user_group
457 user_group = self.db_user_group
460
458
461 c = self.load_default_context()
459 c = self.load_default_context()
462 c.user_group = user_group
460 c.user_group = user_group
463 c.active = 'advanced'
461 c.active = 'advanced'
464 c.group_members_obj = sorted(
462 c.group_members_obj = sorted(
465 (x.user for x in c.user_group.members),
463 (x.user for x in c.user_group.members),
466 key=lambda u: u.username.lower())
464 key=lambda u: u.username.lower())
467
465
468 c.group_to_repos = sorted(
466 c.group_to_repos = sorted(
469 (x.repository for x in c.user_group.users_group_repo_to_perm),
467 (x.repository for x in c.user_group.users_group_repo_to_perm),
470 key=lambda u: u.repo_name.lower())
468 key=lambda u: u.repo_name.lower())
471
469
472 c.group_to_repo_groups = sorted(
470 c.group_to_repo_groups = sorted(
473 (x.group for x in c.user_group.users_group_repo_group_to_perm),
471 (x.group for x in c.user_group.users_group_repo_group_to_perm),
474 key=lambda u: u.group_name.lower())
472 key=lambda u: u.group_name.lower())
475
473
476 c.group_to_review_rules = sorted(
474 c.group_to_review_rules = sorted(
477 (x.users_group for x in c.user_group.user_group_review_rules),
475 (x.users_group for x in c.user_group.user_group_review_rules),
478 key=lambda u: u.users_group_name.lower())
476 key=lambda u: u.users_group_name.lower())
479
477
480 return self._get_template_context(c)
478 return self._get_template_context(c)
481
479
482 @LoginRequired()
480 @LoginRequired()
483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
481 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 @CSRFRequired()
482 @CSRFRequired()
485 def user_group_edit_advanced_set_synchronization(self):
483 def user_group_edit_advanced_set_synchronization(self):
486 _ = self.request.translate
484 _ = self.request.translate
487 user_group = self.db_user_group
485 user_group = self.db_user_group
488 user_group_id = user_group.users_group_id
486 user_group_id = user_group.users_group_id
489
487
490 existing = user_group.group_data.get('extern_type')
488 existing = user_group.group_data.get('extern_type')
491
489
492 if existing:
490 if existing:
493 new_state = user_group.group_data
491 new_state = user_group.group_data
494 new_state['extern_type'] = None
492 new_state['extern_type'] = None
495 else:
493 else:
496 new_state = user_group.group_data
494 new_state = user_group.group_data
497 new_state['extern_type'] = 'manual'
495 new_state['extern_type'] = 'manual'
498 new_state['extern_type_set_by'] = self._rhodecode_user.username
496 new_state['extern_type_set_by'] = self._rhodecode_user.username
499
497
500 try:
498 try:
501 user_group.group_data = new_state
499 user_group.group_data = new_state
502 Session().add(user_group)
500 Session().add(user_group)
503 Session().commit()
501 Session().commit()
504
502
505 h.flash(_('User Group synchronization updated successfully'),
503 h.flash(_('User Group synchronization updated successfully'),
506 category='success')
504 category='success')
507 except Exception:
505 except Exception:
508 log.exception("Exception during sync settings saving")
506 log.exception("Exception during sync settings saving")
509 h.flash(_('An error occurred during synchronization update'),
507 h.flash(_('An error occurred during synchronization update'),
510 category='error')
508 category='error')
511
509
512 raise HTTPFound(
510 raise HTTPFound(
513 h.route_path('edit_user_group_advanced',
511 h.route_path('edit_user_group_advanced',
514 user_group_id=user_group_id))
512 user_group_id=user_group_id))
@@ -1,32 +1,30 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 def includeme(config):
20 def includeme(config):
23 from rhodecode.apps.user_group_profile.views import UserGroupProfileView
21 from rhodecode.apps.user_group_profile.views import UserGroupProfileView
24
22
25 config.add_route(
23 config.add_route(
26 name='user_group_profile',
24 name='user_group_profile',
27 pattern='/_profile_user_group/{user_group_name}')
25 pattern='/_profile_user_group/{user_group_name}')
28 config.add_view(
26 config.add_view(
29 UserGroupProfileView,
27 UserGroupProfileView,
30 attr='user_group_profile',
28 attr='user_group_profile',
31 route_name='user_group_profile', request_method='GET',
29 route_name='user_group_profile', request_method='GET',
32 renderer='rhodecode:templates/user_group/user_group.mako')
30 renderer='rhodecode:templates/user_group/user_group.mako')
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,75 +1,74 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 from rhodecode.model.user_group import UserGroupModel
18 from rhodecode.model.user_group import UserGroupModel
20 from rhodecode.tests import (
19 from rhodecode.tests import (
21 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
20 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
22 from rhodecode.tests.fixture import Fixture
21 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.utils import AssertResponse
22 from rhodecode.tests.utils import AssertResponse
24
23
25 fixture = Fixture()
24 fixture = Fixture()
26
25
27
26
28 def route_path(name, **kwargs):
27 def route_path(name, **kwargs):
29 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
28 return '/_profile_user_group/{user_group_name}'.format(**kwargs)
30
29
31
30
32 class TestUsersController(TestController):
31 class TestUsersController(TestController):
33
32
34 def test_user_group_profile(self, user_util):
33 def test_user_group_profile(self, user_util):
35 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
34 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
36 user, usergroup = user_util.create_user_with_group()
35 user, usergroup = user_util.create_user_with_group()
37
36
38 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
37 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
39 response.mustcontain(usergroup.users_group_name)
38 response.mustcontain(usergroup.users_group_name)
40 response.mustcontain(user.username)
39 response.mustcontain(user.username)
41
40
42 def test_user_can_check_own_group(self, user_util):
41 def test_user_can_check_own_group(self, user_util):
43 user = user_util.create_user(
42 user = user_util.create_user(
44 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
43 TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS, email='testme@rhodecode.org')
45 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
44 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
46 usergroup = user_util.create_user_group(owner=user)
45 usergroup = user_util.create_user_group(owner=user)
47 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
46 response = self.app.get(route_path('profile_user_group', user_group_name=usergroup.users_group_name))
48 response.mustcontain(usergroup.users_group_name)
47 response.mustcontain(usergroup.users_group_name)
49 response.mustcontain(user.username)
48 response.mustcontain(user.username)
50
49
51 def test_user_can_not_check_other_group(self, user_util):
50 def test_user_can_not_check_other_group(self, user_util):
52 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
51 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
53 user_group = user_util.create_user_group()
52 user_group = user_util.create_user_group()
54 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
53 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.none')
55 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
54 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=404)
56 assert response.status_code == 404
55 assert response.status_code == 404
57
56
58 def test_another_user_can_check_if_he_is_in_group(self, user_util):
57 def test_another_user_can_check_if_he_is_in_group(self, user_util):
59 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
58 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
60 user = user_util.create_user(
59 user = user_util.create_user(
61 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
60 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
62 user_group = user_util.create_user_group()
61 user_group = user_util.create_user_group()
63 UserGroupModel().add_user_to_group(user_group, user)
62 UserGroupModel().add_user_to_group(user_group, user)
64 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
63 UserGroupModel().grant_user_permission(user_group, self._get_logged_user(), 'usergroup.read')
65 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
64 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name))
66 response.mustcontain(user_group.users_group_name)
65 response.mustcontain(user_group.users_group_name)
67 response.mustcontain(user.username)
66 response.mustcontain(user.username)
68
67
69 def test_with_anonymous_user(self, user_util):
68 def test_with_anonymous_user(self, user_util):
70 user = user_util.create_user(
69 user = user_util.create_user(
71 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
70 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
72 user_group = user_util.create_user_group()
71 user_group = user_util.create_user_group()
73 UserGroupModel().add_user_to_group(user_group, user)
72 UserGroupModel().add_user_to_group(user_group, user)
74 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
73 response = self.app.get(route_path('profile_user_group', user_group_name=user_group.users_group_name), status=302)
75 assert response.status_code == 302 No newline at end of file
74 assert response.status_code == 302
@@ -1,50 +1,48 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPNotFound
21 from pyramid.httpexceptions import HTTPNotFound
24
22
25
23
26 from rhodecode.apps._base import BaseAppView
24 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.auth import HasUserGroupPermissionAnyDecorator, LoginRequired, NotAnonymous
25 from rhodecode.lib.auth import HasUserGroupPermissionAnyDecorator, LoginRequired, NotAnonymous
28 from rhodecode.model.db import UserGroup, User
26 from rhodecode.model.db import UserGroup, User
29
27
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class UserGroupProfileView(BaseAppView):
32 class UserGroupProfileView(BaseAppView):
35
33
36 @LoginRequired()
34 @LoginRequired()
37 @NotAnonymous()
35 @NotAnonymous()
38 @HasUserGroupPermissionAnyDecorator('usergroup.read', 'usergroup.write', 'usergroup.admin',)
36 @HasUserGroupPermissionAnyDecorator('usergroup.read', 'usergroup.write', 'usergroup.admin',)
39 def user_group_profile(self):
37 def user_group_profile(self):
40 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
41 c.active = 'profile'
39 c.active = 'profile'
42 self.db_user_group_name = self.request.matchdict.get('user_group_name')
40 self.db_user_group_name = self.request.matchdict.get('user_group_name')
43 c.user_group = UserGroup().get_by_group_name(self.db_user_group_name)
41 c.user_group = UserGroup().get_by_group_name(self.db_user_group_name)
44 if not c.user_group:
42 if not c.user_group:
45 raise HTTPNotFound()
43 raise HTTPNotFound()
46 group_members_obj = sorted((x.user for x in c.user_group.members),
44 group_members_obj = sorted((x.user for x in c.user_group.members),
47 key=lambda u: u.username.lower())
45 key=lambda u: u.username.lower())
48 c.group_members = group_members_obj
46 c.group_members = group_members_obj
49 c.anonymous = self._rhodecode_user.username == User.DEFAULT_USER
47 c.anonymous = self._rhodecode_user.username == User.DEFAULT_USER
50 return self._get_template_context(c)
48 return self._get_template_context(c)
@@ -1,32 +1,30 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21
19
22 def includeme(config):
20 def includeme(config):
23 from rhodecode.apps.user_profile.views import UserProfileView
21 from rhodecode.apps.user_profile.views import UserProfileView
24
22
25 config.add_route(
23 config.add_route(
26 name='user_profile',
24 name='user_profile',
27 pattern='/_profiles/{username}')
25 pattern='/_profiles/{username}')
28 config.add_view(
26 config.add_view(
29 UserProfileView,
27 UserProfileView,
30 attr='user_profile',
28 attr='user_profile',
31 route_name='user_profile', request_method='GET',
29 route_name='user_profile', request_method='GET',
32 renderer='rhodecode:templates/users/user.mako')
30 renderer='rhodecode:templates/users/user.mako')
@@ -1,19 +1,17 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,74 +1,73 b''
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
2 #
4 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
7 #
6 #
8 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
10 # GNU General Public License for more details.
12 #
11 #
13 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
14 #
16 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
18
20 import pytest
19 import pytest
21
20
22 from rhodecode.model.db import User
21 from rhodecode.model.db import User
23 from rhodecode.tests import (
22 from rhodecode.tests import (
24 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
23 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
25 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
24 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
26 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.utils import AssertResponse
26 from rhodecode.tests.utils import AssertResponse
28
27
29 fixture = Fixture()
28 fixture = Fixture()
30
29
31
30
32 def route_path(name, **kwargs):
31 def route_path(name, **kwargs):
33 return '/_profiles/{username}'.format(**kwargs)
32 return '/_profiles/{username}'.format(**kwargs)
34
33
35
34
36 class TestUsersController(TestController):
35 class TestUsersController(TestController):
37
36
38 def test_user_profile(self, user_util):
37 def test_user_profile(self, user_util):
39 edit_link_css = '.user-profile .panel-edit'
38 edit_link_css = '.user-profile .panel-edit'
40 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
41 user = user_util.create_user(
40 user = user_util.create_user(
42 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
41 'test-my-user', password='qweqwe', email='testme@rhodecode.org')
43 username = user.username
42 username = user.username
44
43
45 response = self.app.get(route_path('user_profile', username=username))
44 response = self.app.get(route_path('user_profile', username=username))
46 response.mustcontain('testme')
45 response.mustcontain('testme')
47 response.mustcontain('testme@rhodecode.org')
46 response.mustcontain('testme@rhodecode.org')
48 assert_response = response.assert_response()
47 assert_response = response.assert_response()
49 assert_response.no_element_exists(edit_link_css)
48 assert_response.no_element_exists(edit_link_css)
50
49
51 # edit should be available to superadmin users
50 # edit should be available to superadmin users
52 self.logout_user()
51 self.logout_user()
53 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
52 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
54 response = self.app.get(route_path('user_profile', username=username))
53 response = self.app.get(route_path('user_profile', username=username))
55 assert_response = response.assert_response()
54 assert_response = response.assert_response()
56 assert_response.element_contains(edit_link_css, 'Edit')
55 assert_response.element_contains(edit_link_css, 'Edit')
57
56
58 def test_user_profile_not_available(self, user_util):
57 def test_user_profile_not_available(self, user_util):
59 user = user_util.create_user()
58 user = user_util.create_user()
60 username = user.username
59 username = user.username
61
60
62 # not logged in, redirect
61 # not logged in, redirect
63 self.app.get(route_path('user_profile', username=username), status=302)
62 self.app.get(route_path('user_profile', username=username), status=302)
64
63
65 self.log_user()
64 self.log_user()
66 # after log-in show
65 # after log-in show
67 self.app.get(route_path('user_profile', username=username), status=200)
66 self.app.get(route_path('user_profile', username=username), status=200)
68
67
69 # default user, not allowed to show it
68 # default user, not allowed to show it
70 self.app.get(
69 self.app.get(
71 route_path('user_profile', username=User.DEFAULT_USER), status=404)
70 route_path('user_profile', username=User.DEFAULT_USER), status=404)
72
71
73 # actual 404
72 # actual 404
74 self.app.get(route_path('user_profile', username='unknown'), status=404)
73 self.app.get(route_path('user_profile', username='unknown'), status=404)
@@ -1,49 +1,47 b''
1
2
3 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 #
2 #
5 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
8 #
6 #
9 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
10 # GNU General Public License for more details.
13 #
11 #
14 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
14 #
17 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
18
21 import logging
19 import logging
22
20
23 from pyramid.httpexceptions import HTTPNotFound
21 from pyramid.httpexceptions import HTTPNotFound
24
22
25 from rhodecode.apps._base import BaseAppView
23 from rhodecode.apps._base import BaseAppView
26 from rhodecode.lib.auth import LoginRequired, NotAnonymous
24 from rhodecode.lib.auth import LoginRequired, NotAnonymous
27
25
28 from rhodecode.model.db import User
26 from rhodecode.model.db import User
29 from rhodecode.model.user import UserModel
27 from rhodecode.model.user import UserModel
30
28
31 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
32
30
33
31
34 class UserProfileView(BaseAppView):
32 class UserProfileView(BaseAppView):
35
33
36 @LoginRequired()
34 @LoginRequired()
37 @NotAnonymous()
35 @NotAnonymous()
38 def user_profile(self):
36 def user_profile(self):
39 # register local template context
37 # register local template context
40 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
41 c.active = 'user_profile'
39 c.active = 'user_profile'
42
40
43 username = self.request.matchdict.get('username')
41 username = self.request.matchdict.get('username')
44
42
45 c.user = UserModel().get_by_username(username)
43 c.user = UserModel().get_by_username(username)
46 if not c.user or c.user.username == User.DEFAULT_USER:
44 if not c.user or c.user.username == User.DEFAULT_USER:
47 raise HTTPNotFound()
45 raise HTTPNotFound()
48
46
49 return self._get_template_context(c)
47 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now