##// END OF EJS Templates
apps: various fixes and improvements for python3
super-admin -
r5072:3f386a04 default
parent child Browse files
Show More
@@ -1,147 +1,154 b''
1 1
2 import dataclasses
2 3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 4 #
4 5 # This program is free software: you can redistribute it and/or modify
5 6 # it under the terms of the GNU Affero General Public License, version 3
6 7 # (only), as published by the Free Software Foundation.
7 8 #
8 9 # This program is distributed in the hope that it will be useful,
9 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 12 # GNU General Public License for more details.
12 13 #
13 14 # You should have received a copy of the GNU Affero General Public License
14 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 16 #
16 17 # This program is dual-licensed. If you wish to learn more about the
17 18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 20
20 21
21 22 import logging
22 23 import collections
23 24
24 25 from zope.interface import implementer
25 26
26 27 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
27 28 from rhodecode.lib.utils2 import str2bool
28 29 from rhodecode.translation import _
29 30
30 31
31 32 log = logging.getLogger(__name__)
32 33
33 NavListEntry = collections.namedtuple(
34 'NavListEntry', ['key', 'name', 'url', 'active_list'])
34
35 @dataclasses.dataclass
36 class NavListEntry:
37 key: str
38 name: str
39 url: str
40 active_list: list
35 41
36 42
37 43 class NavEntry(object):
38 44 """
39 45 Represents an entry in the admin navigation.
40 46
41 47 :param key: Unique identifier used to store reference in an OrderedDict.
42 48 :param name: Display name, usually a translation string.
43 49 :param view_name: Name of the view, used generate the URL.
44 50 :param active_list: list of urls that we select active for this element
45 51 """
46 52
47 53 def __init__(self, key, name, view_name, active_list=None):
48 54 self.key = key
49 55 self.name = name
50 56 self.view_name = view_name
51 57 self._active_list = active_list or []
52 58
53 59 def generate_url(self, request):
54 60 return request.route_path(self.view_name)
55 61
56 62 def get_localized_name(self, request):
57 63 return request.translate(self.name)
58 64
59 65 @property
60 66 def active_list(self):
61 67 active_list = [self.key]
62 68 if self._active_list:
63 69 active_list = self._active_list
64 70 return active_list
65 71
66 72
67 73 @implementer(IAdminNavigationRegistry)
68 74 class NavigationRegistry(object):
69 75
70 76 _base_entries = [
71 77 NavEntry('global', _('Global'),
72 78 'admin_settings_global'),
73 79 NavEntry('vcs', _('VCS'),
74 80 'admin_settings_vcs'),
75 81 NavEntry('visual', _('Visual'),
76 82 'admin_settings_visual'),
77 83 NavEntry('mapping', _('Remap and Rescan'),
78 84 'admin_settings_mapping'),
79 85 NavEntry('issuetracker', _('Issue Tracker'),
80 86 'admin_settings_issuetracker'),
81 87 NavEntry('email', _('Email'),
82 88 'admin_settings_email'),
83 89 NavEntry('hooks', _('Hooks'),
84 90 'admin_settings_hooks'),
85 91 NavEntry('search', _('Full Text Search'),
86 92 'admin_settings_search'),
87 93 NavEntry('system', _('System Info'),
88 94 'admin_settings_system'),
89 95 NavEntry('exceptions', _('Exceptions Tracker'),
90 96 'admin_settings_exception_tracker',
91 97 active_list=['exceptions', 'exceptions_browse']),
92 98 NavEntry('process_management', _('Processes'),
93 99 'admin_settings_process_management'),
94 100 NavEntry('sessions', _('User Sessions'),
95 101 'admin_settings_sessions'),
96 102 NavEntry('open_source', _('Open Source Licenses'),
97 103 'admin_settings_open_source'),
98 104 NavEntry('automation', _('Automation'),
99 105 'admin_settings_automation')
100 106 ]
101 107
102 108 _labs_entry = NavEntry('labs', _('Labs'),
103 109 'admin_settings_labs')
104 110
105 111 def __init__(self, labs_active=False):
106 112 self._registered_entries = collections.OrderedDict()
107 113 for item in self.__class__._base_entries:
108 self._registered_entries[item.key] = item
114 self.add_entry(item)
109 115
110 116 if labs_active:
111 117 self.add_entry(self._labs_entry)
112 118
113 119 def add_entry(self, entry):
114 120 self._registered_entries[entry.key] = entry
115 121
116 122 def get_navlist(self, request):
117 123 nav_list = [
118 124 NavListEntry(i.key, i.get_localized_name(request),
119 125 i.generate_url(request), i.active_list)
120 for i in self._registered_entries.values()]
126 for i in self._registered_entries.values()
127 ]
121 128 return nav_list
122 129
123 130
124 131 def navigation_registry(request, registry=None):
125 132 """
126 133 Helper that returns the admin navigation registry.
127 134 """
128 135 pyramid_registry = registry or request.registry
129 136 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
130 137 return nav_registry
131 138
132 139
133 140 def navigation_list(request):
134 141 """
135 142 Helper that returns the admin navigation as list of NavListEntry objects.
136 143 """
137 144 return navigation_registry(request).get_navlist(request)
138 145
139 146
140 147 def includeme(config):
141 148 # Create admin navigation registry and add it to the pyramid registry.
142 149 settings = config.get_settings()
143 150 labs_active = str2bool(settings.get('labs_settings_active', False))
144 151 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
145 152 config.registry.registerUtility(navigation_registry_instance)
146 153 log.debug('Created new navigation instance, %s', navigation_registry_instance)
147 154
@@ -1,56 +1,55 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from rhodecode import events
24 24 from rhodecode.lib import rc_cache
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28 # names of namespaces used for different permission related cached
29 29 # during flush operation we need to take care of all those
30 30 cache_namespaces = [
31 31 'cache_user_auth.{}',
32 32 'cache_user_repo_acl_ids.{}',
33 33 'cache_user_user_group_acl_ids.{}',
34 34 'cache_user_repo_group_acl_ids.{}'
35 35 ]
36 36
37 37
38 38 def trigger_user_permission_flush(event):
39 39 """
40 40 Subscriber to the `UserPermissionsChange`. This triggers the
41 41 automatic flush of permission caches, so the users affected receive new permissions
42 42 Right Away
43 43 """
44 invalidate = True
44
45 45 affected_user_ids = set(event.user_ids)
46 46 for user_id in affected_user_ids:
47 47 for cache_namespace_uid_tmpl in cache_namespaces:
48 48 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
49 del_keys = rc_cache.clear_cache_namespace(
50 'cache_perms', cache_namespace_uid, invalidate=invalidate)
49 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
51 50 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
52 51 del_keys, user_id, cache_namespace_uid)
53 52
54 53
55 54 def includeme(config):
56 55 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
@@ -1,356 +1,358 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import datetime
21 21 import logging
22 22 import time
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 28
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 from sqlalchemy.orm import aliased
31 32
32 33 from rhodecode import events
33 34 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 35
35 36 from rhodecode.lib.auth import (
36 37 LoginRequired, CSRFRequired, NotAnonymous,
37 38 HasPermissionAny, HasRepoGroupPermissionAny)
38 39 from rhodecode.lib import helpers as h, audit_logger
39 40 from rhodecode.lib.str_utils import safe_int, safe_str
40 41 from rhodecode.model.forms import RepoGroupForm
41 42 from rhodecode.model.permission import PermissionModel
42 43 from rhodecode.model.repo_group import RepoGroupModel
43 44 from rhodecode.model.scm import RepoGroupList
44 45 from rhodecode.model.db import (
45 46 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51 52
52 53 def load_default_context(self):
53 54 c = self._get_local_tmpl_context()
54 55
55 56 return c
56 57
57 58 def _load_form_data(self, c):
58 59 allow_empty_group = False
59 60
60 61 if self._can_create_repo_group():
61 62 # we're global admin, we're ok and we can create TOP level groups
62 63 allow_empty_group = True
63 64
64 65 # override the choices for this form, we need to filter choices
65 66 # and display only those we have ADMIN right
66 67 groups_with_admin_rights = RepoGroupList(
67 68 RepoGroup.query().all(),
68 69 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 70 c.repo_groups = RepoGroup.groups_choices(
70 71 groups=groups_with_admin_rights,
71 72 show_empty_group=allow_empty_group)
72 73 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73 74
74 75 def _can_create_repo_group(self, parent_group_id=None):
75 76 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 77 create_repo_group = HasPermissionAny(
77 78 'hg.repogroup.create.true')('group create controller')
78 79 if is_admin or (create_repo_group and not parent_group_id):
79 80 # we're global admin, or we have global repo group create
80 81 # permission
81 82 # we're ok and we can create TOP level groups
82 83 return True
83 84 elif parent_group_id:
84 85 # we check the permission if we can write to parent group
85 86 group = RepoGroup.get(parent_group_id)
86 87 group_name = group.group_name if group else None
87 88 if HasRepoGroupPermissionAny('group.admin')(
88 89 group_name, 'check if user is an admin of group'):
89 90 # we're an admin of passed in group, we're ok.
90 91 return True
91 92 else:
92 93 return False
93 94 return False
94 95
95 96 # permission check in data loading of
96 97 # `repo_group_list_data` via RepoGroupList
97 98 @LoginRequired()
98 99 @NotAnonymous()
99 100 def repo_group_list(self):
100 101 c = self.load_default_context()
101 102 return self._get_template_context(c)
102 103
103 104 # permission check inside
104 105 @LoginRequired()
105 106 @NotAnonymous()
106 107 def repo_group_list_data(self):
107 108 self.load_default_context()
108 109 column_map = {
109 110 'name': 'group_name_hash',
110 111 'desc': 'group_description',
111 112 'last_change': 'updated_on',
112 113 'top_level_repos': 'repos_total',
113 114 'owner': 'user_username',
114 115 }
115 116 draw, start, limit = self._extract_chunk(self.request)
116 117 search_q, order_by, order_dir = self._extract_ordering(
117 118 self.request, column_map=column_map)
118 119
119 120 _render = self.request.get_partial_renderer(
120 121 'rhodecode:templates/data_table/_dt_elements.mako')
121 122 c = _render.get_call_context()
122 123
123 124 def quick_menu(repo_group_name):
124 125 return _render('quick_repo_group_menu', repo_group_name)
125 126
126 127 def repo_group_lnk(repo_group_name):
127 128 return _render('repo_group_name', repo_group_name)
128 129
129 130 def last_change(last_change):
130 131 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 132 ts = time.time()
132 133 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 134 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 135 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 136 return _render("last_change", last_change)
136 137
137 138 def desc(desc, personal):
138 139 return _render(
139 140 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140 141
141 142 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 143 return _render(
143 144 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144 145
145 146 def user_profile(username):
146 147 return _render('user_profile', username)
147 148
148 149 _perms = ['group.admin']
149 150 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150 151
151 152 repo_groups_data_total_count = RepoGroup.query()\
152 153 .filter(or_(
153 154 # generate multiple IN to fix limitation problems
154 155 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 156 )) \
156 157 .count()
157 158
158 159 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 160 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 161 .count()
161 162
162 163 repo_count = count(Repository.repo_id)
164 OwnerUser = aliased(User)
163 165 base_q = Session.query(
164 166 RepoGroup.group_name,
165 167 RepoGroup.group_name_hash,
166 168 RepoGroup.group_description,
167 169 RepoGroup.group_id,
168 170 RepoGroup.personal,
169 171 RepoGroup.updated_on,
170 User,
172 OwnerUser.username.label('owner_username'),
171 173 repo_count.label('repos_count')
172 174 ) \
173 175 .filter(or_(
174 176 # generate multiple IN to fix limitation problems
175 177 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 178 )) \
177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 .join(User, User.user_id == RepoGroup.user_id) \
179 .group_by(RepoGroup, User)
179 .outerjoin(Repository, RepoGroup.group_id == Repository.group_id) \
180 .join(OwnerUser, RepoGroup.user_id == OwnerUser.user_id)
181
182 base_q = base_q.group_by(RepoGroup, OwnerUser)
180 183
181 184 if search_q:
182 like_expression = u'%{}%'.format(safe_unicode(search_q))
185 like_expression = u'%{}%'.format(safe_str(search_q))
183 186 base_q = base_q.filter(or_(
184 187 RepoGroup.group_name.ilike(like_expression),
185 188 ))
186 189
187 190 repo_groups_data_total_filtered_count = base_q.count()
188 191 # the inactive isn't really used, but we still make it same as other data grids
189 192 # which use inactive (users,user groups)
190 193 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191 194
192 195 sort_defined = False
193 196 if order_by == 'group_name':
194 197 sort_col = func.lower(RepoGroup.group_name)
195 198 sort_defined = True
196 199 elif order_by == 'repos_total':
197 200 sort_col = repo_count
198 201 sort_defined = True
199 202 elif order_by == 'user_username':
200 sort_col = User.username
203 sort_col = OwnerUser.username
201 204 else:
202 205 sort_col = getattr(RepoGroup, order_by, None)
203 206
204 207 if sort_defined or sort_col:
205 208 if order_dir == 'asc':
206 209 sort_col = sort_col.asc()
207 210 else:
208 211 sort_col = sort_col.desc()
209 212
210 213 base_q = base_q.order_by(sort_col)
211 214 base_q = base_q.offset(start).limit(limit)
212 215
213 216 # authenticated access to user groups
214 217 auth_repo_group_list = base_q.all()
215 218
216 219 repo_groups_data = []
217 220 for repo_gr in auth_repo_group_list:
218 221 row = {
219 222 "menu": quick_menu(repo_gr.group_name),
220 223 "name": repo_group_lnk(repo_gr.group_name),
221 224
222 225 "last_change": last_change(repo_gr.updated_on),
223 226
224 227 "last_changeset": "",
225 228 "last_changeset_raw": "",
226 229
227 230 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 "owner": user_profile(repo_gr.User.username),
231 "owner": user_profile(repo_gr.owner_username),
229 232 "top_level_repos": repo_gr.repos_count,
230 233 "action": repo_group_actions(
231 234 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232
233 235 }
234 236
235 237 repo_groups_data.append(row)
236 238
237 239 data = ({
238 240 'draw': draw,
239 241 'data': repo_groups_data,
240 242 'recordsTotal': repo_groups_data_total_count,
241 243 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 244 'recordsFiltered': repo_groups_data_total_filtered_count,
243 245 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 246 })
245 247
246 248 return data
247 249
248 250 @LoginRequired()
249 251 @NotAnonymous()
250 252 # perm checks inside
251 253 def repo_group_new(self):
252 254 c = self.load_default_context()
253 255
254 256 # perm check for admin, create_group perm or admin of parent_group
255 257 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 258 _gr = RepoGroup.get(parent_group_id)
257 259 if not self._can_create_repo_group(parent_group_id):
258 260 raise HTTPForbidden()
259 261
260 262 self._load_form_data(c)
261 263
262 264 defaults = {} # Future proof for default of repo group
263 265
264 266 parent_group_choice = '-1'
265 267 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 268 parent_group_choice = self._rhodecode_user.personal_repo_group
267 269
268 270 if parent_group_id and _gr:
269 271 if parent_group_id in [x[0] for x in c.repo_groups]:
270 parent_group_choice = safe_unicode(parent_group_id)
272 parent_group_choice = safe_str(parent_group_id)
271 273
272 274 defaults.update({'group_parent_id': parent_group_choice})
273 275
274 276 data = render(
275 277 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 278 self._get_template_context(c), self.request)
277 279
278 280 html = formencode.htmlfill.render(
279 281 data,
280 282 defaults=defaults,
281 283 encoding="UTF-8",
282 284 force_defaults=False
283 285 )
284 286 return Response(html)
285 287
286 288 @LoginRequired()
287 289 @NotAnonymous()
288 290 @CSRFRequired()
289 291 # perm checks inside
290 292 def repo_group_create(self):
291 293 c = self.load_default_context()
292 294 _ = self.request.translate
293 295
294 296 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 297 can_create = self._can_create_repo_group(parent_group_id)
296 298
297 299 self._load_form_data(c)
298 300 # permissions for can create group based on parent_id are checked
299 301 # here in the Form
300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
302 available_groups = list(map(lambda k: safe_str(k[0]), c.repo_groups))
301 303 repo_group_form = RepoGroupForm(
302 304 self.request.translate, available_groups=available_groups,
303 305 can_create_in_root=can_create)()
304 306
305 307 repo_group_name = self.request.POST.get('group_name')
306 308 try:
307 309 owner = self._rhodecode_user
308 310 form_result = repo_group_form.to_python(dict(self.request.POST))
309 311 copy_permissions = form_result.get('group_copy_permissions')
310 312 repo_group = RepoGroupModel().create(
311 313 group_name=form_result['group_name_full'],
312 314 group_description=form_result['group_description'],
313 315 owner=owner.user_id,
314 316 copy_permissions=form_result['group_copy_permissions']
315 317 )
316 318 Session().flush()
317 319
318 320 repo_group_data = repo_group.get_api_data()
319 321 audit_logger.store_web(
320 322 'repo_group.create', action_data={'data': repo_group_data},
321 323 user=self._rhodecode_user)
322 324
323 325 Session().commit()
324 326
325 327 _new_group_name = form_result['group_name_full']
326 328
327 329 repo_group_url = h.link_to(
328 330 _new_group_name,
329 331 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 332 h.flash(h.literal(_('Created repository group %s')
331 333 % repo_group_url), category='success')
332 334
333 335 except formencode.Invalid as errors:
334 336 data = render(
335 337 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 338 self._get_template_context(c), self.request)
337 339 html = formencode.htmlfill.render(
338 340 data,
339 341 defaults=errors.value,
340 342 errors=errors.unpack_errors() or {},
341 343 prefix_error=False,
342 344 encoding="UTF-8",
343 345 force_defaults=False
344 346 )
345 347 return Response(html)
346 348 except Exception:
347 349 log.exception("Exception during creation of repository group")
348 350 h.flash(_('Error occurred during creation of repository group %s')
349 351 % repo_group_name, category='error')
350 352 raise HTTPFound(h.route_path('home'))
351 353
352 354 PermissionModel().trigger_permission_flush()
353 355
354 356 raise HTTPFound(
355 357 h.route_path('repo_group_home',
356 358 repo_group_name=form_result['group_name_full']))
@@ -1,250 +1,256 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 from sqlalchemy.orm import aliased
29 30
30 31 from rhodecode import events
31 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 33 from rhodecode.lib.celerylib.utils import get_task_id
33 34
34 35 from rhodecode.lib.auth import (
35 36 LoginRequired, CSRFRequired, NotAnonymous,
36 37 HasPermissionAny, HasRepoGroupPermissionAny)
37 38 from rhodecode.lib import helpers as h
38 39 from rhodecode.lib.utils import repo_name_slug
39 40 from rhodecode.lib.utils2 import safe_int, safe_str
40 41 from rhodecode.model.forms import RepoForm
41 42 from rhodecode.model.permission import PermissionModel
42 43 from rhodecode.model.repo import RepoModel
43 44 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 45 from rhodecode.model.settings import SettingsModel
45 46 from rhodecode.model.db import (
46 47 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47 48
48 49 log = logging.getLogger(__name__)
49 50
50 51
51 52 class AdminReposView(BaseAppView, DataGridAppView):
52 53
53 54 def load_default_context(self):
54 55 c = self._get_local_tmpl_context()
55 56 return c
56 57
57 58 def _load_form_data(self, c):
58 59 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 60 perm_set=['group.write', 'group.admin'])
60 61 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 62 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
62 63 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63 64
64 65 @LoginRequired()
65 66 @NotAnonymous()
66 67 # perms check inside
67 68 def repository_list(self):
68 69 c = self.load_default_context()
69 70 return self._get_template_context(c)
70 71
71 72 @LoginRequired()
72 73 @NotAnonymous()
73 74 # perms check inside
74 75 def repository_list_data(self):
75 76 self.load_default_context()
76 77 column_map = {
77 78 'name': 'repo_name',
78 79 'desc': 'description',
79 80 'last_change': 'updated_on',
80 81 'owner': 'user_username',
81 82 }
82 83 draw, start, limit = self._extract_chunk(self.request)
83 84 search_q, order_by, order_dir = self._extract_ordering(
84 85 self.request, column_map=column_map)
85 86
86 87 _perms = ['repository.admin']
87 88 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88 89
89 90 repos_data_total_count = Repository.query() \
90 91 .filter(or_(
91 92 # generate multiple IN to fix limitation problems
92 93 *in_filter_generator(Repository.repo_id, allowed_ids))
93 94 ) \
94 95 .count()
95 96
97 RepoFork = aliased(Repository)
98 OwnerUser = aliased(User)
96 99 base_q = Session.query(
97 100 Repository.repo_id,
98 101 Repository.repo_name,
99 102 Repository.description,
100 103 Repository.repo_type,
101 104 Repository.repo_state,
102 105 Repository.private,
103 106 Repository.archived,
104 Repository.fork,
105 107 Repository.updated_on,
106 108 Repository._changeset_cache,
107 User,
109 RepoFork.repo_name.label('fork_repo_name'),
110 OwnerUser.username.label('owner_username'),
108 111 ) \
109 112 .filter(or_(
110 113 # generate multiple IN to fix limitation problems
111 114 *in_filter_generator(Repository.repo_id, allowed_ids))
112 115 ) \
113 .join(User, User.user_id == Repository.user_id) \
114 .group_by(Repository, User)
116 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
117 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
115 118
116 119 if search_q:
117 120 like_expression = u'%{}%'.format(safe_str(search_q))
118 121 base_q = base_q.filter(or_(
119 122 Repository.repo_name.ilike(like_expression),
120 123 ))
121 124
125 #TODO: check if we need group_by here ?
126 #base_q = base_q.group_by(Repository, User)
127
122 128 repos_data_total_filtered_count = base_q.count()
123 129
124 130 sort_defined = False
125 131 if order_by == 'repo_name':
126 132 sort_col = func.lower(Repository.repo_name)
127 133 sort_defined = True
128 134 elif order_by == 'user_username':
129 sort_col = User.username
135 sort_col = OwnerUser.username
130 136 else:
131 137 sort_col = getattr(Repository, order_by, None)
132 138
133 139 if sort_defined or sort_col:
134 140 if order_dir == 'asc':
135 141 sort_col = sort_col.asc()
136 142 else:
137 143 sort_col = sort_col.desc()
138 144
139 145 base_q = base_q.order_by(sort_col)
140 146 base_q = base_q.offset(start).limit(limit)
141 147
142 148 repos_list = base_q.all()
143 149
144 150 repos_data = RepoModel().get_repos_as_dict(
145 151 repo_list=repos_list, admin=True, super_user_actions=True)
146 152
147 153 data = ({
148 154 'draw': draw,
149 155 'data': repos_data,
150 156 'recordsTotal': repos_data_total_count,
151 157 'recordsFiltered': repos_data_total_filtered_count,
152 158 })
153 159 return data
154 160
155 161 @LoginRequired()
156 162 @NotAnonymous()
157 163 # perms check inside
158 164 def repository_new(self):
159 165 c = self.load_default_context()
160 166
161 167 new_repo = self.request.GET.get('repo', '')
162 168 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 169 _gr = RepoGroup.get(parent_group_id)
164 170
165 171 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 172 # you're not super admin nor have global create permissions,
167 173 # but maybe you have at least write permission to a parent group ?
168 174
169 175 gr_name = _gr.group_name if _gr else None
170 176 # create repositories with write permission on group is set to true
171 177 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 178 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 179 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 180 if not (group_admin or (group_write and create_on_write)):
175 181 raise HTTPForbidden()
176 182
177 183 self._load_form_data(c)
178 184 c.new_repo = repo_name_slug(new_repo)
179 185
180 186 # apply the defaults from defaults page
181 187 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 188 # set checkbox to autochecked
183 189 defaults['repo_copy_permissions'] = True
184 190
185 191 parent_group_choice = '-1'
186 192 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 193 parent_group_choice = self._rhodecode_user.personal_repo_group
188 194
189 195 if parent_group_id and _gr:
190 196 if parent_group_id in [x[0] for x in c.repo_groups]:
191 parent_group_choice = safe_unicode(parent_group_id)
197 parent_group_choice = safe_str(parent_group_id)
192 198
193 199 defaults.update({'repo_group': parent_group_choice})
194 200
195 201 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 202 self._get_template_context(c), self.request)
197 203 html = formencode.htmlfill.render(
198 204 data,
199 205 defaults=defaults,
200 206 encoding="UTF-8",
201 207 force_defaults=False
202 208 )
203 209 return Response(html)
204 210
205 211 @LoginRequired()
206 212 @NotAnonymous()
207 213 @CSRFRequired()
208 214 # perms check inside
209 215 def repository_create(self):
210 216 c = self.load_default_context()
211 217
212 218 form_result = {}
213 219 self._load_form_data(c)
214 220
215 221 try:
216 222 # CanWriteToGroup validators checks permissions of this POST
217 223 form = RepoForm(
218 224 self.request.translate, repo_groups=c.repo_groups_choices)()
219 225 form_result = form.to_python(dict(self.request.POST))
220 226 copy_permissions = form_result.get('repo_copy_permissions')
221 227 # create is done sometimes async on celery, db transaction
222 228 # management is handled there.
223 229 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 230 task_id = get_task_id(task)
225 231 except formencode.Invalid as errors:
226 232 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 233 self._get_template_context(c), self.request)
228 234 html = formencode.htmlfill.render(
229 235 data,
230 236 defaults=errors.value,
231 237 errors=errors.unpack_errors() or {},
232 238 prefix_error=False,
233 239 encoding="UTF-8",
234 240 force_defaults=False
235 241 )
236 242 return Response(html)
237 243
238 244 except Exception as e:
239 245 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 246 h.flash(msg, category='error')
241 247 raise HTTPFound(h.route_path('home'))
242 248
243 249 repo_name = form_result.get('repo_name_full')
244 250
245 251 affected_user_ids = [self._rhodecode_user.user_id]
246 252 PermissionModel().trigger_permission_flush(affected_user_ids)
247 253
248 254 raise HTTPFound(
249 255 h.route_path('repo_creating', repo_name=repo_name,
250 256 _query=dict(task_id=task_id)))
@@ -1,1323 +1,1323 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 34 from rhodecode.authentication.plugins import auth_rhodecode
35 35 from rhodecode.events import trigger
36 36 from rhodecode.model.db import true, UserNotice
37 37
38 38 from rhodecode.lib import audit_logger, rc_cache, auth
39 39 from rhodecode.lib.exceptions import (
40 40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 42 UserOwnsArtifactsException, DefaultUserException)
43 43 from rhodecode.lib import ext_json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.helpers import SqlPage
48 48 from rhodecode.lib.utils2 import safe_int, safe_str, AttributeDict
49 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.forms import (
51 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 52 UserExtraEmailForm, UserExtraIpForm)
53 53 from rhodecode.model.permission import PermissionModel
54 54 from rhodecode.model.repo_group import RepoGroupModel
55 55 from rhodecode.model.ssh_key import SshKeyModel
56 56 from rhodecode.model.user import UserModel
57 57 from rhodecode.model.user_group import UserGroupModel
58 58 from rhodecode.model.db import (
59 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 60 UserApiKeys, UserSshKeys, RepoGroup)
61 61 from rhodecode.model.meta import Session
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class AdminUsersView(BaseAppView, DataGridAppView):
67 67
68 68 def load_default_context(self):
69 69 c = self._get_local_tmpl_context()
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @HasPermissionAllDecorator('hg.admin')
74 74 def users_list(self):
75 75 c = self.load_default_context()
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def users_list_data(self):
81 81 self.load_default_context()
82 82 column_map = {
83 83 'first_name': 'name',
84 84 'last_name': 'lastname',
85 85 }
86 86 draw, start, limit = self._extract_chunk(self.request)
87 87 search_q, order_by, order_dir = self._extract_ordering(
88 88 self.request, column_map=column_map)
89 89 _render = self.request.get_partial_renderer(
90 90 'rhodecode:templates/data_table/_dt_elements.mako')
91 91
92 92 def user_actions(user_id, username):
93 93 return _render("user_actions", user_id, username)
94 94
95 95 users_data_total_count = User.query()\
96 96 .filter(User.username != User.DEFAULT_USER) \
97 97 .count()
98 98
99 99 users_data_total_inactive_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .filter(User.active != true())\
102 102 .count()
103 103
104 104 # json generate
105 105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 106 base_inactive_q = base_q.filter(User.active != true())
107 107
108 108 if search_q:
109 109 like_expression = '%{}%'.format(safe_str(search_q))
110 110 base_q = base_q.filter(or_(
111 111 User.username.ilike(like_expression),
112 112 User._email.ilike(like_expression),
113 113 User.name.ilike(like_expression),
114 114 User.lastname.ilike(like_expression),
115 115 ))
116 116 base_inactive_q = base_q.filter(User.active != true())
117 117
118 118 users_data_total_filtered_count = base_q.count()
119 119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120 120
121 121 sort_col = getattr(User, order_by, None)
122 122 if sort_col:
123 123 if order_dir == 'asc':
124 124 # handle null values properly to order by NULL last
125 125 if order_by in ['last_activity']:
126 126 sort_col = coalesce(sort_col, datetime.date.max)
127 127 sort_col = sort_col.asc()
128 128 else:
129 129 # handle null values properly to order by NULL last
130 130 if order_by in ['last_activity']:
131 131 sort_col = coalesce(sort_col, datetime.date.min)
132 132 sort_col = sort_col.desc()
133 133
134 134 base_q = base_q.order_by(sort_col)
135 135 base_q = base_q.offset(start).limit(limit)
136 136
137 137 users_list = base_q.all()
138 138
139 139 users_data = []
140 140 for user in users_list:
141 141 users_data.append({
142 142 "username": h.gravatar_with_user(self.request, user.username),
143 143 "email": user.email,
144 144 "first_name": user.first_name,
145 145 "last_name": user.last_name,
146 146 "last_login": h.format_date(user.last_login),
147 147 "last_activity": h.format_date(user.last_activity),
148 148 "active": h.bool2icon(user.active),
149 149 "active_raw": user.active,
150 150 "admin": h.bool2icon(user.admin),
151 151 "extern_type": user.extern_type,
152 152 "extern_name": user.extern_name,
153 153 "action": user_actions(user.user_id, user.username),
154 154 })
155 155 data = ({
156 156 'draw': draw,
157 157 'data': users_data,
158 158 'recordsTotal': users_data_total_count,
159 159 'recordsFiltered': users_data_total_filtered_count,
160 160 'recordsTotalInactive': users_data_total_inactive_count,
161 161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 162 })
163 163
164 164 return data
165 165
166 166 def _set_personal_repo_group_template_vars(self, c_obj):
167 167 DummyUser = AttributeDict({
168 168 'username': '${username}',
169 169 'user_id': '${user_id}',
170 170 })
171 171 c_obj.default_create_repo_group = RepoGroupModel() \
172 172 .get_default_create_personal_repo_group()
173 173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 174 .get_personal_group_name(DummyUser)
175 175
176 176 @LoginRequired()
177 177 @HasPermissionAllDecorator('hg.admin')
178 178 def users_new(self):
179 179 _ = self.request.translate
180 180 c = self.load_default_context()
181 181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 182 self._set_personal_repo_group_template_vars(c)
183 183 return self._get_template_context(c)
184 184
185 185 @LoginRequired()
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @CSRFRequired()
188 188 def users_create(self):
189 189 _ = self.request.translate
190 190 c = self.load_default_context()
191 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 192 user_model = UserModel()
193 193 user_form = UserForm(self.request.translate)()
194 194 try:
195 195 form_result = user_form.to_python(dict(self.request.POST))
196 196 user = user_model.create(form_result)
197 197 Session().flush()
198 198 creation_data = user.get_api_data()
199 199 username = form_result['username']
200 200
201 201 audit_logger.store_web(
202 202 'user.create', action_data={'data': creation_data},
203 203 user=c.rhodecode_user)
204 204
205 205 user_link = h.link_to(
206 206 h.escape(username),
207 207 h.route_path('user_edit', user_id=user.user_id))
208 208 h.flash(h.literal(_('Created user %(user_link)s')
209 209 % {'user_link': user_link}), category='success')
210 210 Session().commit()
211 211 except formencode.Invalid as errors:
212 212 self._set_personal_repo_group_template_vars(c)
213 213 data = render(
214 214 'rhodecode:templates/admin/users/user_add.mako',
215 215 self._get_template_context(c), self.request)
216 216 html = formencode.htmlfill.render(
217 217 data,
218 218 defaults=errors.value,
219 219 errors=errors.unpack_errors() or {},
220 220 prefix_error=False,
221 221 encoding="UTF-8",
222 222 force_defaults=False
223 223 )
224 224 return Response(html)
225 225 except UserCreationError as e:
226 226 h.flash(safe_str(e), 'error')
227 227 except Exception:
228 228 log.exception("Exception creation of user")
229 229 h.flash(_('Error occurred during creation of user %s')
230 230 % self.request.POST.get('username'), category='error')
231 231 raise HTTPFound(h.route_path('users'))
232 232
233 233
234 234 class UsersView(UserAppView):
235 235 ALLOW_SCOPED_TOKENS = False
236 236 """
237 237 This view has alternative version inside EE, if modified please take a look
238 238 in there as well.
239 239 """
240 240
241 241 def get_auth_plugins(self):
242 242 valid_plugins = []
243 243 authn_registry = get_authn_registry(self.request.registry)
244 244 for plugin in authn_registry.get_plugins_for_authentication():
245 245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 246 valid_plugins.append(plugin)
247 247 elif plugin.name == 'rhodecode':
248 248 valid_plugins.append(plugin)
249 249
250 250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 251 # moment
252 252 extern_type = self.db_user.extern_type
253 253 if extern_type not in [x.uid for x in valid_plugins]:
254 254 try:
255 255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 256 if plugin:
257 257 valid_plugins.append(plugin)
258 258
259 259 except Exception:
260 260 log.exception(
261 261 'Could not extend user plugins with `{}`'.format(extern_type))
262 262 return valid_plugins
263 263
264 264 def load_default_context(self):
265 265 req = self.request
266 266
267 267 c = self._get_local_tmpl_context()
268 268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 269 c.allowed_languages = [
270 270 ('en', 'English (en)'),
271 271 ('de', 'German (de)'),
272 272 ('fr', 'French (fr)'),
273 273 ('it', 'Italian (it)'),
274 274 ('ja', 'Japanese (ja)'),
275 275 ('pl', 'Polish (pl)'),
276 276 ('pt', 'Portuguese (pt)'),
277 277 ('ru', 'Russian (ru)'),
278 278 ('zh', 'Chinese (zh)'),
279 279 ]
280 280
281 281 c.allowed_extern_types = [
282 282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 283 ]
284 284 perms = req.registry.settings.get('available_permissions')
285 285 if not perms:
286 286 # inject info about available permissions
287 287 auth.set_available_permissions(req.registry.settings)
288 288
289 289 c.available_permissions = req.registry.settings['available_permissions']
290 290 PermissionModel().set_global_permission_choices(
291 291 c, gettext_translator=req.translate)
292 292
293 293 return c
294 294
295 295 @LoginRequired()
296 296 @HasPermissionAllDecorator('hg.admin')
297 297 @CSRFRequired()
298 298 def user_update(self):
299 299 _ = self.request.translate
300 300 c = self.load_default_context()
301 301
302 302 user_id = self.db_user_id
303 303 c.user = self.db_user
304 304
305 305 c.active = 'profile'
306 306 c.extern_type = c.user.extern_type
307 307 c.extern_name = c.user.extern_name
308 308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 309 available_languages = [x[0] for x in c.allowed_languages]
310 310 _form = UserForm(self.request.translate, edit=True,
311 311 available_languages=available_languages,
312 312 old_data={'user_id': user_id,
313 313 'email': c.user.email})()
314 314
315 315 c.edit_mode = self.request.POST.get('edit') == '1'
316 316 form_result = {}
317 317 old_values = c.user.get_api_data()
318 318 try:
319 319 form_result = _form.to_python(dict(self.request.POST))
320 320 skip_attrs = ['extern_name']
321 321 # TODO: plugin should define if username can be updated
322 322
323 323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 324 # forbid updating username for external accounts
325 325 skip_attrs.append('username')
326 326
327 327 UserModel().update_user(
328 328 user_id, skip_attrs=skip_attrs, **form_result)
329 329
330 330 audit_logger.store_web(
331 331 'user.edit', action_data={'old_data': old_values},
332 332 user=c.rhodecode_user)
333 333
334 334 Session().commit()
335 335 h.flash(_('User updated successfully'), category='success')
336 336 except formencode.Invalid as errors:
337 337 data = render(
338 338 'rhodecode:templates/admin/users/user_edit.mako',
339 339 self._get_template_context(c), self.request)
340 340 html = formencode.htmlfill.render(
341 341 data,
342 342 defaults=errors.value,
343 343 errors=errors.unpack_errors() or {},
344 344 prefix_error=False,
345 345 encoding="UTF-8",
346 346 force_defaults=False
347 347 )
348 348 return Response(html)
349 349 except UserCreationError as e:
350 350 h.flash(safe_str(e), 'error')
351 351 except Exception:
352 352 log.exception("Exception updating user")
353 353 h.flash(_('Error occurred during update of user %s')
354 354 % form_result.get('username'), category='error')
355 355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356 356
357 357 @LoginRequired()
358 358 @HasPermissionAllDecorator('hg.admin')
359 359 @CSRFRequired()
360 360 def user_delete(self):
361 361 _ = self.request.translate
362 362 c = self.load_default_context()
363 363 c.user = self.db_user
364 364
365 _repos = c.user.repositories
366 _repo_groups = c.user.repository_groups
367 _user_groups = c.user.user_groups
368 _pull_requests = c.user.user_pull_requests
369 _artifacts = c.user.artifacts
365 _repos = len(c.user.repositories)
366 _repo_groups = len(c.user.repository_groups)
367 _user_groups = len(c.user.user_groups)
368 _pull_requests = len(c.user.user_pull_requests)
369 _artifacts = len(c.user.artifacts)
370 370
371 371 handle_repos = None
372 372 handle_repo_groups = None
373 373 handle_user_groups = None
374 374 handle_pull_requests = None
375 375 handle_artifacts = None
376 376
377 377 # calls for flash of handle based on handle case detach or delete
378 378 def set_handle_flash_repos():
379 379 handle = handle_repos
380 380 if handle == 'detach':
381 h.flash(_('Detached %s repositories') % len(_repos),
381 h.flash(_('Detached %s repositories') % _repos,
382 382 category='success')
383 383 elif handle == 'delete':
384 h.flash(_('Deleted %s repositories') % len(_repos),
384 h.flash(_('Deleted %s repositories') % _repos,
385 385 category='success')
386 386
387 387 def set_handle_flash_repo_groups():
388 388 handle = handle_repo_groups
389 389 if handle == 'detach':
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
390 h.flash(_('Detached %s repository groups') % _repo_groups,
391 391 category='success')
392 392 elif handle == 'delete':
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
393 h.flash(_('Deleted %s repository groups') % _repo_groups,
394 394 category='success')
395 395
396 396 def set_handle_flash_user_groups():
397 397 handle = handle_user_groups
398 398 if handle == 'detach':
399 h.flash(_('Detached %s user groups') % len(_user_groups),
399 h.flash(_('Detached %s user groups') % _user_groups,
400 400 category='success')
401 401 elif handle == 'delete':
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
402 h.flash(_('Deleted %s user groups') % _user_groups,
403 403 category='success')
404 404
405 405 def set_handle_flash_pull_requests():
406 406 handle = handle_pull_requests
407 407 if handle == 'detach':
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
408 h.flash(_('Detached %s pull requests') % _pull_requests,
409 409 category='success')
410 410 elif handle == 'delete':
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
411 h.flash(_('Deleted %s pull requests') % _pull_requests,
412 412 category='success')
413 413
414 414 def set_handle_flash_artifacts():
415 415 handle = handle_artifacts
416 416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
417 h.flash(_('Detached %s artifacts') % _artifacts,
418 418 category='success')
419 419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
420 h.flash(_('Deleted %s artifacts') % _artifacts,
421 421 category='success')
422 422
423 423 handle_user = User.get_first_super_admin()
424 424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 425 if handle_user_id:
426 426 # NOTE(marcink): we get new owner for objects...
427 427 handle_user = User.get_or_404(handle_user_id)
428 428
429 429 if _repos and self.request.POST.get('user_repos'):
430 430 handle_repos = self.request.POST['user_repos']
431 431
432 432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 433 handle_repo_groups = self.request.POST['user_repo_groups']
434 434
435 435 if _user_groups and self.request.POST.get('user_user_groups'):
436 436 handle_user_groups = self.request.POST['user_user_groups']
437 437
438 438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 439 handle_pull_requests = self.request.POST['user_pull_requests']
440 440
441 441 if _artifacts and self.request.POST.get('user_artifacts'):
442 442 handle_artifacts = self.request.POST['user_artifacts']
443 443
444 444 old_values = c.user.get_api_data()
445 445
446 446 try:
447 447
448 448 UserModel().delete(
449 449 c.user,
450 450 handle_repos=handle_repos,
451 451 handle_repo_groups=handle_repo_groups,
452 452 handle_user_groups=handle_user_groups,
453 453 handle_pull_requests=handle_pull_requests,
454 454 handle_artifacts=handle_artifacts,
455 455 handle_new_owner=handle_user
456 456 )
457 457
458 458 audit_logger.store_web(
459 459 'user.delete', action_data={'old_data': old_values},
460 460 user=c.rhodecode_user)
461 461
462 462 Session().commit()
463 463 set_handle_flash_repos()
464 464 set_handle_flash_repo_groups()
465 465 set_handle_flash_user_groups()
466 466 set_handle_flash_pull_requests()
467 467 set_handle_flash_artifacts()
468 468 username = h.escape(old_values['username'])
469 469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 472 UserOwnsArtifactsException, DefaultUserException) as e:
473 473
474 474 h.flash(safe_str(e), category='warning')
475 475 except Exception:
476 476 log.exception("Exception during deletion of user")
477 477 h.flash(_('An error occurred during deletion of user'),
478 478 category='error')
479 479 raise HTTPFound(h.route_path('users'))
480 480
481 481 @LoginRequired()
482 482 @HasPermissionAllDecorator('hg.admin')
483 483 def user_edit(self):
484 484 _ = self.request.translate
485 485 c = self.load_default_context()
486 486 c.user = self.db_user
487 487
488 488 c.active = 'profile'
489 489 c.extern_type = c.user.extern_type
490 490 c.extern_name = c.user.extern_name
491 491 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
492 492 c.edit_mode = self.request.GET.get('edit') == '1'
493 493
494 494 defaults = c.user.get_dict()
495 495 defaults.update({'language': c.user.user_data.get('language')})
496 496
497 497 data = render(
498 498 'rhodecode:templates/admin/users/user_edit.mako',
499 499 self._get_template_context(c), self.request)
500 500 html = formencode.htmlfill.render(
501 501 data,
502 502 defaults=defaults,
503 503 encoding="UTF-8",
504 504 force_defaults=False
505 505 )
506 506 return Response(html)
507 507
508 508 @LoginRequired()
509 509 @HasPermissionAllDecorator('hg.admin')
510 510 def user_edit_advanced(self):
511 511 _ = self.request.translate
512 512 c = self.load_default_context()
513 513
514 514 user_id = self.db_user_id
515 515 c.user = self.db_user
516 516
517 517 c.detach_user = User.get_first_super_admin()
518 518 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
519 519 if detach_user_id:
520 520 c.detach_user = User.get_or_404(detach_user_id)
521 521
522 522 c.active = 'advanced'
523 523 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
524 524 c.personal_repo_group_name = RepoGroupModel()\
525 525 .get_personal_group_name(c.user)
526 526
527 527 c.user_to_review_rules = sorted(
528 528 (x.user for x in c.user.user_review_rules),
529 529 key=lambda u: u.username.lower())
530 530
531 531 defaults = c.user.get_dict()
532 532
533 533 # Interim workaround if the user participated on any pull requests as a
534 534 # reviewer.
535 535 has_review = len(c.user.reviewer_pull_requests)
536 536 c.can_delete_user = not has_review
537 537 c.can_delete_user_message = ''
538 538 inactive_link = h.link_to(
539 539 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
540 540 if has_review == 1:
541 541 c.can_delete_user_message = h.literal(_(
542 542 'The user participates as reviewer in {} pull request and '
543 543 'cannot be deleted. \nYou can set the user to '
544 544 '"{}" instead of deleting it.').format(
545 545 has_review, inactive_link))
546 546 elif has_review:
547 547 c.can_delete_user_message = h.literal(_(
548 548 'The user participates as reviewer in {} pull requests and '
549 549 'cannot be deleted. \nYou can set the user to '
550 550 '"{}" instead of deleting it.').format(
551 551 has_review, inactive_link))
552 552
553 553 data = render(
554 554 'rhodecode:templates/admin/users/user_edit.mako',
555 555 self._get_template_context(c), self.request)
556 556 html = formencode.htmlfill.render(
557 557 data,
558 558 defaults=defaults,
559 559 encoding="UTF-8",
560 560 force_defaults=False
561 561 )
562 562 return Response(html)
563 563
564 564 @LoginRequired()
565 565 @HasPermissionAllDecorator('hg.admin')
566 566 def user_edit_global_perms(self):
567 567 _ = self.request.translate
568 568 c = self.load_default_context()
569 569 c.user = self.db_user
570 570
571 571 c.active = 'global_perms'
572 572
573 573 c.default_user = User.get_default_user()
574 574 defaults = c.user.get_dict()
575 575 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
576 576 defaults.update(c.default_user.get_default_perms())
577 577 defaults.update(c.user.get_default_perms())
578 578
579 579 data = render(
580 580 'rhodecode:templates/admin/users/user_edit.mako',
581 581 self._get_template_context(c), self.request)
582 582 html = formencode.htmlfill.render(
583 583 data,
584 584 defaults=defaults,
585 585 encoding="UTF-8",
586 586 force_defaults=False
587 587 )
588 588 return Response(html)
589 589
590 590 @LoginRequired()
591 591 @HasPermissionAllDecorator('hg.admin')
592 592 @CSRFRequired()
593 593 def user_edit_global_perms_update(self):
594 594 _ = self.request.translate
595 595 c = self.load_default_context()
596 596
597 597 user_id = self.db_user_id
598 598 c.user = self.db_user
599 599
600 600 c.active = 'global_perms'
601 601 try:
602 602 # first stage that verifies the checkbox
603 603 _form = UserIndividualPermissionsForm(self.request.translate)
604 604 form_result = _form.to_python(dict(self.request.POST))
605 605 inherit_perms = form_result['inherit_default_permissions']
606 606 c.user.inherit_default_permissions = inherit_perms
607 607 Session().add(c.user)
608 608
609 609 if not inherit_perms:
610 610 # only update the individual ones if we un check the flag
611 611 _form = UserPermissionsForm(
612 612 self.request.translate,
613 613 [x[0] for x in c.repo_create_choices],
614 614 [x[0] for x in c.repo_create_on_write_choices],
615 615 [x[0] for x in c.repo_group_create_choices],
616 616 [x[0] for x in c.user_group_create_choices],
617 617 [x[0] for x in c.fork_choices],
618 618 [x[0] for x in c.inherit_default_permission_choices])()
619 619
620 620 form_result = _form.to_python(dict(self.request.POST))
621 621 form_result.update({'perm_user_id': c.user.user_id})
622 622
623 623 PermissionModel().update_user_permissions(form_result)
624 624
625 625 # TODO(marcink): implement global permissions
626 626 # audit_log.store_web('user.edit.permissions')
627 627
628 628 Session().commit()
629 629
630 630 h.flash(_('User global permissions updated successfully'),
631 631 category='success')
632 632
633 633 except formencode.Invalid as errors:
634 634 data = render(
635 635 'rhodecode:templates/admin/users/user_edit.mako',
636 636 self._get_template_context(c), self.request)
637 637 html = formencode.htmlfill.render(
638 638 data,
639 639 defaults=errors.value,
640 640 errors=errors.unpack_errors() or {},
641 641 prefix_error=False,
642 642 encoding="UTF-8",
643 643 force_defaults=False
644 644 )
645 645 return Response(html)
646 646 except Exception:
647 647 log.exception("Exception during permissions saving")
648 648 h.flash(_('An error occurred during permissions saving'),
649 649 category='error')
650 650
651 651 affected_user_ids = [user_id]
652 652 PermissionModel().trigger_permission_flush(affected_user_ids)
653 653 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
654 654
655 655 @LoginRequired()
656 656 @HasPermissionAllDecorator('hg.admin')
657 657 @CSRFRequired()
658 658 def user_enable_force_password_reset(self):
659 659 _ = self.request.translate
660 660 c = self.load_default_context()
661 661
662 662 user_id = self.db_user_id
663 663 c.user = self.db_user
664 664
665 665 try:
666 666 c.user.update_userdata(force_password_change=True)
667 667
668 668 msg = _('Force password change enabled for user')
669 669 audit_logger.store_web('user.edit.password_reset.enabled',
670 670 user=c.rhodecode_user)
671 671
672 672 Session().commit()
673 673 h.flash(msg, category='success')
674 674 except Exception:
675 675 log.exception("Exception during password reset for user")
676 676 h.flash(_('An error occurred during password reset for user'),
677 677 category='error')
678 678
679 679 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
680 680
681 681 @LoginRequired()
682 682 @HasPermissionAllDecorator('hg.admin')
683 683 @CSRFRequired()
684 684 def user_disable_force_password_reset(self):
685 685 _ = self.request.translate
686 686 c = self.load_default_context()
687 687
688 688 user_id = self.db_user_id
689 689 c.user = self.db_user
690 690
691 691 try:
692 692 c.user.update_userdata(force_password_change=False)
693 693
694 694 msg = _('Force password change disabled for user')
695 695 audit_logger.store_web(
696 696 'user.edit.password_reset.disabled',
697 697 user=c.rhodecode_user)
698 698
699 699 Session().commit()
700 700 h.flash(msg, category='success')
701 701 except Exception:
702 702 log.exception("Exception during password reset for user")
703 703 h.flash(_('An error occurred during password reset for user'),
704 704 category='error')
705 705
706 706 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
707 707
708 708 @LoginRequired()
709 709 @HasPermissionAllDecorator('hg.admin')
710 710 @CSRFRequired()
711 711 def user_notice_dismiss(self):
712 712 _ = self.request.translate
713 713 c = self.load_default_context()
714 714
715 715 user_id = self.db_user_id
716 716 c.user = self.db_user
717 717 user_notice_id = safe_int(self.request.POST.get('notice_id'))
718 718 notice = UserNotice().query()\
719 719 .filter(UserNotice.user_id == user_id)\
720 720 .filter(UserNotice.user_notice_id == user_notice_id)\
721 721 .scalar()
722 722 read = False
723 723 if notice:
724 724 notice.notice_read = True
725 725 Session().add(notice)
726 726 Session().commit()
727 727 read = True
728 728
729 729 return {'notice': user_notice_id, 'read': read}
730 730
731 731 @LoginRequired()
732 732 @HasPermissionAllDecorator('hg.admin')
733 733 @CSRFRequired()
734 734 def user_create_personal_repo_group(self):
735 735 """
736 736 Create personal repository group for this user
737 737 """
738 738 from rhodecode.model.repo_group import RepoGroupModel
739 739
740 740 _ = self.request.translate
741 741 c = self.load_default_context()
742 742
743 743 user_id = self.db_user_id
744 744 c.user = self.db_user
745 745
746 746 personal_repo_group = RepoGroup.get_user_personal_repo_group(
747 747 c.user.user_id)
748 748 if personal_repo_group:
749 749 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
750 750
751 751 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
752 752 named_personal_group = RepoGroup.get_by_group_name(
753 753 personal_repo_group_name)
754 754 try:
755 755
756 756 if named_personal_group and named_personal_group.user_id == c.user.user_id:
757 757 # migrate the same named group, and mark it as personal
758 758 named_personal_group.personal = True
759 759 Session().add(named_personal_group)
760 760 Session().commit()
761 761 msg = _('Linked repository group `%s` as personal' % (
762 762 personal_repo_group_name,))
763 763 h.flash(msg, category='success')
764 764 elif not named_personal_group:
765 765 RepoGroupModel().create_personal_repo_group(c.user)
766 766
767 767 msg = _('Created repository group `%s`' % (
768 768 personal_repo_group_name,))
769 769 h.flash(msg, category='success')
770 770 else:
771 771 msg = _('Repository group `%s` is already taken' % (
772 772 personal_repo_group_name,))
773 773 h.flash(msg, category='warning')
774 774 except Exception:
775 775 log.exception("Exception during repository group creation")
776 776 msg = _(
777 777 'An error occurred during repository group creation for user')
778 778 h.flash(msg, category='error')
779 779 Session().rollback()
780 780
781 781 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
782 782
783 783 @LoginRequired()
784 784 @HasPermissionAllDecorator('hg.admin')
785 785 def auth_tokens(self):
786 786 _ = self.request.translate
787 787 c = self.load_default_context()
788 788 c.user = self.db_user
789 789
790 790 c.active = 'auth_tokens'
791 791
792 792 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
793 793 c.role_values = [
794 794 (x, AuthTokenModel.cls._get_role_name(x))
795 795 for x in AuthTokenModel.cls.ROLES]
796 796 c.role_options = [(c.role_values, _("Role"))]
797 797 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
798 798 c.user.user_id, show_expired=True)
799 799 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
800 800 return self._get_template_context(c)
801 801
802 802 @LoginRequired()
803 803 @HasPermissionAllDecorator('hg.admin')
804 804 def auth_tokens_view(self):
805 805 _ = self.request.translate
806 806 c = self.load_default_context()
807 807 c.user = self.db_user
808 808
809 809 auth_token_id = self.request.POST.get('auth_token_id')
810 810
811 811 if auth_token_id:
812 812 token = UserApiKeys.get_or_404(auth_token_id)
813 813
814 814 return {
815 815 'auth_token': token.api_key
816 816 }
817 817
818 818 def maybe_attach_token_scope(self, token):
819 819 # implemented in EE edition
820 820 pass
821 821
822 822 @LoginRequired()
823 823 @HasPermissionAllDecorator('hg.admin')
824 824 @CSRFRequired()
825 825 def auth_tokens_add(self):
826 826 _ = self.request.translate
827 827 c = self.load_default_context()
828 828
829 829 user_id = self.db_user_id
830 830 c.user = self.db_user
831 831
832 832 user_data = c.user.get_api_data()
833 833 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
834 834 description = self.request.POST.get('description')
835 835 role = self.request.POST.get('role')
836 836
837 837 token = UserModel().add_auth_token(
838 838 user=c.user.user_id,
839 839 lifetime_minutes=lifetime, role=role, description=description,
840 840 scope_callback=self.maybe_attach_token_scope)
841 841 token_data = token.get_api_data()
842 842
843 843 audit_logger.store_web(
844 844 'user.edit.token.add', action_data={
845 845 'data': {'token': token_data, 'user': user_data}},
846 846 user=self._rhodecode_user, )
847 847 Session().commit()
848 848
849 849 h.flash(_("Auth token successfully created"), category='success')
850 850 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
851 851
852 852 @LoginRequired()
853 853 @HasPermissionAllDecorator('hg.admin')
854 854 @CSRFRequired()
855 855 def auth_tokens_delete(self):
856 856 _ = self.request.translate
857 857 c = self.load_default_context()
858 858
859 859 user_id = self.db_user_id
860 860 c.user = self.db_user
861 861
862 862 user_data = c.user.get_api_data()
863 863
864 864 del_auth_token = self.request.POST.get('del_auth_token')
865 865
866 866 if del_auth_token:
867 867 token = UserApiKeys.get_or_404(del_auth_token)
868 868 token_data = token.get_api_data()
869 869
870 870 AuthTokenModel().delete(del_auth_token, c.user.user_id)
871 871 audit_logger.store_web(
872 872 'user.edit.token.delete', action_data={
873 873 'data': {'token': token_data, 'user': user_data}},
874 874 user=self._rhodecode_user,)
875 875 Session().commit()
876 876 h.flash(_("Auth token successfully deleted"), category='success')
877 877
878 878 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
879 879
880 880 @LoginRequired()
881 881 @HasPermissionAllDecorator('hg.admin')
882 882 def ssh_keys(self):
883 883 _ = self.request.translate
884 884 c = self.load_default_context()
885 885 c.user = self.db_user
886 886
887 887 c.active = 'ssh_keys'
888 888 c.default_key = self.request.GET.get('default_key')
889 889 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
890 890 return self._get_template_context(c)
891 891
892 892 @LoginRequired()
893 893 @HasPermissionAllDecorator('hg.admin')
894 894 def ssh_keys_generate_keypair(self):
895 895 _ = self.request.translate
896 896 c = self.load_default_context()
897 897
898 898 c.user = self.db_user
899 899
900 900 c.active = 'ssh_keys_generate'
901 901 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
902 902 private_format = self.request.GET.get('private_format') \
903 903 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
904 904 c.private, c.public = SshKeyModel().generate_keypair(
905 905 comment=comment, private_format=private_format)
906 906
907 907 return self._get_template_context(c)
908 908
909 909 @LoginRequired()
910 910 @HasPermissionAllDecorator('hg.admin')
911 911 @CSRFRequired()
912 912 def ssh_keys_add(self):
913 913 _ = self.request.translate
914 914 c = self.load_default_context()
915 915
916 916 user_id = self.db_user_id
917 917 c.user = self.db_user
918 918
919 919 user_data = c.user.get_api_data()
920 920 key_data = self.request.POST.get('key_data')
921 921 description = self.request.POST.get('description')
922 922
923 923 fingerprint = 'unknown'
924 924 try:
925 925 if not key_data:
926 926 raise ValueError('Please add a valid public key')
927 927
928 928 key = SshKeyModel().parse_key(key_data.strip())
929 929 fingerprint = key.hash_md5()
930 930
931 931 ssh_key = SshKeyModel().create(
932 932 c.user.user_id, fingerprint, key.keydata, description)
933 933 ssh_key_data = ssh_key.get_api_data()
934 934
935 935 audit_logger.store_web(
936 936 'user.edit.ssh_key.add', action_data={
937 937 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
938 938 user=self._rhodecode_user, )
939 939 Session().commit()
940 940
941 941 # Trigger an event on change of keys.
942 942 trigger(SshKeyFileChangeEvent(), self.request.registry)
943 943
944 944 h.flash(_("Ssh Key successfully created"), category='success')
945 945
946 946 except IntegrityError:
947 947 log.exception("Exception during ssh key saving")
948 948 err = 'Such key with fingerprint `{}` already exists, ' \
949 949 'please use a different one'.format(fingerprint)
950 950 h.flash(_('An error occurred during ssh key saving: {}').format(err),
951 951 category='error')
952 952 except Exception as e:
953 953 log.exception("Exception during ssh key saving")
954 954 h.flash(_('An error occurred during ssh key saving: {}').format(e),
955 955 category='error')
956 956
957 957 return HTTPFound(
958 958 h.route_path('edit_user_ssh_keys', user_id=user_id))
959 959
960 960 @LoginRequired()
961 961 @HasPermissionAllDecorator('hg.admin')
962 962 @CSRFRequired()
963 963 def ssh_keys_delete(self):
964 964 _ = self.request.translate
965 965 c = self.load_default_context()
966 966
967 967 user_id = self.db_user_id
968 968 c.user = self.db_user
969 969
970 970 user_data = c.user.get_api_data()
971 971
972 972 del_ssh_key = self.request.POST.get('del_ssh_key')
973 973
974 974 if del_ssh_key:
975 975 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
976 976 ssh_key_data = ssh_key.get_api_data()
977 977
978 978 SshKeyModel().delete(del_ssh_key, c.user.user_id)
979 979 audit_logger.store_web(
980 980 'user.edit.ssh_key.delete', action_data={
981 981 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
982 982 user=self._rhodecode_user,)
983 983 Session().commit()
984 984 # Trigger an event on change of keys.
985 985 trigger(SshKeyFileChangeEvent(), self.request.registry)
986 986 h.flash(_("Ssh key successfully deleted"), category='success')
987 987
988 988 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
989 989
990 990 @LoginRequired()
991 991 @HasPermissionAllDecorator('hg.admin')
992 992 def emails(self):
993 993 _ = self.request.translate
994 994 c = self.load_default_context()
995 995 c.user = self.db_user
996 996
997 997 c.active = 'emails'
998 998 c.user_email_map = UserEmailMap.query() \
999 999 .filter(UserEmailMap.user == c.user).all()
1000 1000
1001 1001 return self._get_template_context(c)
1002 1002
1003 1003 @LoginRequired()
1004 1004 @HasPermissionAllDecorator('hg.admin')
1005 1005 @CSRFRequired()
1006 1006 def emails_add(self):
1007 1007 _ = self.request.translate
1008 1008 c = self.load_default_context()
1009 1009
1010 1010 user_id = self.db_user_id
1011 1011 c.user = self.db_user
1012 1012
1013 1013 email = self.request.POST.get('new_email')
1014 1014 user_data = c.user.get_api_data()
1015 1015 try:
1016 1016
1017 1017 form = UserExtraEmailForm(self.request.translate)()
1018 1018 data = form.to_python({'email': email})
1019 1019 email = data['email']
1020 1020
1021 1021 UserModel().add_extra_email(c.user.user_id, email)
1022 1022 audit_logger.store_web(
1023 1023 'user.edit.email.add',
1024 1024 action_data={'email': email, 'user': user_data},
1025 1025 user=self._rhodecode_user)
1026 1026 Session().commit()
1027 1027 h.flash(_("Added new email address `%s` for user account") % email,
1028 1028 category='success')
1029 1029 except formencode.Invalid as error:
1030 1030 msg = error.unpack_errors()['email']
1031 1031 h.flash(h.escape(msg), category='error')
1032 1032 except IntegrityError:
1033 1033 log.warning("Email %s already exists", email)
1034 1034 h.flash(_('Email `{}` is already registered for another user.').format(email),
1035 1035 category='error')
1036 1036 except Exception:
1037 1037 log.exception("Exception during email saving")
1038 1038 h.flash(_('An error occurred during email saving'),
1039 1039 category='error')
1040 1040 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1041 1041
1042 1042 @LoginRequired()
1043 1043 @HasPermissionAllDecorator('hg.admin')
1044 1044 @CSRFRequired()
1045 1045 def emails_delete(self):
1046 1046 _ = self.request.translate
1047 1047 c = self.load_default_context()
1048 1048
1049 1049 user_id = self.db_user_id
1050 1050 c.user = self.db_user
1051 1051
1052 1052 email_id = self.request.POST.get('del_email_id')
1053 1053 user_model = UserModel()
1054 1054
1055 1055 email = UserEmailMap.query().get(email_id).email
1056 1056 user_data = c.user.get_api_data()
1057 1057 user_model.delete_extra_email(c.user.user_id, email_id)
1058 1058 audit_logger.store_web(
1059 1059 'user.edit.email.delete',
1060 1060 action_data={'email': email, 'user': user_data},
1061 1061 user=self._rhodecode_user)
1062 1062 Session().commit()
1063 1063 h.flash(_("Removed email address from user account"),
1064 1064 category='success')
1065 1065 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1066 1066
1067 1067 @LoginRequired()
1068 1068 @HasPermissionAllDecorator('hg.admin')
1069 1069 def ips(self):
1070 1070 _ = self.request.translate
1071 1071 c = self.load_default_context()
1072 1072 c.user = self.db_user
1073 1073
1074 1074 c.active = 'ips'
1075 1075 c.user_ip_map = UserIpMap.query() \
1076 1076 .filter(UserIpMap.user == c.user).all()
1077 1077
1078 1078 c.inherit_default_ips = c.user.inherit_default_permissions
1079 1079 c.default_user_ip_map = UserIpMap.query() \
1080 1080 .filter(UserIpMap.user == User.get_default_user()).all()
1081 1081
1082 1082 return self._get_template_context(c)
1083 1083
1084 1084 @LoginRequired()
1085 1085 @HasPermissionAllDecorator('hg.admin')
1086 1086 @CSRFRequired()
1087 1087 # NOTE(marcink): this view is allowed for default users, as we can
1088 1088 # edit their IP white list
1089 1089 def ips_add(self):
1090 1090 _ = self.request.translate
1091 1091 c = self.load_default_context()
1092 1092
1093 1093 user_id = self.db_user_id
1094 1094 c.user = self.db_user
1095 1095
1096 1096 user_model = UserModel()
1097 1097 desc = self.request.POST.get('description')
1098 1098 try:
1099 1099 ip_list = user_model.parse_ip_range(
1100 1100 self.request.POST.get('new_ip'))
1101 1101 except Exception as e:
1102 1102 ip_list = []
1103 1103 log.exception("Exception during ip saving")
1104 1104 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1105 1105 category='error')
1106 1106 added = []
1107 1107 user_data = c.user.get_api_data()
1108 1108 for ip in ip_list:
1109 1109 try:
1110 1110 form = UserExtraIpForm(self.request.translate)()
1111 1111 data = form.to_python({'ip': ip})
1112 1112 ip = data['ip']
1113 1113
1114 1114 user_model.add_extra_ip(c.user.user_id, ip, desc)
1115 1115 audit_logger.store_web(
1116 1116 'user.edit.ip.add',
1117 1117 action_data={'ip': ip, 'user': user_data},
1118 1118 user=self._rhodecode_user)
1119 1119 Session().commit()
1120 1120 added.append(ip)
1121 1121 except formencode.Invalid as error:
1122 1122 msg = error.unpack_errors()['ip']
1123 1123 h.flash(msg, category='error')
1124 1124 except Exception:
1125 1125 log.exception("Exception during ip saving")
1126 1126 h.flash(_('An error occurred during ip saving'),
1127 1127 category='error')
1128 1128 if added:
1129 1129 h.flash(
1130 1130 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1131 1131 category='success')
1132 1132 if 'default_user' in self.request.POST:
1133 1133 # case for editing global IP list we do it for 'DEFAULT' user
1134 1134 raise HTTPFound(h.route_path('admin_permissions_ips'))
1135 1135 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1136 1136
1137 1137 @LoginRequired()
1138 1138 @HasPermissionAllDecorator('hg.admin')
1139 1139 @CSRFRequired()
1140 1140 # NOTE(marcink): this view is allowed for default users, as we can
1141 1141 # edit their IP white list
1142 1142 def ips_delete(self):
1143 1143 _ = self.request.translate
1144 1144 c = self.load_default_context()
1145 1145
1146 1146 user_id = self.db_user_id
1147 1147 c.user = self.db_user
1148 1148
1149 1149 ip_id = self.request.POST.get('del_ip_id')
1150 1150 user_model = UserModel()
1151 1151 user_data = c.user.get_api_data()
1152 1152 ip = UserIpMap.query().get(ip_id).ip_addr
1153 1153 user_model.delete_extra_ip(c.user.user_id, ip_id)
1154 1154 audit_logger.store_web(
1155 1155 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1156 1156 user=self._rhodecode_user)
1157 1157 Session().commit()
1158 1158 h.flash(_("Removed ip address from user whitelist"), category='success')
1159 1159
1160 1160 if 'default_user' in self.request.POST:
1161 1161 # case for editing global IP list we do it for 'DEFAULT' user
1162 1162 raise HTTPFound(h.route_path('admin_permissions_ips'))
1163 1163 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1164 1164
1165 1165 @LoginRequired()
1166 1166 @HasPermissionAllDecorator('hg.admin')
1167 1167 def groups_management(self):
1168 1168 c = self.load_default_context()
1169 1169 c.user = self.db_user
1170 1170 c.data = c.user.group_member
1171 1171
1172 1172 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1173 1173 for group in c.user.group_member]
1174 1174 c.groups = ext_json.str_json(groups)
1175 1175 c.active = 'groups'
1176 1176
1177 1177 return self._get_template_context(c)
1178 1178
1179 1179 @LoginRequired()
1180 1180 @HasPermissionAllDecorator('hg.admin')
1181 1181 @CSRFRequired()
1182 1182 def groups_management_updates(self):
1183 1183 _ = self.request.translate
1184 1184 c = self.load_default_context()
1185 1185
1186 1186 user_id = self.db_user_id
1187 1187 c.user = self.db_user
1188 1188
1189 1189 user_groups = set(self.request.POST.getall('users_group_id'))
1190 1190 user_groups_objects = []
1191 1191
1192 1192 for ugid in user_groups:
1193 1193 user_groups_objects.append(
1194 1194 UserGroupModel().get_group(safe_int(ugid)))
1195 1195 user_group_model = UserGroupModel()
1196 1196 added_to_groups, removed_from_groups = \
1197 1197 user_group_model.change_groups(c.user, user_groups_objects)
1198 1198
1199 1199 user_data = c.user.get_api_data()
1200 1200 for user_group_id in added_to_groups:
1201 1201 user_group = UserGroup.get(user_group_id)
1202 1202 old_values = user_group.get_api_data()
1203 1203 audit_logger.store_web(
1204 1204 'user_group.edit.member.add',
1205 1205 action_data={'user': user_data, 'old_data': old_values},
1206 1206 user=self._rhodecode_user)
1207 1207
1208 1208 for user_group_id in removed_from_groups:
1209 1209 user_group = UserGroup.get(user_group_id)
1210 1210 old_values = user_group.get_api_data()
1211 1211 audit_logger.store_web(
1212 1212 'user_group.edit.member.delete',
1213 1213 action_data={'user': user_data, 'old_data': old_values},
1214 1214 user=self._rhodecode_user)
1215 1215
1216 1216 Session().commit()
1217 1217 c.active = 'user_groups_management'
1218 1218 h.flash(_("Groups successfully changed"), category='success')
1219 1219
1220 1220 return HTTPFound(h.route_path(
1221 1221 'edit_user_groups_management', user_id=user_id))
1222 1222
1223 1223 @LoginRequired()
1224 1224 @HasPermissionAllDecorator('hg.admin')
1225 1225 def user_audit_logs(self):
1226 1226 _ = self.request.translate
1227 1227 c = self.load_default_context()
1228 1228 c.user = self.db_user
1229 1229
1230 1230 c.active = 'audit'
1231 1231
1232 1232 p = safe_int(self.request.GET.get('page', 1), 1)
1233 1233
1234 1234 filter_term = self.request.GET.get('filter')
1235 1235 user_log = UserModel().get_user_log(c.user, filter_term)
1236 1236
1237 1237 def url_generator(page_num):
1238 1238 query_params = {
1239 1239 'page': page_num
1240 1240 }
1241 1241 if filter_term:
1242 1242 query_params['filter'] = filter_term
1243 1243 return self.request.current_route_path(_query=query_params)
1244 1244
1245 1245 c.audit_logs = SqlPage(
1246 1246 user_log, page=p, items_per_page=10, url_maker=url_generator)
1247 1247 c.filter_term = filter_term
1248 1248 return self._get_template_context(c)
1249 1249
1250 1250 @LoginRequired()
1251 1251 @HasPermissionAllDecorator('hg.admin')
1252 1252 def user_audit_logs_download(self):
1253 1253 _ = self.request.translate
1254 1254 c = self.load_default_context()
1255 1255 c.user = self.db_user
1256 1256
1257 1257 user_log = UserModel().get_user_log(c.user, filter_term=None)
1258 1258
1259 1259 audit_log_data = {}
1260 1260 for entry in user_log:
1261 1261 audit_log_data[entry.user_log_id] = entry.get_dict()
1262 1262
1263 1263 response = Response(ext_json.formatted_str_json(audit_log_data))
1264 1264 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1265 1265 response.content_type = 'application/json'
1266 1266
1267 1267 return response
1268 1268
1269 1269 @LoginRequired()
1270 1270 @HasPermissionAllDecorator('hg.admin')
1271 1271 def user_perms_summary(self):
1272 1272 _ = self.request.translate
1273 1273 c = self.load_default_context()
1274 1274 c.user = self.db_user
1275 1275
1276 1276 c.active = 'perms_summary'
1277 1277 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1278 1278
1279 1279 return self._get_template_context(c)
1280 1280
1281 1281 @LoginRequired()
1282 1282 @HasPermissionAllDecorator('hg.admin')
1283 1283 def user_perms_summary_json(self):
1284 1284 self.load_default_context()
1285 1285 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1286 1286
1287 1287 return perm_user.permissions
1288 1288
1289 1289 @LoginRequired()
1290 1290 @HasPermissionAllDecorator('hg.admin')
1291 1291 def user_caches(self):
1292 1292 _ = self.request.translate
1293 1293 c = self.load_default_context()
1294 1294 c.user = self.db_user
1295 1295
1296 1296 c.active = 'caches'
1297 1297 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1298 1298
1299 1299 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1300 1300 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1301 1301 c.backend = c.region.backend
1302 1302 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1303 1303
1304 1304 return self._get_template_context(c)
1305 1305
1306 1306 @LoginRequired()
1307 1307 @HasPermissionAllDecorator('hg.admin')
1308 1308 @CSRFRequired()
1309 1309 def user_caches_update(self):
1310 1310 _ = self.request.translate
1311 1311 c = self.load_default_context()
1312 1312 c.user = self.db_user
1313 1313
1314 1314 c.active = 'caches'
1315 1315 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1316 1316
1317 1317 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1318 1318 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1319 1319
1320 1320 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1321 1321
1322 1322 return HTTPFound(h.route_path(
1323 1323 'edit_user_caches', user_id=c.user.user_id))
@@ -1,378 +1,377 b''
1 1
2 2
3 3 # Copyright (C) 2013-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.lib import helpers as h, ext_json
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 35 from rhodecode.lib.utils2 import time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 38 from rhodecode.model.gist import GistModel
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.db import Gist, User, or_
41 41 from rhodecode.model import validation_schema
42 42 from rhodecode.model.validation_schema.schemas import gist_schema
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class GistView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 _ = self.request.translate
52 52 c = self._get_local_tmpl_context()
53 53 c.user = c.auth_user.get_instance()
54 54
55 55 c.lifetime_values = [
56 56 (-1, _('forever')),
57 57 (5, _('5 minutes')),
58 58 (60, _('1 hour')),
59 59 (60 * 24, _('1 day')),
60 60 (60 * 24 * 30, _('1 month')),
61 61 ]
62 62
63 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 64 c.acl_options = [
65 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 67 ]
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 def gist_show_all(self):
73 73 c = self.load_default_context()
74 74
75 75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
76 76 c.show_private = self.request.GET.get('private') and not_default_user
77 77 c.show_public = self.request.GET.get('public') and not_default_user
78 78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
79 79
80 80 gists = _gists = Gist().query()\
81 81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
82 82 .order_by(Gist.created_on.desc())
83 83
84 84 c.active = 'public'
85 85 # MY private
86 86 if c.show_private and not c.show_public:
87 87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
88 88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
89 89 c.active = 'my_private'
90 90 # MY public
91 91 elif c.show_public and not c.show_private:
92 92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
93 93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 94 c.active = 'my_public'
95 95 # MY public+private
96 96 elif c.show_private and c.show_public:
97 97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
98 98 Gist.gist_type == Gist.GIST_PRIVATE))\
99 99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
100 100 c.active = 'my_all'
101 101 # Show all by super-admin
102 102 elif c.show_all:
103 103 c.active = 'all'
104 104 gists = _gists
105 105
106 106 # default show ALL public gists
107 107 if not c.show_public and not c.show_private and not c.show_all:
108 108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
109 109 c.active = 'public'
110 110
111 111 _render = self.request.get_partial_renderer(
112 112 'rhodecode:templates/data_table/_dt_elements.mako')
113 113
114 114 data = []
115 115
116 116 for gist in gists:
117 117 data.append({
118 118 'created_on': _render('gist_created', gist.created_on),
119 119 'created_on_raw': gist.created_on,
120 120 'type': _render('gist_type', gist.gist_type),
121 121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
122 122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
123 123 'author_raw': h.escape(gist.owner.full_contact),
124 124 'expires': _render('gist_expires', gist.gist_expires),
125 125 'description': _render('gist_description', gist.gist_description)
126 126 })
127 127 c.data = ext_json.str_json(data)
128 128
129 129 return self._get_template_context(c)
130 130
131 131 @LoginRequired()
132 132 @NotAnonymous()
133 133 def gist_new(self):
134 134 c = self.load_default_context()
135 135 return self._get_template_context(c)
136 136
137 137 @LoginRequired()
138 138 @NotAnonymous()
139 139 @CSRFRequired()
140 140 def gist_create(self):
141 141 _ = self.request.translate
142 142 c = self.load_default_context()
143 143
144 144 data = dict(self.request.POST)
145 145
146 146 filename = data.pop('filename', '') or Gist.DEFAULT_FILENAME
147 147
148 148 data['nodes'] = [{
149 149 'filename': filename,
150 150 'content': data.pop('content', ''),
151 151 'mimetype': data.pop('mimetype', None) # None is autodetect
152 152 }]
153 153
154 154 schema = gist_schema.GistSchema().bind(
155 155 lifetime_options=[x[0] for x in c.lifetime_values])
156 156
157 157 try:
158
159 158 schema_data = schema.deserialize(data)
160 159
161 160 # convert to safer format with just KEYs so we sure no duplicates
162 161 schema_data['nodes'] = gist_schema.sequence_to_nodes(schema_data['nodes'])
163 162
164 163 gist = GistModel().create(
165 164 gist_id=schema_data['gistid'], # custom access id not real ID
166 165 description=schema_data['description'],
167 166 owner=self._rhodecode_user.user_id,
168 167 gist_mapping=schema_data['nodes'],
169 168 gist_type=schema_data['gist_type'],
170 169 lifetime=schema_data['lifetime'],
171 170 gist_acl_level=schema_data['gist_acl_level']
172 171 )
173 172 Session().commit()
174 173 new_gist_id = gist.gist_access_id
175 174 except validation_schema.Invalid as errors:
176 175 defaults = data
177 176 errors = errors.asdict()
178 177
179 178 if 'nodes.0.content' in errors:
180 179 errors['content'] = errors['nodes.0.content']
181 180 del errors['nodes.0.content']
182 181 if 'nodes.0.filename' in errors:
183 182 errors['filename'] = errors['nodes.0.filename']
184 183 del errors['nodes.0.filename']
185 184
186 185 data = render('rhodecode:templates/admin/gists/gist_new.mako',
187 186 self._get_template_context(c), self.request)
188 187 html = formencode.htmlfill.render(
189 188 data,
190 189 defaults=defaults,
191 190 errors=errors,
192 191 prefix_error=False,
193 192 encoding="UTF-8",
194 193 force_defaults=False
195 194 )
196 195 return Response(html)
197 196
198 197 except Exception:
199 198 log.exception("Exception while trying to create a gist")
200 199 h.flash(_('Error occurred during gist creation'), category='error')
201 200 raise HTTPFound(h.route_url('gists_new'))
202 201 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
203 202
204 203 @LoginRequired()
205 204 @NotAnonymous()
206 205 @CSRFRequired()
207 206 def gist_delete(self):
208 207 _ = self.request.translate
209 208 gist_id = self.request.matchdict['gist_id']
210 209
211 210 c = self.load_default_context()
212 211 c.gist = Gist.get_or_404(gist_id)
213 212
214 213 owner = c.gist.gist_owner == self._rhodecode_user.user_id
215 214 if not (h.HasPermissionAny('hg.admin')() or owner):
216 215 log.warning('Deletion of Gist was forbidden '
217 216 'by unauthorized user: `%s`', self._rhodecode_user)
218 217 raise HTTPNotFound()
219 218
220 219 GistModel().delete(c.gist)
221 220 Session().commit()
222 221 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
223 222
224 223 raise HTTPFound(h.route_url('gists_show'))
225 224
226 225 def _get_gist(self, gist_id):
227 226
228 227 gist = Gist.get_or_404(gist_id)
229 228
230 229 # Check if this gist is expired
231 230 if gist.gist_expires != -1:
232 231 if time.time() > gist.gist_expires:
233 232 log.error(
234 233 'Gist expired at %s', time_to_datetime(gist.gist_expires))
235 234 raise HTTPNotFound()
236 235
237 236 # check if this gist requires a login
238 237 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
239 238 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
240 239 log.error("Anonymous user %s tried to access protected gist `%s`",
241 240 self._rhodecode_user, gist_id)
242 241 raise HTTPNotFound()
243 242 return gist
244 243
245 244 @LoginRequired()
246 245 def gist_show(self):
247 246 gist_id = self.request.matchdict['gist_id']
248 247
249 248 # TODO(marcink): expose those via matching dict
250 249 revision = self.request.matchdict.get('revision', 'tip')
251 250 f_path = self.request.matchdict.get('f_path', None)
252 251 return_format = self.request.matchdict.get('format')
253 252
254 253 c = self.load_default_context()
255 254 c.gist = self._get_gist(gist_id)
256 255 c.render = not self.request.GET.get('no-render', False)
257 256
258 257 try:
259 258 c.file_last_commit, c.files = GistModel().get_gist_files(
260 259 gist_id, revision=revision)
261 260 except VCSError:
262 261 log.exception("Exception in gist show")
263 262 raise HTTPNotFound()
264 263
265 264 if return_format == 'raw':
266 content = '\n\n'.join([f.content for f in c.files
267 if (f_path is None or f.path == f_path)])
265 content = b'\n\n'.join([f.content for f in c.files
266 if (f_path is None or f.path == f_path)])
268 267 response = Response(content)
269 268 response.content_type = 'text/plain'
270 269 return response
271 270 elif return_format:
272 271 raise HTTPBadRequest()
273 272
274 273 return self._get_template_context(c)
275 274
276 275 @LoginRequired()
277 276 @NotAnonymous()
278 277 def gist_edit(self):
279 278 _ = self.request.translate
280 279 gist_id = self.request.matchdict['gist_id']
281 280 c = self.load_default_context()
282 281 c.gist = self._get_gist(gist_id)
283 282
284 283 owner = c.gist.gist_owner == self._rhodecode_user.user_id
285 284 if not (h.HasPermissionAny('hg.admin')() or owner):
286 285 raise HTTPNotFound()
287 286
288 287 try:
289 288 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
290 289 except VCSError:
291 290 log.exception("Exception in gist edit")
292 291 raise HTTPNotFound()
293 292
294 293 if c.gist.gist_expires == -1:
295 294 expiry = _('never')
296 295 else:
297 296 # this cannot use timeago, since it's used in select2 as a value
298 297 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
299 298
300 299 c.lifetime_values.append(
301 300 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
302 301 )
303 302
304 303 return self._get_template_context(c)
305 304
306 305 @LoginRequired()
307 306 @NotAnonymous()
308 307 @CSRFRequired()
309 308 def gist_update(self):
310 309 _ = self.request.translate
311 310 gist_id = self.request.matchdict['gist_id']
312 311 c = self.load_default_context()
313 312 c.gist = self._get_gist(gist_id)
314 313
315 314 owner = c.gist.gist_owner == self._rhodecode_user.user_id
316 315 if not (h.HasPermissionAny('hg.admin')() or owner):
317 316 raise HTTPNotFound()
318 317
319 318 data = peppercorn.parse(self.request.POST.items())
320 319
321 320 schema = gist_schema.GistSchema()
322 321 schema = schema.bind(
323 322 # '0' is special value to leave lifetime untouched
324 323 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
325 324 )
326 325
327 326 try:
328 327 schema_data = schema.deserialize(data)
329 328 # convert to safer format with just KEYs so we sure no duplicates
330 329 schema_data['nodes'] = gist_schema.sequence_to_nodes(
331 330 schema_data['nodes'])
332 331
333 332 GistModel().update(
334 333 gist=c.gist,
335 334 description=schema_data['description'],
336 335 owner=c.gist.owner,
337 336 gist_mapping=schema_data['nodes'],
338 337 lifetime=schema_data['lifetime'],
339 338 gist_acl_level=schema_data['gist_acl_level']
340 339 )
341 340
342 341 Session().commit()
343 342 h.flash(_('Successfully updated gist content'), category='success')
344 343 except NodeNotChangedError:
345 344 # raised if nothing was changed in repo itself. We anyway then
346 345 # store only DB stuff for gist
347 346 Session().commit()
348 347 h.flash(_('Successfully updated gist data'), category='success')
349 348 except validation_schema.Invalid as errors:
350 349 errors = h.escape(errors.asdict())
351 350 h.flash(_('Error occurred during update of gist {}: {}').format(
352 351 gist_id, errors), category='error')
353 352 except Exception:
354 353 log.exception("Exception in gist edit")
355 354 h.flash(_('Error occurred during update of gist %s') % gist_id,
356 355 category='error')
357 356
358 357 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
359 358
360 359 @LoginRequired()
361 360 @NotAnonymous()
362 361 def gist_edit_check_revision(self):
363 362 _ = self.request.translate
364 363 gist_id = self.request.matchdict['gist_id']
365 364 c = self.load_default_context()
366 365 c.gist = self._get_gist(gist_id)
367 366
368 367 last_rev = c.gist.scm_instance().get_commit()
369 368 success = True
370 369 revision = self.request.GET.get('revision')
371 370
372 371 if revision != last_rev.raw_id:
373 372 log.error('Last revision %s is different then submitted %s',
374 373 revision, last_rev)
375 374 # our gist has newer version than we
376 375 success = False
377 376
378 377 return {'success': success}
@@ -1,856 +1,856 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 26
27 27 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoGroupPermissionAny, AuthUser)
32 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 33 from rhodecode.lib.index import searcher_from_config
34 34 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
35 35 from rhodecode.lib.vcs.nodes import FileNode
36 36 from rhodecode.model.db import (
37 37 func, true, or_, case, cast, in_filter_generator, String, Session,
38 38 Repository, RepoGroup, User, UserGroup, PullRequest)
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.repo_group import RepoGroupModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.user_group import UserGroupModel
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class HomeView(BaseAppView, DataGridAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context()
51 51 c.user = c.auth_user.get_instance()
52 52 return c
53 53
54 54 @LoginRequired()
55 55 def user_autocomplete_data(self):
56 56 self.load_default_context()
57 57 query = self.request.GET.get('query')
58 58 active = str2bool(self.request.GET.get('active') or True)
59 59 include_groups = str2bool(self.request.GET.get('user_groups'))
60 60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
61 61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
62 62
63 63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
64 64 query, active, include_groups)
65 65
66 66 _users = UserModel().get_users(
67 67 name_contains=query, only_active=active)
68 68
69 69 def maybe_skip_default_user(usr):
70 70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
71 71 return False
72 72 return True
73 _users = filter(maybe_skip_default_user, _users)
73 _users = list(filter(maybe_skip_default_user, _users))
74 74
75 75 if include_groups:
76 76 # extend with user groups
77 77 _user_groups = UserGroupModel().get_user_groups(
78 78 name_contains=query, only_active=active,
79 79 expand_groups=expand_groups)
80 80 _users = _users + _user_groups
81 81
82 82 return {'suggestions': _users}
83 83
84 84 @LoginRequired()
85 85 @NotAnonymous()
86 86 def user_group_autocomplete_data(self):
87 87 self.load_default_context()
88 88 query = self.request.GET.get('query')
89 89 active = str2bool(self.request.GET.get('active') or True)
90 90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
91 91
92 92 log.debug('generating user group list, query:%s, active:%s',
93 93 query, active)
94 94
95 95 _user_groups = UserGroupModel().get_user_groups(
96 96 name_contains=query, only_active=active,
97 97 expand_groups=expand_groups)
98 98 _user_groups = _user_groups
99 99
100 100 return {'suggestions': _user_groups}
101 101
102 102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
103 103 org_query = name_contains
104 104 allowed_ids = self._rhodecode_user.repo_acl_ids(
105 105 ['repository.read', 'repository.write', 'repository.admin'],
106 106 cache=True, name_filter=name_contains) or [-1]
107 107
108 108 query = Session().query(
109 109 Repository.repo_name,
110 110 Repository.repo_id,
111 111 Repository.repo_type,
112 112 Repository.private,
113 113 )\
114 114 .filter(Repository.archived.isnot(true()))\
115 115 .filter(or_(
116 116 # generate multiple IN to fix limitation problems
117 117 *in_filter_generator(Repository.repo_id, allowed_ids)
118 118 ))
119 119
120 120 query = query.order_by(case(
121 121 [
122 122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
123 123 ],
124 124 ))
125 125 query = query.order_by(func.length(Repository.repo_name))
126 126 query = query.order_by(Repository.repo_name)
127 127
128 128 if repo_type:
129 129 query = query.filter(Repository.repo_type == repo_type)
130 130
131 131 if name_contains:
132 132 ilike_expression = '%{}%'.format(safe_str(name_contains))
133 133 query = query.filter(
134 134 Repository.repo_name.ilike(ilike_expression))
135 135 query = query.limit(limit)
136 136
137 137 acl_iter = query
138 138
139 139 return [
140 140 {
141 141 'id': obj.repo_name,
142 142 'value': org_query,
143 143 'value_display': obj.repo_name,
144 144 'text': obj.repo_name,
145 145 'type': 'repo',
146 146 'repo_id': obj.repo_id,
147 147 'repo_type': obj.repo_type,
148 148 'private': obj.private,
149 149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
150 150 }
151 151 for obj in acl_iter]
152 152
153 153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
154 154 org_query = name_contains
155 155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
156 156 ['group.read', 'group.write', 'group.admin'],
157 157 cache=True, name_filter=name_contains) or [-1]
158 158
159 159 query = Session().query(
160 160 RepoGroup.group_id,
161 161 RepoGroup.group_name,
162 162 )\
163 163 .filter(or_(
164 164 # generate multiple IN to fix limitation problems
165 165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
166 166 ))
167 167
168 168 query = query.order_by(case(
169 169 [
170 170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
171 171 ],
172 172 ))
173 173 query = query.order_by(func.length(RepoGroup.group_name))
174 174 query = query.order_by(RepoGroup.group_name)
175 175
176 176 if name_contains:
177 177 ilike_expression = u'%{}%'.format(safe_str(name_contains))
178 178 query = query.filter(
179 179 RepoGroup.group_name.ilike(ilike_expression))
180 180 query = query.limit(limit)
181 181
182 182 acl_iter = query
183 183
184 184 return [
185 185 {
186 186 'id': obj.group_name,
187 187 'value': org_query,
188 188 'value_display': obj.group_name,
189 189 'text': obj.group_name,
190 190 'type': 'repo_group',
191 191 'repo_group_id': obj.group_id,
192 192 'url': h.route_path(
193 193 'repo_group_home', repo_group_name=obj.group_name)
194 194 }
195 195 for obj in acl_iter]
196 196
197 197 def _get_user_list(self, name_contains=None, limit=20):
198 198 org_query = name_contains
199 199 if not name_contains:
200 200 return [], False
201 201
202 202 # TODO(marcink): should all logged in users be allowed to search others?
203 203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
204 204 if not allowed_user_search:
205 205 return [], False
206 206
207 207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
208 208 if len(name_contains) != 1:
209 209 return [], False
210 210
211 211 name_contains = name_contains[0]
212 212
213 213 query = User.query()\
214 214 .order_by(func.length(User.username))\
215 215 .order_by(User.username) \
216 216 .filter(User.username != User.DEFAULT_USER)
217 217
218 218 if name_contains:
219 219 ilike_expression = u'%{}%'.format(safe_str(name_contains))
220 220 query = query.filter(
221 221 User.username.ilike(ilike_expression))
222 222 query = query.limit(limit)
223 223
224 224 acl_iter = query
225 225
226 226 return [
227 227 {
228 228 'id': obj.user_id,
229 229 'value': org_query,
230 230 'value_display': 'user: `{}`'.format(obj.username),
231 231 'type': 'user',
232 'icon_link': h.gravatar_url(obj.email, 30),
232 'icon_link': h.gravatar_url(obj.email, 30, request=self.request),
233 233 'url': h.route_path(
234 234 'user_profile', username=obj.username)
235 235 }
236 236 for obj in acl_iter], True
237 237
238 238 def _get_user_groups_list(self, name_contains=None, limit=20):
239 239 org_query = name_contains
240 240 if not name_contains:
241 241 return [], False
242 242
243 243 # TODO(marcink): should all logged in users be allowed to search others?
244 244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
245 245 if not allowed_user_search:
246 246 return [], False
247 247
248 248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
249 249 if len(name_contains) != 1:
250 250 return [], False
251 251
252 252 name_contains = name_contains[0]
253 253
254 254 query = UserGroup.query()\
255 255 .order_by(func.length(UserGroup.users_group_name))\
256 256 .order_by(UserGroup.users_group_name)
257 257
258 258 if name_contains:
259 259 ilike_expression = u'%{}%'.format(safe_str(name_contains))
260 260 query = query.filter(
261 261 UserGroup.users_group_name.ilike(ilike_expression))
262 262 query = query.limit(limit)
263 263
264 264 acl_iter = query
265 265
266 266 return [
267 267 {
268 268 'id': obj.users_group_id,
269 269 'value': org_query,
270 270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
271 271 'type': 'user_group',
272 272 'url': h.route_path(
273 273 'user_group_profile', user_group_name=obj.users_group_name)
274 274 }
275 275 for obj in acl_iter], True
276 276
277 277 def _get_pull_request_list(self, name_contains=None, limit=20):
278 278 org_query = name_contains
279 279 if not name_contains:
280 280 return [], False
281 281
282 282 # TODO(marcink): should all logged in users be allowed to search others?
283 283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
284 284 if not allowed_user_search:
285 285 return [], False
286 286
287 287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
288 288 if len(name_contains) != 1:
289 289 return [], False
290 290
291 291 name_contains = name_contains[0]
292 292
293 293 allowed_ids = self._rhodecode_user.repo_acl_ids(
294 294 ['repository.read', 'repository.write', 'repository.admin'],
295 295 cache=True) or [-1]
296 296
297 297 query = Session().query(
298 298 PullRequest.pull_request_id,
299 299 PullRequest.title,
300 300 )
301 301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
302 302
303 303 query = query.filter(or_(
304 304 # generate multiple IN to fix limitation problems
305 305 *in_filter_generator(Repository.repo_id, allowed_ids)
306 306 ))
307 307
308 308 query = query.order_by(PullRequest.pull_request_id)
309 309
310 310 if name_contains:
311 311 ilike_expression = u'%{}%'.format(safe_str(name_contains))
312 312 query = query.filter(or_(
313 313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
314 314 PullRequest.title.ilike(ilike_expression),
315 315 PullRequest.description.ilike(ilike_expression),
316 316 ))
317 317
318 318 query = query.limit(limit)
319 319
320 320 acl_iter = query
321 321
322 322 return [
323 323 {
324 324 'id': obj.pull_request_id,
325 325 'value': org_query,
326 326 'value_display': 'pull request: `!{} - {}`'.format(
327 327 obj.pull_request_id, safe_str(obj.title[:50])),
328 328 'type': 'pull_request',
329 329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
330 330 }
331 331 for obj in acl_iter], True
332 332
333 333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
334 334 repo_name = repo_group_name = None
335 335 if repo:
336 336 repo_name = repo.repo_name
337 337 if repo_group:
338 338 repo_group_name = repo_group.group_name
339 339
340 340 org_query = query
341 341 if not query or len(query) < 3 or not searcher:
342 342 return [], False
343 343
344 344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
345 345
346 346 if len(commit_hashes) != 1:
347 347 return [], False
348 348
349 349 commit_hash = commit_hashes[0]
350 350
351 351 result = searcher.search(
352 352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
353 353 repo_name, repo_group_name, raise_on_exc=False)
354 354
355 355 commits = []
356 356 for entry in result['results']:
357 357 repo_data = {
358 358 'repository_id': entry.get('repository_id'),
359 359 'repository_type': entry.get('repo_type'),
360 360 'repository_name': entry.get('repository'),
361 361 }
362 362
363 363 commit_entry = {
364 364 'id': entry['commit_id'],
365 365 'value': org_query,
366 366 'value_display': '`{}` commit: {}'.format(
367 367 entry['repository'], entry['commit_id']),
368 368 'type': 'commit',
369 369 'repo': entry['repository'],
370 370 'repo_data': repo_data,
371 371
372 372 'url': h.route_path(
373 373 'repo_commit',
374 374 repo_name=entry['repository'], commit_id=entry['commit_id'])
375 375 }
376 376
377 377 commits.append(commit_entry)
378 378 return commits, True
379 379
380 380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
381 381 repo_name = repo_group_name = None
382 382 if repo:
383 383 repo_name = repo.repo_name
384 384 if repo_group:
385 385 repo_group_name = repo_group.group_name
386 386
387 387 org_query = query
388 388 if not query or len(query) < 3 or not searcher:
389 389 return [], False
390 390
391 391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
392 392 if len(paths_re) != 1:
393 393 return [], False
394 394
395 395 file_path = paths_re[0]
396 396
397 397 search_path = searcher.escape_specials(file_path)
398 398 result = searcher.search(
399 399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
400 400 repo_name, repo_group_name, raise_on_exc=False)
401 401
402 402 files = []
403 403 for entry in result['results']:
404 404 repo_data = {
405 405 'repository_id': entry.get('repository_id'),
406 406 'repository_type': entry.get('repo_type'),
407 407 'repository_name': entry.get('repository'),
408 408 }
409 409
410 410 file_entry = {
411 411 'id': entry['commit_id'],
412 412 'value': org_query,
413 413 'value_display': '`{}` file: {}'.format(
414 414 entry['repository'], entry['file']),
415 415 'type': 'file',
416 416 'repo': entry['repository'],
417 417 'repo_data': repo_data,
418 418
419 419 'url': h.route_path(
420 420 'repo_files',
421 421 repo_name=entry['repository'], commit_id=entry['commit_id'],
422 422 f_path=entry['file'])
423 423 }
424 424
425 425 files.append(file_entry)
426 426 return files, True
427 427
428 428 @LoginRequired()
429 429 def repo_list_data(self):
430 430 _ = self.request.translate
431 431 self.load_default_context()
432 432
433 433 query = self.request.GET.get('query')
434 434 repo_type = self.request.GET.get('repo_type')
435 435 log.debug('generating repo list, query:%s, repo_type:%s',
436 436 query, repo_type)
437 437
438 438 res = []
439 439 repos = self._get_repo_list(query, repo_type=repo_type)
440 440 if repos:
441 441 res.append({
442 442 'text': _('Repositories'),
443 443 'children': repos
444 444 })
445 445
446 446 data = {
447 447 'more': False,
448 448 'results': res
449 449 }
450 450 return data
451 451
452 452 @LoginRequired()
453 453 def repo_group_list_data(self):
454 454 _ = self.request.translate
455 455 self.load_default_context()
456 456
457 457 query = self.request.GET.get('query')
458 458
459 459 log.debug('generating repo group list, query:%s',
460 460 query)
461 461
462 462 res = []
463 463 repo_groups = self._get_repo_group_list(query)
464 464 if repo_groups:
465 465 res.append({
466 466 'text': _('Repository Groups'),
467 467 'children': repo_groups
468 468 })
469 469
470 470 data = {
471 471 'more': False,
472 472 'results': res
473 473 }
474 474 return data
475 475
476 476 def _get_default_search_queries(self, search_context, searcher, query):
477 477 if not searcher:
478 478 return []
479 479
480 480 is_es_6 = searcher.is_es_6
481 481
482 482 queries = []
483 483 repo_group_name, repo_name, repo_context = None, None, None
484 484
485 485 # repo group context
486 486 if search_context.get('search_context[repo_group_name]'):
487 487 repo_group_name = search_context.get('search_context[repo_group_name]')
488 488 if search_context.get('search_context[repo_name]'):
489 489 repo_name = search_context.get('search_context[repo_name]')
490 490 repo_context = search_context.get('search_context[repo_view_type]')
491 491
492 492 if is_es_6 and repo_name:
493 493 # files
494 494 def query_modifier():
495 495 qry = query
496 496 return {'q': qry, 'type': 'content'}
497 497
498 498 label = u'File content search for `{}`'.format(h.escape(query))
499 499 file_qry = {
500 500 'id': -10,
501 501 'value': query,
502 502 'value_display': label,
503 503 'value_icon': '<i class="icon-code"></i>',
504 504 'type': 'search',
505 505 'subtype': 'repo',
506 506 'url': h.route_path('search_repo',
507 507 repo_name=repo_name,
508 508 _query=query_modifier())
509 509 }
510 510
511 511 # commits
512 512 def query_modifier():
513 513 qry = query
514 514 return {'q': qry, 'type': 'commit'}
515 515
516 516 label = u'Commit search for `{}`'.format(h.escape(query))
517 517 commit_qry = {
518 518 'id': -20,
519 519 'value': query,
520 520 'value_display': label,
521 521 'value_icon': '<i class="icon-history"></i>',
522 522 'type': 'search',
523 523 'subtype': 'repo',
524 524 'url': h.route_path('search_repo',
525 525 repo_name=repo_name,
526 526 _query=query_modifier())
527 527 }
528 528
529 529 if repo_context in ['commit', 'commits']:
530 530 queries.extend([commit_qry, file_qry])
531 531 elif repo_context in ['files', 'summary']:
532 532 queries.extend([file_qry, commit_qry])
533 533 else:
534 534 queries.extend([commit_qry, file_qry])
535 535
536 536 elif is_es_6 and repo_group_name:
537 537 # files
538 538 def query_modifier():
539 539 qry = query
540 540 return {'q': qry, 'type': 'content'}
541 541
542 542 label = u'File content search for `{}`'.format(query)
543 543 file_qry = {
544 544 'id': -30,
545 545 'value': query,
546 546 'value_display': label,
547 547 'value_icon': '<i class="icon-code"></i>',
548 548 'type': 'search',
549 549 'subtype': 'repo_group',
550 550 'url': h.route_path('search_repo_group',
551 551 repo_group_name=repo_group_name,
552 552 _query=query_modifier())
553 553 }
554 554
555 555 # commits
556 556 def query_modifier():
557 557 qry = query
558 558 return {'q': qry, 'type': 'commit'}
559 559
560 560 label = u'Commit search for `{}`'.format(query)
561 561 commit_qry = {
562 562 'id': -40,
563 563 'value': query,
564 564 'value_display': label,
565 565 'value_icon': '<i class="icon-history"></i>',
566 566 'type': 'search',
567 567 'subtype': 'repo_group',
568 568 'url': h.route_path('search_repo_group',
569 569 repo_group_name=repo_group_name,
570 570 _query=query_modifier())
571 571 }
572 572
573 573 if repo_context in ['commit', 'commits']:
574 574 queries.extend([commit_qry, file_qry])
575 575 elif repo_context in ['files', 'summary']:
576 576 queries.extend([file_qry, commit_qry])
577 577 else:
578 578 queries.extend([commit_qry, file_qry])
579 579
580 580 # Global, not scoped
581 581 if not queries:
582 582 queries.append(
583 583 {
584 584 'id': -1,
585 585 'value': query,
586 586 'value_display': u'File content search for: `{}`'.format(query),
587 587 'value_icon': '<i class="icon-code"></i>',
588 588 'type': 'search',
589 589 'subtype': 'global',
590 590 'url': h.route_path('search',
591 591 _query={'q': query, 'type': 'content'})
592 592 })
593 593 queries.append(
594 594 {
595 595 'id': -2,
596 596 'value': query,
597 597 'value_display': u'Commit search for: `{}`'.format(query),
598 598 'value_icon': '<i class="icon-history"></i>',
599 599 'type': 'search',
600 600 'subtype': 'global',
601 601 'url': h.route_path('search',
602 602 _query={'q': query, 'type': 'commit'})
603 603 })
604 604
605 605 return queries
606 606
607 607 @LoginRequired()
608 608 def goto_switcher_data(self):
609 609 c = self.load_default_context()
610 610
611 611 _ = self.request.translate
612 612
613 613 query = self.request.GET.get('query')
614 614 log.debug('generating main filter data, query %s', query)
615 615
616 616 res = []
617 617 if not query:
618 618 return {'suggestions': res}
619 619
620 620 def no_match(name):
621 621 return {
622 622 'id': -1,
623 623 'value': "",
624 624 'value_display': name,
625 625 'type': 'text',
626 626 'url': ""
627 627 }
628 628 searcher = searcher_from_config(self.request.registry.settings)
629 629 has_specialized_search = False
630 630
631 631 # set repo context
632 632 repo = None
633 633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
634 634 if repo_id:
635 635 repo = Repository.get(repo_id)
636 636
637 637 # set group context
638 638 repo_group = None
639 639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
640 640 if repo_group_id:
641 641 repo_group = RepoGroup.get(repo_group_id)
642 642 prefix_match = False
643 643
644 644 # user: type search
645 645 if not prefix_match:
646 646 users, prefix_match = self._get_user_list(query)
647 647 if users:
648 648 has_specialized_search = True
649 649 for serialized_user in users:
650 650 res.append(serialized_user)
651 651 elif prefix_match:
652 652 has_specialized_search = True
653 653 res.append(no_match('No matching users found'))
654 654
655 655 # user_group: type search
656 656 if not prefix_match:
657 657 user_groups, prefix_match = self._get_user_groups_list(query)
658 658 if user_groups:
659 659 has_specialized_search = True
660 660 for serialized_user_group in user_groups:
661 661 res.append(serialized_user_group)
662 662 elif prefix_match:
663 663 has_specialized_search = True
664 664 res.append(no_match('No matching user groups found'))
665 665
666 666 # pr: type search
667 667 if not prefix_match:
668 668 pull_requests, prefix_match = self._get_pull_request_list(query)
669 669 if pull_requests:
670 670 has_specialized_search = True
671 671 for serialized_pull_request in pull_requests:
672 672 res.append(serialized_pull_request)
673 673 elif prefix_match:
674 674 has_specialized_search = True
675 675 res.append(no_match('No matching pull requests found'))
676 676
677 677 # FTS commit: type search
678 678 if not prefix_match:
679 679 commits, prefix_match = self._get_hash_commit_list(
680 680 c.auth_user, searcher, query, repo, repo_group)
681 681 if commits:
682 682 has_specialized_search = True
683 683 unique_repos = collections.OrderedDict()
684 684 for commit in commits:
685 685 repo_name = commit['repo']
686 686 unique_repos.setdefault(repo_name, []).append(commit)
687 687
688 688 for _repo, commits in unique_repos.items():
689 689 for commit in commits:
690 690 res.append(commit)
691 691 elif prefix_match:
692 692 has_specialized_search = True
693 693 res.append(no_match('No matching commits found'))
694 694
695 695 # FTS file: type search
696 696 if not prefix_match:
697 697 paths, prefix_match = self._get_path_list(
698 698 c.auth_user, searcher, query, repo, repo_group)
699 699 if paths:
700 700 has_specialized_search = True
701 701 unique_repos = collections.OrderedDict()
702 702 for path in paths:
703 703 repo_name = path['repo']
704 704 unique_repos.setdefault(repo_name, []).append(path)
705 705
706 706 for repo, paths in unique_repos.items():
707 707 for path in paths:
708 708 res.append(path)
709 709 elif prefix_match:
710 710 has_specialized_search = True
711 711 res.append(no_match('No matching files found'))
712 712
713 713 # main suggestions
714 714 if not has_specialized_search:
715 715 repo_group_name = ''
716 716 if repo_group:
717 717 repo_group_name = repo_group.group_name
718 718
719 719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
720 720 res.append(_q)
721 721
722 722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
723 723 for serialized_repo_group in repo_groups:
724 724 res.append(serialized_repo_group)
725 725
726 726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
727 727 for serialized_repo in repos:
728 728 res.append(serialized_repo)
729 729
730 730 if not repos and not repo_groups:
731 731 res.append(no_match('No matches found'))
732 732
733 733 return {'suggestions': res}
734 734
735 735 @LoginRequired()
736 736 def main_page(self):
737 737 c = self.load_default_context()
738 738 c.repo_group = None
739 739 return self._get_template_context(c)
740 740
741 741 def _main_page_repo_groups_data(self, repo_group_id):
742 742 column_map = {
743 743 'name': 'group_name_hash',
744 744 'desc': 'group_description',
745 745 'last_change': 'updated_on',
746 746 'owner': 'user_username',
747 747 }
748 748 draw, start, limit = self._extract_chunk(self.request)
749 749 search_q, order_by, order_dir = self._extract_ordering(
750 750 self.request, column_map=column_map)
751 751 return RepoGroupModel().get_repo_groups_data_table(
752 752 draw, start, limit,
753 753 search_q, order_by, order_dir,
754 754 self._rhodecode_user, repo_group_id)
755 755
756 756 def _main_page_repos_data(self, repo_group_id):
757 757 column_map = {
758 758 'name': 'repo_name',
759 759 'desc': 'description',
760 760 'last_change': 'updated_on',
761 761 'owner': 'user_username',
762 762 }
763 763 draw, start, limit = self._extract_chunk(self.request)
764 764 search_q, order_by, order_dir = self._extract_ordering(
765 765 self.request, column_map=column_map)
766 766 return RepoModel().get_repos_data_table(
767 767 draw, start, limit,
768 768 search_q, order_by, order_dir,
769 769 self._rhodecode_user, repo_group_id)
770 770
771 771 @LoginRequired()
772 772 def main_page_repo_groups_data(self):
773 773 self.load_default_context()
774 774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
775 775
776 776 if repo_group_id:
777 777 group = RepoGroup.get_or_404(repo_group_id)
778 778 _perms = AuthUser.repo_group_read_perms
779 779 if not HasRepoGroupPermissionAny(*_perms)(
780 780 group.group_name, 'user is allowed to list repo group children'):
781 781 raise HTTPNotFound()
782 782
783 783 return self._main_page_repo_groups_data(repo_group_id)
784 784
785 785 @LoginRequired()
786 786 def main_page_repos_data(self):
787 787 self.load_default_context()
788 788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
789 789
790 790 if repo_group_id:
791 791 group = RepoGroup.get_or_404(repo_group_id)
792 792 _perms = AuthUser.repo_group_read_perms
793 793 if not HasRepoGroupPermissionAny(*_perms)(
794 794 group.group_name, 'user is allowed to list repo group children'):
795 795 raise HTTPNotFound()
796 796
797 797 return self._main_page_repos_data(repo_group_id)
798 798
799 799 @LoginRequired()
800 800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
801 801 def repo_group_main_page(self):
802 802 c = self.load_default_context()
803 803 c.repo_group = self.request.db_repo_group
804 804 return self._get_template_context(c)
805 805
806 806 @LoginRequired()
807 807 @CSRFRequired()
808 808 def markup_preview(self):
809 809 # Technically a CSRF token is not needed as no state changes with this
810 810 # call. However, as this is a POST is better to have it, so automated
811 811 # tools don't flag it as potential CSRF.
812 812 # Post is required because the payload could be bigger than the maximum
813 813 # allowed by GET.
814 814
815 815 text = self.request.POST.get('text')
816 816 renderer = self.request.POST.get('renderer') or 'rst'
817 817 if text:
818 818 return h.render(text, renderer=renderer, mentions=True)
819 819 return ''
820 820
821 821 @LoginRequired()
822 822 @CSRFRequired()
823 823 def file_preview(self):
824 824 # Technically a CSRF token is not needed as no state changes with this
825 825 # call. However, as this is a POST is better to have it, so automated
826 826 # tools don't flag it as potential CSRF.
827 827 # Post is required because the payload could be bigger than the maximum
828 828 # allowed by GET.
829 829
830 830 text = self.request.POST.get('text')
831 831 file_path = self.request.POST.get('file_path')
832 832
833 833 renderer = h.renderer_from_filename(file_path)
834 834
835 835 if renderer:
836 836 return h.render(text, renderer=renderer, mentions=True)
837 837 else:
838 838 self.load_default_context()
839 839 _render = self.request.get_partial_renderer(
840 840 'rhodecode:templates/files/file_content.mako')
841 841
842 842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
843 843
844 844 return _render('render_lines', lines)
845 845
846 846 @LoginRequired()
847 847 @CSRFRequired()
848 848 def store_user_session_attr(self):
849 849 key = self.request.POST.get('key')
850 850 val = self.request.POST.get('val')
851 851
852 852 existing_value = self.request.session.get(key)
853 853 if existing_value != val:
854 854 self.request.session[key] = val
855 855
856 856 return 'stored:{}:{}'.format(key, val)
@@ -1,188 +1,187 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import deform
23 23
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode import events
28 28 from rhodecode.apps._base import RepoGroupAppView
29 29 from rhodecode.forms import RcForm
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAll,
34 34 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
35 35 from rhodecode.model.db import Session, RepoGroup, User
36 36 from rhodecode.model.permission import PermissionModel
37 37 from rhodecode.model.scm import RepoGroupList
38 38 from rhodecode.model.repo_group import RepoGroupModel
39 39 from rhodecode.model.validation_schema.schemas import repo_group_schema
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class RepoGroupSettingsView(RepoGroupAppView):
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 c.repo_group = self.db_repo_group
48 48 no_parrent = not c.repo_group.parent_group
49 49 can_create_in_root = self._can_create_repo_group()
50 50
51 51 show_root_location = False
52 52 if no_parrent or can_create_in_root:
53 53 # we're global admin, we're ok and we can create TOP level groups
54 54 # or in case this group is already at top-level we also allow
55 55 # creation in root
56 56 show_root_location = True
57 57
58 58 acl_groups = RepoGroupList(
59 59 RepoGroup.query().all(),
60 60 perm_set=['group.admin'])
61 61 c.repo_groups = RepoGroup.groups_choices(
62 62 groups=acl_groups,
63 63 show_empty_group=show_root_location)
64 64 # filter out current repo group
65 65 exclude_group_ids = [c.repo_group.group_id]
66 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
67 c.repo_groups)
68 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
66 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]
69 68
70 69 parent_group = c.repo_group.parent_group
71 70
72 71 add_parent_group = (parent_group and (
73 72 parent_group.group_id not in c.repo_groups_choices))
74 73 if add_parent_group:
75 74 c.repo_groups_choices.append(parent_group.group_id)
76 75 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
77 76 return c
78 77
79 78 def _can_create_repo_group(self, parent_group_id=None):
80 79 is_admin = HasPermissionAll('hg.admin')('group create controller')
81 80 create_repo_group = HasPermissionAll(
82 81 'hg.repogroup.create.true')('group create controller')
83 82 if is_admin or (create_repo_group and not parent_group_id):
84 83 # we're global admin, or we have global repo group create
85 84 # permission
86 85 # we're ok and we can create TOP level groups
87 86 return True
88 87 elif parent_group_id:
89 88 # we check the permission if we can write to parent group
90 89 group = RepoGroup.get(parent_group_id)
91 90 group_name = group.group_name if group else None
92 91 if HasRepoGroupPermissionAny('group.admin')(
93 92 group_name, 'check if user is an admin of group'):
94 93 # we're an admin of passed in group, we're ok.
95 94 return True
96 95 else:
97 96 return False
98 97 return False
99 98
100 99 def _get_schema(self, c, old_values=None):
101 100 return repo_group_schema.RepoGroupSettingsSchema().bind(
102 101 repo_group_repo_group_options=c.repo_groups_choices,
103 102 repo_group_repo_group_items=c.repo_groups,
104 103
105 104 # user caller
106 105 user=self._rhodecode_user,
107 106 old_values=old_values
108 107 )
109 108
110 109 @LoginRequired()
111 110 @HasRepoGroupPermissionAnyDecorator('group.admin')
112 111 def edit_settings(self):
113 112 c = self.load_default_context()
114 113 c.active = 'settings'
115 114
116 115 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
117 116 defaults['repo_group_owner'] = defaults['user']
118 117
119 118 schema = self._get_schema(c)
120 119 c.form = RcForm(schema, appstruct=defaults)
121 120 return self._get_template_context(c)
122 121
123 122 @LoginRequired()
124 123 @HasRepoGroupPermissionAnyDecorator('group.admin')
125 124 @CSRFRequired()
126 125 def edit_settings_update(self):
127 126 _ = self.request.translate
128 127 c = self.load_default_context()
129 128 c.active = 'settings'
130 129
131 130 old_repo_group_name = self.db_repo_group_name
132 131 new_repo_group_name = old_repo_group_name
133 132
134 133 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
135 134 schema = self._get_schema(c, old_values=old_values)
136 135
137 136 c.form = RcForm(schema)
138 pstruct = self.request.POST.items()
137 pstruct = list(self.request.POST.items())
139 138
140 139 try:
141 140 schema_data = c.form.validate(pstruct)
142 141 except deform.ValidationFailure as err_form:
143 142 return self._get_template_context(c)
144 143
145 144 # data is now VALID, proceed with updates
146 145 # save validated data back into the updates dict
147 146 validated_updates = dict(
148 147 group_name=schema_data['repo_group']['repo_group_name_without_group'],
149 148 group_parent_id=schema_data['repo_group']['repo_group_id'],
150 149 user=schema_data['repo_group_owner'],
151 150 group_description=schema_data['repo_group_description'],
152 151 enable_locking=schema_data['repo_group_enable_locking'],
153 152 )
154 153
155 154 try:
156 155 RepoGroupModel().update(self.db_repo_group, validated_updates)
157 156
158 157 audit_logger.store_web(
159 158 'repo_group.edit', action_data={'old_data': old_values},
160 159 user=c.rhodecode_user)
161 160
162 161 Session().commit()
163 162
164 163 # use the new full name for redirect once we know we updated
165 164 # the name on filesystem and in DB
166 165 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
167 166
168 167 h.flash(_('Repository Group `{}` updated successfully').format(
169 168 old_repo_group_name), category='success')
170 169
171 170 except Exception:
172 171 log.exception("Exception during update or repository group")
173 172 h.flash(_('Error occurred during update of repository group %s')
174 173 % old_repo_group_name, category='error')
175 174
176 175 name_changed = old_repo_group_name != new_repo_group_name
177 176 if name_changed:
178 177 current_perms = self.db_repo_group.permissions(expand_from_user_groups=True)
179 178 affected_user_ids = [perm['user_id'] for perm in current_perms]
180 179
181 180 # NOTE(marcink): also add owner maybe it has changed
182 181 owner = User.get_by_username(schema_data['repo_group_owner'])
183 182 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
184 183 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
185 184 PermissionModel().trigger_permission_flush(affected_user_ids)
186 185
187 186 raise HTTPFound(
188 187 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
@@ -1,1227 +1,1227 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24 from rhodecode.apps.repository.views.repo_artifacts import RepoArtifactsView
25 25 from rhodecode.apps.repository.views.repo_audit_logs import AuditLogsView
26 26 from rhodecode.apps.repository.views.repo_automation import RepoAutomationView
27 27 from rhodecode.apps.repository.views.repo_bookmarks import RepoBookmarksView
28 28 from rhodecode.apps.repository.views.repo_branch_permissions import RepoSettingsBranchPermissionsView
29 29 from rhodecode.apps.repository.views.repo_branches import RepoBranchesView
30 30 from rhodecode.apps.repository.views.repo_caches import RepoCachesView
31 31 from rhodecode.apps.repository.views.repo_changelog import RepoChangelogView
32 32 from rhodecode.apps.repository.views.repo_checks import RepoChecksView
33 33 from rhodecode.apps.repository.views.repo_commits import RepoCommitsView
34 34 from rhodecode.apps.repository.views.repo_compare import RepoCompareView
35 35 from rhodecode.apps.repository.views.repo_feed import RepoFeedView
36 36 from rhodecode.apps.repository.views.repo_files import RepoFilesView
37 37 from rhodecode.apps.repository.views.repo_forks import RepoForksView
38 38 from rhodecode.apps.repository.views.repo_maintainance import RepoMaintenanceView
39 39 from rhodecode.apps.repository.views.repo_permissions import RepoSettingsPermissionsView
40 40 from rhodecode.apps.repository.views.repo_pull_requests import RepoPullRequestsView
41 41 from rhodecode.apps.repository.views.repo_review_rules import RepoReviewRulesView
42 42 from rhodecode.apps.repository.views.repo_settings import RepoSettingsView
43 43 from rhodecode.apps.repository.views.repo_settings_advanced import RepoSettingsAdvancedView
44 44 from rhodecode.apps.repository.views.repo_settings_fields import RepoSettingsFieldsView
45 45 from rhodecode.apps.repository.views.repo_settings_issue_trackers import RepoSettingsIssueTrackersView
46 46 from rhodecode.apps.repository.views.repo_settings_remote import RepoSettingsRemoteView
47 47 from rhodecode.apps.repository.views.repo_settings_vcs import RepoSettingsVcsView
48 48 from rhodecode.apps.repository.views.repo_strip import RepoStripView
49 49 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
50 50 from rhodecode.apps.repository.views.repo_tags import RepoTagsView
51 51
52 52 # repo creating checks, special cases that aren't repo routes
53 53 config.add_route(
54 54 name='repo_creating',
55 55 pattern='/{repo_name:.*?[^/]}/repo_creating')
56 56 config.add_view(
57 57 RepoChecksView,
58 58 attr='repo_creating',
59 59 route_name='repo_creating', request_method='GET',
60 60 renderer='rhodecode:templates/admin/repos/repo_creating.mako')
61 61
62 62 config.add_route(
63 63 name='repo_creating_check',
64 64 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
65 65 config.add_view(
66 66 RepoChecksView,
67 67 attr='repo_creating_check',
68 68 route_name='repo_creating_check', request_method='GET',
69 69 renderer='json_ext')
70 70
71 71 # Summary
72 72 # NOTE(marcink): one additional route is defined in very bottom, catch
73 73 # all pattern
74 74 config.add_route(
75 75 name='repo_summary_explicit',
76 76 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
77 77 config.add_view(
78 78 RepoSummaryView,
79 79 attr='summary',
80 80 route_name='repo_summary_explicit', request_method='GET',
81 81 renderer='rhodecode:templates/summary/summary.mako')
82 82
83 83 config.add_route(
84 84 name='repo_summary_commits',
85 85 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
86 86 config.add_view(
87 87 RepoSummaryView,
88 88 attr='summary_commits',
89 89 route_name='repo_summary_commits', request_method='GET',
90 90 renderer='rhodecode:templates/summary/summary_commits.mako')
91 91
92 92 # Commits
93 93 config.add_route(
94 94 name='repo_commit',
95 95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
96 96 config.add_view(
97 97 RepoCommitsView,
98 98 attr='repo_commit_show',
99 99 route_name='repo_commit', request_method='GET',
100 100 renderer=None)
101 101
102 102 config.add_route(
103 103 name='repo_commit_children',
104 104 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
105 105 config.add_view(
106 106 RepoCommitsView,
107 107 attr='repo_commit_children',
108 108 route_name='repo_commit_children', request_method='GET',
109 109 renderer='json_ext', xhr=True)
110 110
111 111 config.add_route(
112 112 name='repo_commit_parents',
113 113 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
114 114 config.add_view(
115 115 RepoCommitsView,
116 116 attr='repo_commit_parents',
117 117 route_name='repo_commit_parents', request_method='GET',
118 118 renderer='json_ext')
119 119
120 120 config.add_route(
121 121 name='repo_commit_raw',
122 122 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
123 123 config.add_view(
124 124 RepoCommitsView,
125 125 attr='repo_commit_raw',
126 126 route_name='repo_commit_raw', request_method='GET',
127 127 renderer=None)
128 128
129 129 config.add_route(
130 130 name='repo_commit_patch',
131 131 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
132 132 config.add_view(
133 133 RepoCommitsView,
134 134 attr='repo_commit_patch',
135 135 route_name='repo_commit_patch', request_method='GET',
136 136 renderer=None)
137 137
138 138 config.add_route(
139 139 name='repo_commit_download',
140 140 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
141 141 config.add_view(
142 142 RepoCommitsView,
143 143 attr='repo_commit_download',
144 144 route_name='repo_commit_download', request_method='GET',
145 145 renderer=None)
146 146
147 147 config.add_route(
148 148 name='repo_commit_data',
149 149 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
150 150 config.add_view(
151 151 RepoCommitsView,
152 152 attr='repo_commit_data',
153 153 route_name='repo_commit_data', request_method='GET',
154 154 renderer='json_ext', xhr=True)
155 155
156 156 config.add_route(
157 157 name='repo_commit_comment_create',
158 158 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
159 159 config.add_view(
160 160 RepoCommitsView,
161 161 attr='repo_commit_comment_create',
162 162 route_name='repo_commit_comment_create', request_method='POST',
163 163 renderer='json_ext')
164 164
165 165 config.add_route(
166 166 name='repo_commit_comment_preview',
167 167 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
168 168 config.add_view(
169 169 RepoCommitsView,
170 170 attr='repo_commit_comment_preview',
171 171 route_name='repo_commit_comment_preview', request_method='POST',
172 172 renderer='string', xhr=True)
173 173
174 174 config.add_route(
175 175 name='repo_commit_comment_history_view',
176 176 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/history_view/{comment_history_id}', repo_route=True)
177 177 config.add_view(
178 178 RepoCommitsView,
179 179 attr='repo_commit_comment_history_view',
180 180 route_name='repo_commit_comment_history_view', request_method='POST',
181 181 renderer='string', xhr=True)
182 182
183 183 config.add_route(
184 184 name='repo_commit_comment_attachment_upload',
185 185 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
186 186 config.add_view(
187 187 RepoCommitsView,
188 188 attr='repo_commit_comment_attachment_upload',
189 189 route_name='repo_commit_comment_attachment_upload', request_method='POST',
190 190 renderer='json_ext', xhr=True)
191 191
192 192 config.add_route(
193 193 name='repo_commit_comment_delete',
194 194 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
195 195 config.add_view(
196 196 RepoCommitsView,
197 197 attr='repo_commit_comment_delete',
198 198 route_name='repo_commit_comment_delete', request_method='POST',
199 199 renderer='json_ext')
200 200
201 201 config.add_route(
202 202 name='repo_commit_comment_edit',
203 203 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
204 204 config.add_view(
205 205 RepoCommitsView,
206 206 attr='repo_commit_comment_edit',
207 207 route_name='repo_commit_comment_edit', request_method='POST',
208 208 renderer='json_ext')
209 209
210 210 # still working url for backward compat.
211 211 config.add_route(
212 212 name='repo_commit_raw_deprecated',
213 213 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
214 214 config.add_view(
215 215 RepoCommitsView,
216 216 attr='repo_commit_raw',
217 217 route_name='repo_commit_raw_deprecated', request_method='GET',
218 218 renderer=None)
219 219
220 220 # Files
221 221 config.add_route(
222 222 name='repo_archivefile',
223 223 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
224 224 config.add_view(
225 225 RepoFilesView,
226 226 attr='repo_archivefile',
227 227 route_name='repo_archivefile', request_method='GET',
228 228 renderer=None)
229 229
230 230 config.add_route(
231 231 name='repo_files_diff',
232 232 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
233 233 config.add_view(
234 234 RepoFilesView,
235 235 attr='repo_files_diff',
236 236 route_name='repo_files_diff', request_method='GET',
237 237 renderer=None)
238 238
239 239 config.add_route( # legacy route to make old links work
240 240 name='repo_files_diff_2way_redirect',
241 241 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
242 242 config.add_view(
243 243 RepoFilesView,
244 244 attr='repo_files_diff_2way_redirect',
245 245 route_name='repo_files_diff_2way_redirect', request_method='GET',
246 246 renderer=None)
247 247
248 248 config.add_route(
249 249 name='repo_files',
250 250 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
251 251 config.add_view(
252 252 RepoFilesView,
253 253 attr='repo_files',
254 254 route_name='repo_files', request_method='GET',
255 255 renderer=None)
256 256
257 257 config.add_route(
258 258 name='repo_files:default_path',
259 259 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
260 260 config.add_view(
261 261 RepoFilesView,
262 262 attr='repo_files',
263 263 route_name='repo_files:default_path', request_method='GET',
264 264 renderer=None)
265 265
266 266 config.add_route(
267 267 name='repo_files:default_commit',
268 268 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
269 269 config.add_view(
270 270 RepoFilesView,
271 271 attr='repo_files',
272 272 route_name='repo_files:default_commit', request_method='GET',
273 273 renderer=None)
274 274
275 275 config.add_route(
276 276 name='repo_files:rendered',
277 277 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
278 278 config.add_view(
279 279 RepoFilesView,
280 280 attr='repo_files',
281 281 route_name='repo_files:rendered', request_method='GET',
282 282 renderer=None)
283 283
284 284 config.add_route(
285 285 name='repo_files:annotated',
286 286 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
287 287 config.add_view(
288 288 RepoFilesView,
289 289 attr='repo_files',
290 290 route_name='repo_files:annotated', request_method='GET',
291 291 renderer=None)
292 292
293 293 config.add_route(
294 294 name='repo_files:annotated_previous',
295 295 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
296 296 config.add_view(
297 297 RepoFilesView,
298 298 attr='repo_files_annotated_previous',
299 299 route_name='repo_files:annotated_previous', request_method='GET',
300 300 renderer=None)
301 301
302 302 config.add_route(
303 303 name='repo_nodetree_full',
304 304 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
305 305 config.add_view(
306 306 RepoFilesView,
307 307 attr='repo_nodetree_full',
308 308 route_name='repo_nodetree_full', request_method='GET',
309 309 renderer=None, xhr=True)
310 310
311 311 config.add_route(
312 312 name='repo_nodetree_full:default_path',
313 313 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
314 314 config.add_view(
315 315 RepoFilesView,
316 316 attr='repo_nodetree_full',
317 317 route_name='repo_nodetree_full:default_path', request_method='GET',
318 318 renderer=None, xhr=True)
319 319
320 320 config.add_route(
321 321 name='repo_files_nodelist',
322 322 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
323 323 config.add_view(
324 324 RepoFilesView,
325 325 attr='repo_nodelist',
326 326 route_name='repo_files_nodelist', request_method='GET',
327 327 renderer='json_ext', xhr=True)
328 328
329 329 config.add_route(
330 330 name='repo_file_raw',
331 331 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
332 332 config.add_view(
333 333 RepoFilesView,
334 334 attr='repo_file_raw',
335 335 route_name='repo_file_raw', request_method='GET',
336 336 renderer=None)
337 337
338 338 config.add_route(
339 339 name='repo_file_download',
340 340 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
341 341 config.add_view(
342 342 RepoFilesView,
343 343 attr='repo_file_download',
344 344 route_name='repo_file_download', request_method='GET',
345 345 renderer=None)
346 346
347 347 config.add_route( # backward compat to keep old links working
348 348 name='repo_file_download:legacy',
349 349 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
350 350 repo_route=True)
351 351 config.add_view(
352 352 RepoFilesView,
353 353 attr='repo_file_download',
354 354 route_name='repo_file_download:legacy', request_method='GET',
355 355 renderer=None)
356 356
357 357 config.add_route(
358 358 name='repo_file_history',
359 359 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
360 360 config.add_view(
361 361 RepoFilesView,
362 362 attr='repo_file_history',
363 363 route_name='repo_file_history', request_method='GET',
364 364 renderer='json_ext')
365 365
366 366 config.add_route(
367 367 name='repo_file_authors',
368 368 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
369 369 config.add_view(
370 370 RepoFilesView,
371 371 attr='repo_file_authors',
372 372 route_name='repo_file_authors', request_method='GET',
373 373 renderer='rhodecode:templates/files/file_authors_box.mako')
374 374
375 375 config.add_route(
376 376 name='repo_files_check_head',
377 377 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
378 378 repo_route=True)
379 379 config.add_view(
380 380 RepoFilesView,
381 381 attr='repo_files_check_head',
382 382 route_name='repo_files_check_head', request_method='POST',
383 383 renderer='json_ext', xhr=True)
384 384
385 385 config.add_route(
386 386 name='repo_files_remove_file',
387 387 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
388 388 repo_route=True)
389 389 config.add_view(
390 390 RepoFilesView,
391 391 attr='repo_files_remove_file',
392 392 route_name='repo_files_remove_file', request_method='GET',
393 393 renderer='rhodecode:templates/files/files_delete.mako')
394 394
395 395 config.add_route(
396 396 name='repo_files_delete_file',
397 397 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
398 398 repo_route=True)
399 399 config.add_view(
400 400 RepoFilesView,
401 401 attr='repo_files_delete_file',
402 402 route_name='repo_files_delete_file', request_method='POST',
403 403 renderer=None)
404 404
405 405 config.add_route(
406 406 name='repo_files_edit_file',
407 407 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
408 408 repo_route=True)
409 409 config.add_view(
410 410 RepoFilesView,
411 411 attr='repo_files_edit_file',
412 412 route_name='repo_files_edit_file', request_method='GET',
413 413 renderer='rhodecode:templates/files/files_edit.mako')
414 414
415 415 config.add_route(
416 416 name='repo_files_update_file',
417 417 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
418 418 repo_route=True)
419 419 config.add_view(
420 420 RepoFilesView,
421 421 attr='repo_files_update_file',
422 422 route_name='repo_files_update_file', request_method='POST',
423 423 renderer=None)
424 424
425 425 config.add_route(
426 426 name='repo_files_add_file',
427 427 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
428 428 repo_route=True)
429 429 config.add_view(
430 430 RepoFilesView,
431 431 attr='repo_files_add_file',
432 432 route_name='repo_files_add_file', request_method='GET',
433 433 renderer='rhodecode:templates/files/files_add.mako')
434 434
435 435 config.add_route(
436 436 name='repo_files_upload_file',
437 437 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
438 438 repo_route=True)
439 439 config.add_view(
440 440 RepoFilesView,
441 441 attr='repo_files_add_file',
442 442 route_name='repo_files_upload_file', request_method='GET',
443 443 renderer='rhodecode:templates/files/files_upload.mako')
444 444 config.add_view( # POST creates
445 445 RepoFilesView,
446 446 attr='repo_files_upload_file',
447 447 route_name='repo_files_upload_file', request_method='POST',
448 448 renderer='json_ext')
449 449
450 450 config.add_route(
451 451 name='repo_files_create_file',
452 452 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
453 453 repo_route=True)
454 454 config.add_view( # POST creates
455 455 RepoFilesView,
456 456 attr='repo_files_create_file',
457 457 route_name='repo_files_create_file', request_method='POST',
458 458 renderer=None)
459 459
460 460 # Refs data
461 461 config.add_route(
462 462 name='repo_refs_data',
463 463 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
464 464 config.add_view(
465 465 RepoSummaryView,
466 466 attr='repo_refs_data',
467 467 route_name='repo_refs_data', request_method='GET',
468 468 renderer='json_ext')
469 469
470 470 config.add_route(
471 471 name='repo_refs_changelog_data',
472 472 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
473 473 config.add_view(
474 474 RepoSummaryView,
475 475 attr='repo_refs_changelog_data',
476 476 route_name='repo_refs_changelog_data', request_method='GET',
477 477 renderer='json_ext')
478 478
479 479 config.add_route(
480 480 name='repo_stats',
481 481 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
482 482 config.add_view(
483 483 RepoSummaryView,
484 484 attr='repo_stats',
485 485 route_name='repo_stats', request_method='GET',
486 486 renderer='json_ext')
487 487
488 488 # Commits
489 489 config.add_route(
490 490 name='repo_commits',
491 491 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
492 492 config.add_view(
493 493 RepoChangelogView,
494 494 attr='repo_changelog',
495 495 route_name='repo_commits', request_method='GET',
496 496 renderer='rhodecode:templates/commits/changelog.mako')
497 497 # old routes for backward compat
498 498 config.add_view(
499 499 RepoChangelogView,
500 500 attr='repo_changelog',
501 501 route_name='repo_changelog', request_method='GET',
502 502 renderer='rhodecode:templates/commits/changelog.mako')
503 503
504 504 config.add_route(
505 505 name='repo_commits_elements',
506 506 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
507 507 config.add_view(
508 508 RepoChangelogView,
509 509 attr='repo_commits_elements',
510 510 route_name='repo_commits_elements', request_method=('GET', 'POST'),
511 511 renderer='rhodecode:templates/commits/changelog_elements.mako',
512 512 xhr=True)
513 513
514 514 config.add_route(
515 515 name='repo_commits_elements_file',
516 516 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
517 517 config.add_view(
518 518 RepoChangelogView,
519 519 attr='repo_commits_elements',
520 520 route_name='repo_commits_elements_file', request_method=('GET', 'POST'),
521 521 renderer='rhodecode:templates/commits/changelog_elements.mako',
522 522 xhr=True)
523 523
524 524 config.add_route(
525 525 name='repo_commits_file',
526 526 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
527 527 config.add_view(
528 528 RepoChangelogView,
529 529 attr='repo_changelog',
530 530 route_name='repo_commits_file', request_method='GET',
531 531 renderer='rhodecode:templates/commits/changelog.mako')
532 532 # old routes for backward compat
533 533 config.add_view(
534 534 RepoChangelogView,
535 535 attr='repo_changelog',
536 536 route_name='repo_changelog_file', request_method='GET',
537 537 renderer='rhodecode:templates/commits/changelog.mako')
538 538
539 539 # Changelog (old deprecated name for commits page)
540 540 config.add_route(
541 541 name='repo_changelog',
542 542 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
543 543 config.add_route(
544 544 name='repo_changelog_file',
545 545 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
546 546
547 547 # Compare
548 548 config.add_route(
549 549 name='repo_compare_select',
550 550 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
551 551 config.add_view(
552 552 RepoCompareView,
553 553 attr='compare_select',
554 554 route_name='repo_compare_select', request_method='GET',
555 555 renderer='rhodecode:templates/compare/compare_diff.mako')
556 556
557 557 config.add_route(
558 558 name='repo_compare',
559 559 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
560 560 config.add_view(
561 561 RepoCompareView,
562 562 attr='compare',
563 563 route_name='repo_compare', request_method='GET',
564 564 renderer=None)
565 565
566 566 # Tags
567 567 config.add_route(
568 568 name='tags_home',
569 569 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
570 570 config.add_view(
571 571 RepoTagsView,
572 572 attr='tags',
573 573 route_name='tags_home', request_method='GET',
574 574 renderer='rhodecode:templates/tags/tags.mako')
575 575
576 576 # Branches
577 577 config.add_route(
578 578 name='branches_home',
579 579 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
580 580 config.add_view(
581 581 RepoBranchesView,
582 582 attr='branches',
583 583 route_name='branches_home', request_method='GET',
584 584 renderer='rhodecode:templates/branches/branches.mako')
585 585
586 586 # Bookmarks
587 587 config.add_route(
588 588 name='bookmarks_home',
589 589 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
590 590 config.add_view(
591 591 RepoBookmarksView,
592 592 attr='bookmarks',
593 593 route_name='bookmarks_home', request_method='GET',
594 594 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
595 595
596 596 # Forks
597 597 config.add_route(
598 598 name='repo_fork_new',
599 599 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
600 600 repo_forbid_when_archived=True,
601 601 repo_accepted_types=['hg', 'git'])
602 602 config.add_view(
603 603 RepoForksView,
604 604 attr='repo_fork_new',
605 605 route_name='repo_fork_new', request_method='GET',
606 606 renderer='rhodecode:templates/forks/forks.mako')
607 607
608 608 config.add_route(
609 609 name='repo_fork_create',
610 610 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
611 611 repo_forbid_when_archived=True,
612 612 repo_accepted_types=['hg', 'git'])
613 613 config.add_view(
614 614 RepoForksView,
615 615 attr='repo_fork_create',
616 616 route_name='repo_fork_create', request_method='POST',
617 617 renderer='rhodecode:templates/forks/fork.mako')
618 618
619 619 config.add_route(
620 620 name='repo_forks_show_all',
621 621 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
622 622 repo_accepted_types=['hg', 'git'])
623 623 config.add_view(
624 624 RepoForksView,
625 625 attr='repo_forks_show_all',
626 626 route_name='repo_forks_show_all', request_method='GET',
627 627 renderer='rhodecode:templates/forks/forks.mako')
628 628
629 629 config.add_route(
630 630 name='repo_forks_data',
631 631 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
632 632 repo_accepted_types=['hg', 'git'])
633 633 config.add_view(
634 634 RepoForksView,
635 635 attr='repo_forks_data',
636 636 route_name='repo_forks_data', request_method='GET',
637 637 renderer='json_ext', xhr=True)
638 638
639 639 # Pull Requests
640 640 config.add_route(
641 641 name='pullrequest_show',
642 642 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
643 643 repo_route=True)
644 644 config.add_view(
645 645 RepoPullRequestsView,
646 646 attr='pull_request_show',
647 647 route_name='pullrequest_show', request_method='GET',
648 648 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
649 649
650 650 config.add_route(
651 651 name='pullrequest_show_all',
652 652 pattern='/{repo_name:.*?[^/]}/pull-request',
653 653 repo_route=True, repo_accepted_types=['hg', 'git'])
654 654 config.add_view(
655 655 RepoPullRequestsView,
656 656 attr='pull_request_list',
657 657 route_name='pullrequest_show_all', request_method='GET',
658 658 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
659 659
660 660 config.add_route(
661 661 name='pullrequest_show_all_data',
662 662 pattern='/{repo_name:.*?[^/]}/pull-request-data',
663 663 repo_route=True, repo_accepted_types=['hg', 'git'])
664 664 config.add_view(
665 665 RepoPullRequestsView,
666 666 attr='pull_request_list_data',
667 667 route_name='pullrequest_show_all_data', request_method='GET',
668 668 renderer='json_ext', xhr=True)
669 669
670 670 config.add_route(
671 671 name='pullrequest_repo_refs',
672 672 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
673 673 repo_route=True)
674 674 config.add_view(
675 675 RepoPullRequestsView,
676 676 attr='pull_request_repo_refs',
677 677 route_name='pullrequest_repo_refs', request_method='GET',
678 678 renderer='json_ext', xhr=True)
679 679
680 680 config.add_route(
681 681 name='pullrequest_repo_targets',
682 682 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
683 683 repo_route=True)
684 684 config.add_view(
685 685 RepoPullRequestsView,
686 686 attr='pullrequest_repo_targets',
687 687 route_name='pullrequest_repo_targets', request_method='GET',
688 688 renderer='json_ext', xhr=True)
689 689
690 690 config.add_route(
691 691 name='pullrequest_new',
692 692 pattern='/{repo_name:.*?[^/]}/pull-request/new',
693 693 repo_route=True, repo_accepted_types=['hg', 'git'],
694 694 repo_forbid_when_archived=True)
695 695 config.add_view(
696 696 RepoPullRequestsView,
697 697 attr='pull_request_new',
698 698 route_name='pullrequest_new', request_method='GET',
699 699 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
700 700
701 701 config.add_route(
702 702 name='pullrequest_create',
703 703 pattern='/{repo_name:.*?[^/]}/pull-request/create',
704 704 repo_route=True, repo_accepted_types=['hg', 'git'],
705 705 repo_forbid_when_archived=True)
706 706 config.add_view(
707 707 RepoPullRequestsView,
708 708 attr='pull_request_create',
709 709 route_name='pullrequest_create', request_method='POST',
710 710 renderer=None)
711 711
712 712 config.add_route(
713 713 name='pullrequest_update',
714 714 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
715 715 repo_route=True, repo_forbid_when_archived=True)
716 716 config.add_view(
717 717 RepoPullRequestsView,
718 718 attr='pull_request_update',
719 719 route_name='pullrequest_update', request_method='POST',
720 720 renderer='json_ext')
721 721
722 722 config.add_route(
723 723 name='pullrequest_merge',
724 724 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
725 725 repo_route=True, repo_forbid_when_archived=True)
726 726 config.add_view(
727 727 RepoPullRequestsView,
728 728 attr='pull_request_merge',
729 729 route_name='pullrequest_merge', request_method='POST',
730 730 renderer='json_ext')
731 731
732 732 config.add_route(
733 733 name='pullrequest_delete',
734 734 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
735 735 repo_route=True, repo_forbid_when_archived=True)
736 736 config.add_view(
737 737 RepoPullRequestsView,
738 738 attr='pull_request_delete',
739 739 route_name='pullrequest_delete', request_method='POST',
740 740 renderer='json_ext')
741 741
742 742 config.add_route(
743 743 name='pullrequest_comment_create',
744 744 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
745 745 repo_route=True)
746 746 config.add_view(
747 747 RepoPullRequestsView,
748 748 attr='pull_request_comment_create',
749 749 route_name='pullrequest_comment_create', request_method='POST',
750 750 renderer='json_ext')
751 751
752 752 config.add_route(
753 753 name='pullrequest_comment_edit',
754 754 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
755 755 repo_route=True, repo_accepted_types=['hg', 'git'])
756 756 config.add_view(
757 757 RepoPullRequestsView,
758 758 attr='pull_request_comment_edit',
759 759 route_name='pullrequest_comment_edit', request_method='POST',
760 760 renderer='json_ext')
761 761
762 762 config.add_route(
763 763 name='pullrequest_comment_delete',
764 764 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
765 765 repo_route=True, repo_accepted_types=['hg', 'git'])
766 766 config.add_view(
767 767 RepoPullRequestsView,
768 768 attr='pull_request_comment_delete',
769 769 route_name='pullrequest_comment_delete', request_method='POST',
770 770 renderer='json_ext')
771 771
772 772 config.add_route(
773 773 name='pullrequest_comments',
774 774 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
775 775 repo_route=True)
776 776 config.add_view(
777 777 RepoPullRequestsView,
778 778 attr='pullrequest_comments',
779 779 route_name='pullrequest_comments', request_method='POST',
780 780 renderer='string_html', xhr=True)
781 781
782 782 config.add_route(
783 783 name='pullrequest_todos',
784 784 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
785 785 repo_route=True)
786 786 config.add_view(
787 787 RepoPullRequestsView,
788 788 attr='pullrequest_todos',
789 789 route_name='pullrequest_todos', request_method='POST',
790 790 renderer='string_html', xhr=True)
791 791
792 792 config.add_route(
793 793 name='pullrequest_drafts',
794 794 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
795 795 repo_route=True)
796 796 config.add_view(
797 797 RepoPullRequestsView,
798 798 attr='pullrequest_drafts',
799 799 route_name='pullrequest_drafts', request_method='POST',
800 800 renderer='string_html', xhr=True)
801 801
802 802 # Artifacts, (EE feature)
803 803 config.add_route(
804 804 name='repo_artifacts_list',
805 805 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
806 806 config.add_view(
807 807 RepoArtifactsView,
808 808 attr='repo_artifacts',
809 809 route_name='repo_artifacts_list', request_method='GET',
810 810 renderer='rhodecode:templates/artifacts/artifact_list.mako')
811 811
812 812 # Settings
813 813 config.add_route(
814 814 name='edit_repo',
815 815 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
816 816 config.add_view(
817 817 RepoSettingsView,
818 818 attr='edit_settings',
819 819 route_name='edit_repo', request_method='GET',
820 820 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
821 821 # update is POST on edit_repo
822 822 config.add_view(
823 823 RepoSettingsView,
824 824 attr='edit_settings_update',
825 825 route_name='edit_repo', request_method='POST',
826 826 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
827 827
828 828 # Settings advanced
829 829 config.add_route(
830 830 name='edit_repo_advanced',
831 831 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
832 832 config.add_view(
833 833 RepoSettingsAdvancedView,
834 834 attr='edit_advanced',
835 835 route_name='edit_repo_advanced', request_method='GET',
836 836 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
837 837
838 838 config.add_route(
839 839 name='edit_repo_advanced_archive',
840 840 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
841 841 config.add_view(
842 842 RepoSettingsAdvancedView,
843 843 attr='edit_advanced_archive',
844 844 route_name='edit_repo_advanced_archive', request_method='POST',
845 845 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
846 846
847 847 config.add_route(
848 848 name='edit_repo_advanced_delete',
849 849 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
850 850 config.add_view(
851 851 RepoSettingsAdvancedView,
852 852 attr='edit_advanced_delete',
853 853 route_name='edit_repo_advanced_delete', request_method='POST',
854 854 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
855 855
856 856 config.add_route(
857 857 name='edit_repo_advanced_locking',
858 858 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
859 859 config.add_view(
860 860 RepoSettingsAdvancedView,
861 861 attr='edit_advanced_toggle_locking',
862 862 route_name='edit_repo_advanced_locking', request_method='POST',
863 863 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
864 864
865 865 config.add_route(
866 866 name='edit_repo_advanced_journal',
867 867 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
868 868 config.add_view(
869 869 RepoSettingsAdvancedView,
870 870 attr='edit_advanced_journal',
871 871 route_name='edit_repo_advanced_journal', request_method='POST',
872 872 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
873 873
874 874 config.add_route(
875 875 name='edit_repo_advanced_fork',
876 876 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
877 877 config.add_view(
878 878 RepoSettingsAdvancedView,
879 879 attr='edit_advanced_fork',
880 880 route_name='edit_repo_advanced_fork', request_method='POST',
881 881 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
882 882
883 883 config.add_route(
884 884 name='edit_repo_advanced_hooks',
885 885 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
886 886 config.add_view(
887 887 RepoSettingsAdvancedView,
888 888 attr='edit_advanced_install_hooks',
889 889 route_name='edit_repo_advanced_hooks', request_method='GET',
890 890 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
891 891
892 892 # Caches
893 893 config.add_route(
894 894 name='edit_repo_caches',
895 895 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
896 896 config.add_view(
897 897 RepoCachesView,
898 898 attr='repo_caches',
899 899 route_name='edit_repo_caches', request_method='GET',
900 900 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
901 901 config.add_view(
902 902 RepoCachesView,
903 903 attr='repo_caches_purge',
904 904 route_name='edit_repo_caches', request_method='POST')
905 905
906 906 # Permissions
907 907 config.add_route(
908 908 name='edit_repo_perms',
909 909 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
910 910 config.add_view(
911 911 RepoSettingsPermissionsView,
912 912 attr='edit_permissions',
913 913 route_name='edit_repo_perms', request_method='GET',
914 914 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
915 915 config.add_view(
916 916 RepoSettingsPermissionsView,
917 917 attr='edit_permissions_update',
918 918 route_name='edit_repo_perms', request_method='POST',
919 919 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
920 920
921 921 config.add_route(
922 922 name='edit_repo_perms_set_private',
923 923 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
924 924 config.add_view(
925 925 RepoSettingsPermissionsView,
926 926 attr='edit_permissions_set_private_repo',
927 927 route_name='edit_repo_perms_set_private', request_method='POST',
928 928 renderer='json_ext')
929 929
930 930 # Permissions Branch (EE feature)
931 931 config.add_route(
932 932 name='edit_repo_perms_branch',
933 933 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
934 934 config.add_view(
935 935 RepoSettingsBranchPermissionsView,
936 936 attr='branch_permissions',
937 937 route_name='edit_repo_perms_branch', request_method='GET',
938 938 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
939 939
940 940 config.add_route(
941 941 name='edit_repo_perms_branch_delete',
942 942 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
943 943 repo_route=True)
944 944 ## Only implemented in EE
945 945
946 946 # Maintenance
947 947 config.add_route(
948 948 name='edit_repo_maintenance',
949 949 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
950 950 config.add_view(
951 951 RepoMaintenanceView,
952 952 attr='repo_maintenance',
953 953 route_name='edit_repo_maintenance', request_method='GET',
954 954 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
955 955
956 956 config.add_route(
957 957 name='edit_repo_maintenance_execute',
958 958 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
959 959 config.add_view(
960 960 RepoMaintenanceView,
961 961 attr='repo_maintenance_execute',
962 962 route_name='edit_repo_maintenance_execute', request_method='GET',
963 963 renderer='json', xhr=True)
964 964
965 965 # Fields
966 966 config.add_route(
967 967 name='edit_repo_fields',
968 968 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
969 969 config.add_view(
970 970 RepoSettingsFieldsView,
971 971 attr='repo_field_edit',
972 972 route_name='edit_repo_fields', request_method='GET',
973 973 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
974 974
975 975 config.add_route(
976 976 name='edit_repo_fields_create',
977 977 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
978 978 config.add_view(
979 979 RepoSettingsFieldsView,
980 980 attr='repo_field_create',
981 981 route_name='edit_repo_fields_create', request_method='POST',
982 982 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
983 983
984 984 config.add_route(
985 985 name='edit_repo_fields_delete',
986 986 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
987 987 config.add_view(
988 988 RepoSettingsFieldsView,
989 989 attr='repo_field_delete',
990 990 route_name='edit_repo_fields_delete', request_method='POST',
991 991 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
992 992
993 # Locking
993 # quick actions: locking
994 994 config.add_route(
995 name='repo_edit_toggle_locking',
996 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
995 name='repo_settings_quick_actions',
996 pattern='/{repo_name:.*?[^/]}/settings/quick-action', repo_route=True)
997 997 config.add_view(
998 998 RepoSettingsView,
999 attr='edit_advanced_toggle_locking',
1000 route_name='repo_edit_toggle_locking', request_method='GET',
999 attr='repo_settings_quick_actions',
1000 route_name='repo_settings_quick_actions', request_method='GET',
1001 1001 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1002 1002
1003 1003 # Remote
1004 1004 config.add_route(
1005 1005 name='edit_repo_remote',
1006 1006 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
1007 1007 config.add_view(
1008 1008 RepoSettingsRemoteView,
1009 1009 attr='repo_remote_edit_form',
1010 1010 route_name='edit_repo_remote', request_method='GET',
1011 1011 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1012 1012
1013 1013 config.add_route(
1014 1014 name='edit_repo_remote_pull',
1015 1015 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
1016 1016 config.add_view(
1017 1017 RepoSettingsRemoteView,
1018 1018 attr='repo_remote_pull_changes',
1019 1019 route_name='edit_repo_remote_pull', request_method='POST',
1020 1020 renderer=None)
1021 1021
1022 1022 config.add_route(
1023 1023 name='edit_repo_remote_push',
1024 1024 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
1025 1025
1026 1026 # Statistics
1027 1027 config.add_route(
1028 1028 name='edit_repo_statistics',
1029 1029 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
1030 1030 config.add_view(
1031 1031 RepoSettingsView,
1032 1032 attr='edit_statistics_form',
1033 1033 route_name='edit_repo_statistics', request_method='GET',
1034 1034 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1035 1035
1036 1036 config.add_route(
1037 1037 name='edit_repo_statistics_reset',
1038 1038 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
1039 1039 config.add_view(
1040 1040 RepoSettingsView,
1041 1041 attr='repo_statistics_reset',
1042 1042 route_name='edit_repo_statistics_reset', request_method='POST',
1043 1043 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1044 1044
1045 1045 # Issue trackers
1046 1046 config.add_route(
1047 1047 name='edit_repo_issuetracker',
1048 1048 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
1049 1049 config.add_view(
1050 1050 RepoSettingsIssueTrackersView,
1051 1051 attr='repo_issuetracker',
1052 1052 route_name='edit_repo_issuetracker', request_method='GET',
1053 1053 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1054 1054
1055 1055 config.add_route(
1056 1056 name='edit_repo_issuetracker_test',
1057 1057 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
1058 1058 config.add_view(
1059 1059 RepoSettingsIssueTrackersView,
1060 1060 attr='repo_issuetracker_test',
1061 1061 route_name='edit_repo_issuetracker_test', request_method='POST',
1062 1062 renderer='string', xhr=True)
1063 1063
1064 1064 config.add_route(
1065 1065 name='edit_repo_issuetracker_delete',
1066 1066 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
1067 1067 config.add_view(
1068 1068 RepoSettingsIssueTrackersView,
1069 1069 attr='repo_issuetracker_delete',
1070 1070 route_name='edit_repo_issuetracker_delete', request_method='POST',
1071 1071 renderer='json_ext', xhr=True)
1072 1072
1073 1073 config.add_route(
1074 1074 name='edit_repo_issuetracker_update',
1075 1075 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
1076 1076 config.add_view(
1077 1077 RepoSettingsIssueTrackersView,
1078 1078 attr='repo_issuetracker_update',
1079 1079 route_name='edit_repo_issuetracker_update', request_method='POST',
1080 1080 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1081 1081
1082 1082 # VCS Settings
1083 1083 config.add_route(
1084 1084 name='edit_repo_vcs',
1085 1085 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
1086 1086 config.add_view(
1087 1087 RepoSettingsVcsView,
1088 1088 attr='repo_vcs_settings',
1089 1089 route_name='edit_repo_vcs', request_method='GET',
1090 1090 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1091 1091
1092 1092 config.add_route(
1093 1093 name='edit_repo_vcs_update',
1094 1094 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
1095 1095 config.add_view(
1096 1096 RepoSettingsVcsView,
1097 1097 attr='repo_settings_vcs_update',
1098 1098 route_name='edit_repo_vcs_update', request_method='POST',
1099 1099 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1100 1100
1101 1101 # svn pattern
1102 1102 config.add_route(
1103 1103 name='edit_repo_vcs_svn_pattern_delete',
1104 1104 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
1105 1105 config.add_view(
1106 1106 RepoSettingsVcsView,
1107 1107 attr='repo_settings_delete_svn_pattern',
1108 1108 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
1109 1109 renderer='json_ext', xhr=True)
1110 1110
1111 1111 # Repo Review Rules (EE feature)
1112 1112 config.add_route(
1113 1113 name='repo_reviewers',
1114 1114 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
1115 1115 config.add_view(
1116 1116 RepoReviewRulesView,
1117 1117 attr='repo_review_rules',
1118 1118 route_name='repo_reviewers', request_method='GET',
1119 1119 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1120 1120
1121 1121 config.add_route(
1122 1122 name='repo_default_reviewers_data',
1123 1123 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
1124 1124 config.add_view(
1125 1125 RepoReviewRulesView,
1126 1126 attr='repo_default_reviewers_data',
1127 1127 route_name='repo_default_reviewers_data', request_method='GET',
1128 1128 renderer='json_ext')
1129 1129
1130 1130 # Repo Automation (EE feature)
1131 1131 config.add_route(
1132 1132 name='repo_automation',
1133 1133 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
1134 1134 config.add_view(
1135 1135 RepoAutomationView,
1136 1136 attr='repo_automation',
1137 1137 route_name='repo_automation', request_method='GET',
1138 1138 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1139 1139
1140 1140 # Strip
1141 1141 config.add_route(
1142 1142 name='edit_repo_strip',
1143 1143 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
1144 1144 config.add_view(
1145 1145 RepoStripView,
1146 1146 attr='strip',
1147 1147 route_name='edit_repo_strip', request_method='GET',
1148 1148 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1149 1149
1150 1150 config.add_route(
1151 1151 name='strip_check',
1152 1152 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
1153 1153 config.add_view(
1154 1154 RepoStripView,
1155 1155 attr='strip_check',
1156 1156 route_name='strip_check', request_method='POST',
1157 1157 renderer='json', xhr=True)
1158 1158
1159 1159 config.add_route(
1160 1160 name='strip_execute',
1161 1161 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
1162 1162 config.add_view(
1163 1163 RepoStripView,
1164 1164 attr='strip_execute',
1165 1165 route_name='strip_execute', request_method='POST',
1166 1166 renderer='json', xhr=True)
1167 1167
1168 1168 # Audit logs
1169 1169 config.add_route(
1170 1170 name='edit_repo_audit_logs',
1171 1171 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
1172 1172 config.add_view(
1173 1173 AuditLogsView,
1174 1174 attr='repo_audit_logs',
1175 1175 route_name='edit_repo_audit_logs', request_method='GET',
1176 1176 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
1177 1177
1178 1178 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
1179 1179 config.add_route(
1180 1180 name='rss_feed_home',
1181 1181 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
1182 1182 config.add_view(
1183 1183 RepoFeedView,
1184 1184 attr='rss',
1185 1185 route_name='rss_feed_home', request_method='GET', renderer=None)
1186 1186
1187 1187 config.add_route(
1188 1188 name='rss_feed_home_old',
1189 1189 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
1190 1190 config.add_view(
1191 1191 RepoFeedView,
1192 1192 attr='rss',
1193 1193 route_name='rss_feed_home_old', request_method='GET', renderer=None)
1194 1194
1195 1195 config.add_route(
1196 1196 name='atom_feed_home',
1197 1197 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
1198 1198 config.add_view(
1199 1199 RepoFeedView,
1200 1200 attr='atom',
1201 1201 route_name='atom_feed_home', request_method='GET', renderer=None)
1202 1202
1203 1203 config.add_route(
1204 1204 name='atom_feed_home_old',
1205 1205 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
1206 1206 config.add_view(
1207 1207 RepoFeedView,
1208 1208 attr='atom',
1209 1209 route_name='atom_feed_home_old', request_method='GET', renderer=None)
1210 1210
1211 1211 # NOTE(marcink): needs to be at the end for catch-all
1212 1212 add_route_with_slash(
1213 1213 config,
1214 1214 name='repo_summary',
1215 1215 pattern='/{repo_name:.*?[^/]}', repo_route=True)
1216 1216 config.add_view(
1217 1217 RepoSummaryView,
1218 1218 attr='summary',
1219 1219 route_name='repo_summary', request_method='GET',
1220 1220 renderer='rhodecode:templates/summary/summary.mako')
1221 1221
1222 1222 # TODO(marcink): there's no such route??
1223 1223 config.add_view(
1224 1224 RepoSummaryView,
1225 1225 attr='summary',
1226 1226 route_name='repo_summary_slash', request_method='GET',
1227 1227 renderer='rhodecode:templates/summary/summary.mako') No newline at end of file
@@ -1,819 +1,831 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 21 import collections
22 22
23 23 from pyramid.httpexceptions import (
24 24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 25 from pyramid.renderers import render
26 26 from pyramid.response import Response
27 27
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.apps.file_store import utils as store_utils
30 30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 31
32 32 from rhodecode.lib import diffs, codeblocks, channelstream
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 35 from rhodecode.lib import ext_json
36 36 from collections import OrderedDict
37 37 from rhodecode.lib.diffs import (
38 38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 39 get_diff_whitespace_flag)
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 RepositoryError, CommitDoesNotExistError)
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 47 ChangesetCommentHistory
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.settings import VcsSettingsModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, request):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += request.GET.getall(k)
59 59
60 60
61 61 class RepoCommitsView(RepoAppView):
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.rhodecode_repo = self.rhodecode_vcs_repo
65 65
66 66 return c
67 67
68 68 def _is_diff_cache_enabled(self, target_repo):
69 69 caching_enabled = self._get_general_setting(
70 70 target_repo, 'rhodecode_diff_cache')
71 71 log.debug('Diff caching enabled: %s', caching_enabled)
72 72 return caching_enabled
73 73
74 74 def _commit(self, commit_id_range, method):
75 75 _ = self.request.translate
76 76 c = self.load_default_context()
77 77 c.fulldiff = self.request.GET.get('fulldiff')
78 78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79 79
80 80 # fetch global flags of ignore ws or context lines
81 81 diff_context = get_diff_context(self.request)
82 82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 83
84 84 # diff_limit will cut off the whole diff if the limit is applied
85 85 # otherwise it will just hide the big files from the front-end
86 86 diff_limit = c.visual.cut_off_limit_diff
87 87 file_limit = c.visual.cut_off_limit_file
88 88
89 89 # get ranges of commit ids if preset
90 90 commit_range = commit_id_range.split('...')[:2]
91 91
92 92 try:
93 93 pre_load = ['affected_files', 'author', 'branch', 'date',
94 94 'message', 'parents']
95 95 if self.rhodecode_vcs_repo.alias == 'hg':
96 96 pre_load += ['hidden', 'obsolete', 'phase']
97 97
98 98 if len(commit_range) == 2:
99 99 commits = self.rhodecode_vcs_repo.get_commits(
100 100 start_id=commit_range[0], end_id=commit_range[1],
101 101 pre_load=pre_load, translate_tags=False)
102 102 commits = list(commits)
103 103 else:
104 104 commits = [self.rhodecode_vcs_repo.get_commit(
105 105 commit_id=commit_id_range, pre_load=pre_load)]
106 106
107 107 c.commit_ranges = commits
108 108 if not c.commit_ranges:
109 109 raise RepositoryError('The commit range returned an empty result')
110 110 except CommitDoesNotExistError as e:
111 111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 112 h.flash(msg, category='error')
113 113 raise HTTPNotFound()
114 114 except Exception:
115 115 log.exception("General failure")
116 116 raise HTTPNotFound()
117 117 single_commit = len(c.commit_ranges) == 1
118 118
119 119 if redirect_to_combined and not single_commit:
120 120 source_ref = getattr(c.commit_ranges[0].parents[0]
121 121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 122 target_ref = c.commit_ranges[-1].raw_id
123 123 next_url = h.route_path(
124 124 'repo_compare',
125 125 repo_name=c.repo_name,
126 126 source_ref_type='rev',
127 127 source_ref=source_ref,
128 128 target_ref_type='rev',
129 129 target_ref=target_ref)
130 130 raise HTTPFound(next_url)
131 131
132 132 c.changes = OrderedDict()
133 133 c.lines_added = 0
134 134 c.lines_deleted = 0
135 135
136 136 # auto collapse if we have more than limit
137 137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139 139
140 140 c.commit_statuses = ChangesetStatus.STATUSES
141 141 c.inline_comments = []
142 142 c.files = []
143 143
144 144 c.comments = []
145 145 c.unresolved_comments = []
146 146 c.resolved_comments = []
147 147
148 148 # Single commit
149 149 if single_commit:
150 150 commit = c.commit_ranges[0]
151 151 c.comments = CommentsModel().get_comments(
152 152 self.db_repo.repo_id,
153 153 revision=commit.raw_id)
154 154
155 155 # comments from PR
156 156 statuses = ChangesetStatusModel().get_statuses(
157 157 self.db_repo.repo_id, commit.raw_id,
158 158 with_revisions=True)
159 159
160 160 prs = set()
161 161 reviewers = list()
162 162 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 163 for c_status in statuses:
164 164
165 165 # extract associated pull-requests from votes
166 166 if c_status.pull_request:
167 167 prs.add(c_status.pull_request)
168 168
169 169 # extract reviewers
170 170 _user_id = c_status.author.user_id
171 171 if _user_id not in reviewers_duplicates:
172 172 reviewers.append(
173 173 StrictAttributeDict({
174 174 'user': c_status.author,
175 175
176 176 # fake attributed for commit, page that we don't have
177 177 # but we share the display with PR page
178 178 'mandatory': False,
179 179 'reasons': [],
180 180 'rule_user_group_data': lambda: None
181 181 })
182 182 )
183 183 reviewers_duplicates.add(_user_id)
184 184
185 185 c.reviewers_count = len(reviewers)
186 186 c.observers_count = 0
187 187
188 188 # from associated statuses, check the pull requests, and
189 189 # show comments from them
190 190 for pr in prs:
191 191 c.comments.extend(pr.comments)
192 192
193 193 c.unresolved_comments = CommentsModel()\
194 194 .get_commit_unresolved_todos(commit.raw_id)
195 195 c.resolved_comments = CommentsModel()\
196 196 .get_commit_resolved_todos(commit.raw_id)
197 197
198 198 c.inline_comments_flat = CommentsModel()\
199 199 .get_commit_inline_comments(commit.raw_id)
200 200
201 201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 202 statuses, reviewers)
203 203
204 204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205 205
206 206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207 207
208 208 for review_obj, member, reasons, mandatory, status in review_statuses:
209 209 member_reviewer = h.reviewer_as_json(
210 210 member, reasons=reasons, mandatory=mandatory, role=None,
211 211 user_group=None
212 212 )
213 213
214 214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 215 member_reviewer['review_status'] = current_review_status
216 216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 217 member_reviewer['allowed_to_update'] = False
218 218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219 219
220 220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
221 221
222 222 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225 225
226 226 diff = None
227 227 # Iterate over ranges (default commit view is always one commit)
228 228 for commit in c.commit_ranges:
229 229 c.changes[commit.raw_id] = []
230 230
231 231 commit2 = commit
232 232 commit1 = commit.first_parent
233 233
234 234 if method == 'show':
235 235 inline_comments = CommentsModel().get_inline_comments(
236 236 self.db_repo.repo_id, revision=commit.raw_id)
237 237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 238 inline_comments))
239 239 c.inline_comments = inline_comments
240 240
241 241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 242 self.db_repo)
243 243 cache_file_path = diff_cache_exist(
244 244 cache_path, 'diff', commit.raw_id,
245 245 hide_whitespace_changes, diff_context, c.fulldiff)
246 246
247 247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 248 force_recache = str2bool(self.request.GET.get('force_recache'))
249 249
250 250 cached_diff = None
251 251 if caching_enabled:
252 252 cached_diff = load_cached_diff(cache_file_path)
253 253
254 254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 255 if not force_recache and has_proper_diff_cache:
256 256 diffset = cached_diff['diff']
257 257 else:
258 258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 259 commit1, commit2,
260 260 ignore_whitespace=hide_whitespace_changes,
261 261 context=diff_context)
262 262
263 diff_processor = diffs.DiffProcessor(
264 vcs_diff, format='newdiff', diff_limit=diff_limit,
265 file_limit=file_limit, show_full_diff=c.fulldiff)
263 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
264 diff_limit=diff_limit,
265 file_limit=file_limit,
266 show_full_diff=c.fulldiff)
266 267
267 268 _parsed = diff_processor.prepare()
268 269
269 270 diffset = codeblocks.DiffSet(
270 271 repo_name=self.db_repo_name,
271 272 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 273 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 274
274 275 diffset = self.path_filter.render_patchset_filtered(
275 276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 277
277 278 # save cached diff
278 279 if caching_enabled:
279 280 cache_diff(cache_file_path, diffset, None)
280 281
281 282 c.limited_diff = diffset.limited_diff
282 283 c.changes[commit.raw_id] = diffset
283 284 else:
284 285 # TODO(marcink): no cache usage here...
285 286 _diff = self.rhodecode_vcs_repo.get_diff(
286 287 commit1, commit2,
287 288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 diff_processor = diffs.DiffProcessor(
289 _diff, format='newdiff', diff_limit=diff_limit,
290 file_limit=file_limit, show_full_diff=c.fulldiff)
289 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
290 diff_limit=diff_limit,
291 file_limit=file_limit, show_full_diff=c.fulldiff)
291 292 # downloads/raw we only need RAW diff nothing else
292 293 diff = self.path_filter.get_raw_patch(diff_processor)
293 294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 295
295 296 # sort comments by how they were generated
296 297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 298 c.at_version_num = None
298 299
299 300 if len(c.commit_ranges) == 1:
300 301 c.commit = c.commit_ranges[0]
301 302 c.parent_tmpl = ''.join(
302 303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 304
304 305 if method == 'download':
305 306 response = Response(diff)
306 307 response.content_type = 'text/plain'
307 308 response.content_disposition = (
308 309 'attachment; filename=%s.diff' % commit_id_range[:12])
309 310 return response
310 311 elif method == 'patch':
311 312
312 313 c.diff = safe_str(diff)
313 314 patch = render(
314 315 'rhodecode:templates/changeset/patch_changeset.mako',
315 316 self._get_template_context(c), self.request)
316 317 response = Response(patch)
317 318 response.content_type = 'text/plain'
318 319 return response
319 320 elif method == 'raw':
320 321 response = Response(diff)
321 322 response.content_type = 'text/plain'
322 323 return response
323 324 elif method == 'show':
324 325 if len(c.commit_ranges) == 1:
325 326 html = render(
326 327 'rhodecode:templates/changeset/changeset.mako',
327 328 self._get_template_context(c), self.request)
328 329 return Response(html)
329 330 else:
330 331 c.ancestor = None
331 332 c.target_repo = self.db_repo
332 333 html = render(
333 334 'rhodecode:templates/changeset/changeset_range.mako',
334 335 self._get_template_context(c), self.request)
335 336 return Response(html)
336 337
337 338 raise HTTPBadRequest()
338 339
339 340 @LoginRequired()
340 341 @HasRepoPermissionAnyDecorator(
341 342 'repository.read', 'repository.write', 'repository.admin')
342 343 def repo_commit_show(self):
343 344 commit_id = self.request.matchdict['commit_id']
344 345 return self._commit(commit_id, method='show')
345 346
346 347 @LoginRequired()
347 348 @HasRepoPermissionAnyDecorator(
348 349 'repository.read', 'repository.write', 'repository.admin')
349 350 def repo_commit_raw(self):
350 351 commit_id = self.request.matchdict['commit_id']
351 352 return self._commit(commit_id, method='raw')
352 353
353 354 @LoginRequired()
354 355 @HasRepoPermissionAnyDecorator(
355 356 'repository.read', 'repository.write', 'repository.admin')
356 357 def repo_commit_patch(self):
357 358 commit_id = self.request.matchdict['commit_id']
358 359 return self._commit(commit_id, method='patch')
359 360
360 361 @LoginRequired()
361 362 @HasRepoPermissionAnyDecorator(
362 363 'repository.read', 'repository.write', 'repository.admin')
363 364 def repo_commit_download(self):
364 365 commit_id = self.request.matchdict['commit_id']
365 366 return self._commit(commit_id, method='download')
366 367
367 368 def _commit_comments_create(self, commit_id, comments):
368 369 _ = self.request.translate
369 370 data = {}
370 371 if not comments:
371 372 return
372 373
373 374 commit = self.db_repo.get_commit(commit_id)
374 375
375 376 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 377 for entry in comments:
377 378 c = self.load_default_context()
378 379 comment_type = entry['comment_type']
379 380 text = entry['text']
380 381 status = entry['status']
381 382 is_draft = str2bool(entry['is_draft'])
382 383 resolves_comment_id = entry['resolves_comment_id']
383 384 f_path = entry['f_path']
384 385 line_no = entry['line']
385 386 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
386 387
387 388 if status:
388 389 text = text or (_('Status change %(transition_icon)s %(status)s')
389 390 % {'transition_icon': '>',
390 391 'status': ChangesetStatus.get_status_lbl(status)})
391 392
392 393 comment = CommentsModel().create(
393 394 text=text,
394 395 repo=self.db_repo.repo_id,
395 396 user=self._rhodecode_db_user.user_id,
396 397 commit_id=commit_id,
397 398 f_path=f_path,
398 399 line_no=line_no,
399 400 status_change=(ChangesetStatus.get_status_lbl(status)
400 401 if status else None),
401 402 status_change_type=status,
402 403 comment_type=comment_type,
403 404 is_draft=is_draft,
404 405 resolves_comment_id=resolves_comment_id,
405 406 auth_user=self._rhodecode_user,
406 407 send_email=not is_draft, # skip notification for draft comments
407 408 )
408 409 is_inline = comment.is_inline
409 410
410 411 # get status if set !
411 412 if status:
412 413 # `dont_allow_on_closed_pull_request = True` means
413 414 # if latest status was from pull request and it's closed
414 415 # disallow changing status !
415 416
416 417 try:
417 418 ChangesetStatusModel().set_status(
418 419 self.db_repo.repo_id,
419 420 status,
420 421 self._rhodecode_db_user.user_id,
421 422 comment,
422 423 revision=commit_id,
423 424 dont_allow_on_closed_pull_request=True
424 425 )
425 426 except StatusChangeOnClosedPullRequestError:
426 427 msg = _('Changing the status of a commit associated with '
427 428 'a closed pull request is not allowed')
428 429 log.exception(msg)
429 430 h.flash(msg, category='warning')
430 431 raise HTTPFound(h.route_path(
431 432 'repo_commit', repo_name=self.db_repo_name,
432 433 commit_id=commit_id))
433 434
434 435 Session().flush()
435 436 # this is somehow required to get access to some relationship
436 437 # loaded on comment
437 438 Session().refresh(comment)
438 439
439 440 # skip notifications for drafts
440 441 if not is_draft:
441 442 CommentsModel().trigger_commit_comment_hook(
442 443 self.db_repo, self._rhodecode_user, 'create',
443 444 data={'comment': comment, 'commit': commit})
444 445
445 446 comment_id = comment.comment_id
446 447 data[comment_id] = {
447 448 'target_id': target_elem_id
448 449 }
449 450 Session().flush()
450 451
451 452 c.co = comment
452 453 c.at_version_num = 0
453 454 c.is_new = True
454 455 rendered_comment = render(
455 456 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 457 self._get_template_context(c), self.request)
457 458
458 459 data[comment_id].update(comment.get_dict())
459 460 data[comment_id].update({'rendered_text': rendered_comment})
460 461
461 462 # finalize, commit and redirect
462 463 Session().commit()
463 464
464 465 # skip channelstream for draft comments
465 466 if not all_drafts:
466 467 comment_broadcast_channel = channelstream.comment_channel(
467 468 self.db_repo_name, commit_obj=commit)
468 469
469 470 comment_data = data
470 471 posted_comment_type = 'inline' if is_inline else 'general'
471 472 if len(data) == 1:
472 473 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 474 else:
474 475 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475 476
476 477 channelstream.comment_channelstream_push(
477 478 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 479 comment_data=comment_data)
479 480
480 481 return data
481 482
482 483 @LoginRequired()
483 484 @NotAnonymous()
484 485 @HasRepoPermissionAnyDecorator(
485 486 'repository.read', 'repository.write', 'repository.admin')
486 487 @CSRFRequired()
487 488 def repo_commit_comment_create(self):
488 489 _ = self.request.translate
489 490 commit_id = self.request.matchdict['commit_id']
490 491
491 492 multi_commit_ids = []
492 493 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 494 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 495 if _commit_id not in multi_commit_ids:
495 496 multi_commit_ids.append(_commit_id)
496 497
497 498 commit_ids = multi_commit_ids or [commit_id]
498 499
499 500 data = []
500 501 # Multiple comments for each passed commit id
501 502 for current_id in filter(None, commit_ids):
502 503 comment_data = {
503 504 'comment_type': self.request.POST.get('comment_type'),
504 505 'text': self.request.POST.get('text'),
505 506 'status': self.request.POST.get('changeset_status', None),
506 507 'is_draft': self.request.POST.get('draft'),
507 508 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 509 'close_pull_request': self.request.POST.get('close_pull_request'),
509 510 'f_path': self.request.POST.get('f_path'),
510 511 'line': self.request.POST.get('line'),
511 512 }
512 513 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 514 data.append(comment)
514 515
515 516 return data if len(data) > 1 else data[0]
516 517
517 518 @LoginRequired()
518 519 @NotAnonymous()
519 520 @HasRepoPermissionAnyDecorator(
520 521 'repository.read', 'repository.write', 'repository.admin')
521 522 @CSRFRequired()
522 523 def repo_commit_comment_preview(self):
523 524 # Technically a CSRF token is not needed as no state changes with this
524 525 # call. However, as this is a POST is better to have it, so automated
525 526 # tools don't flag it as potential CSRF.
526 527 # Post is required because the payload could be bigger than the maximum
527 528 # allowed by GET.
528 529
529 530 text = self.request.POST.get('text')
530 531 renderer = self.request.POST.get('renderer') or 'rst'
531 532 if text:
532 533 return h.render(text, renderer=renderer, mentions=True,
533 534 repo_name=self.db_repo_name)
534 535 return ''
535 536
536 537 @LoginRequired()
537 538 @HasRepoPermissionAnyDecorator(
538 539 'repository.read', 'repository.write', 'repository.admin')
539 540 @CSRFRequired()
540 541 def repo_commit_comment_history_view(self):
541 542 c = self.load_default_context()
542 543 comment_id = self.request.matchdict['comment_id']
543 544 comment_history_id = self.request.matchdict['comment_history_id']
544 545
545 546 comment = ChangesetComment.get_or_404(comment_id)
546 547 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 548 if comment.draft and not comment_owner:
548 549 # if we see draft comments history, we only allow this for owner
549 550 raise HTTPNotFound()
550 551
551 552 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
552 553 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
553 554
554 555 if is_repo_comment:
555 556 c.comment_history = comment_history
556 557
557 558 rendered_comment = render(
558 559 'rhodecode:templates/changeset/comment_history.mako',
559 560 self._get_template_context(c), self.request)
560 561 return rendered_comment
561 562 else:
562 563 log.warning('No permissions for user %s to show comment_history_id: %s',
563 564 self._rhodecode_db_user, comment_history_id)
564 565 raise HTTPNotFound()
565 566
566 567 @LoginRequired()
567 568 @NotAnonymous()
568 569 @HasRepoPermissionAnyDecorator(
569 570 'repository.read', 'repository.write', 'repository.admin')
570 571 @CSRFRequired()
571 572 def repo_commit_comment_attachment_upload(self):
572 573 c = self.load_default_context()
573 574 upload_key = 'attachment'
574 575
575 576 file_obj = self.request.POST.get(upload_key)
576 577
577 578 if file_obj is None:
578 579 self.request.response.status = 400
579 580 return {'store_fid': None,
580 581 'access_path': None,
581 582 'error': '{} data field is missing'.format(upload_key)}
582 583
583 584 if not hasattr(file_obj, 'filename'):
584 585 self.request.response.status = 400
585 586 return {'store_fid': None,
586 587 'access_path': None,
587 588 'error': 'filename cannot be read from the data field'}
588 589
589 590 filename = file_obj.filename
590 591 file_display_name = filename
591 592
592 593 metadata = {
593 594 'user_uploaded': {'username': self._rhodecode_user.username,
594 595 'user_id': self._rhodecode_user.user_id,
595 596 'ip': self._rhodecode_user.ip_addr}}
596 597
597 598 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
598 599 allowed_extensions = [
599 600 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
600 601 '.pptx', '.txt', '.xlsx', '.zip']
601 602 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
602 603
603 604 try:
604 605 storage = store_utils.get_file_storage(self.request.registry.settings)
605 606 store_uid, metadata = storage.save_file(
606 607 file_obj.file, filename, extra_metadata=metadata,
607 608 extensions=allowed_extensions, max_filesize=max_file_size)
608 609 except FileNotAllowedException:
609 610 self.request.response.status = 400
610 611 permitted_extensions = ', '.join(allowed_extensions)
611 612 error_msg = 'File `{}` is not allowed. ' \
612 613 'Only following extensions are permitted: {}'.format(
613 614 filename, permitted_extensions)
614 615 return {'store_fid': None,
615 616 'access_path': None,
616 617 'error': error_msg}
617 618 except FileOverSizeException:
618 619 self.request.response.status = 400
619 620 limit_mb = h.format_byte_size_binary(max_file_size)
620 621 return {'store_fid': None,
621 622 'access_path': None,
622 623 'error': 'File {} is exceeding allowed limit of {}.'.format(
623 624 filename, limit_mb)}
624 625
625 626 try:
626 627 entry = FileStore.create(
627 628 file_uid=store_uid, filename=metadata["filename"],
628 629 file_hash=metadata["sha256"], file_size=metadata["size"],
629 630 file_display_name=file_display_name,
630 631 file_description=u'comment attachment `{}`'.format(safe_str(filename)),
631 632 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
632 633 scope_repo_id=self.db_repo.repo_id
633 634 )
634 635 Session().add(entry)
635 636 Session().commit()
636 637 log.debug('Stored upload in DB as %s', entry)
637 638 except Exception:
638 639 log.exception('Failed to store file %s', filename)
639 640 self.request.response.status = 400
640 641 return {'store_fid': None,
641 642 'access_path': None,
642 643 'error': 'File {} failed to store in DB.'.format(filename)}
643 644
644 645 Session().commit()
645 646
646 return {
647 data = {
647 648 'store_fid': store_uid,
648 649 'access_path': h.route_path(
649 650 'download_file', fid=store_uid),
650 651 'fqn_access_path': h.route_url(
651 652 'download_file', fid=store_uid),
652 'repo_access_path': h.route_path(
653 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
653 # for EE those are replaced by FQN links on repo-only like
654 'repo_access_path': h.route_url(
655 'download_file', fid=store_uid),
654 656 'repo_fqn_access_path': h.route_url(
655 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
657 'download_file', fid=store_uid),
656 658 }
659 # this data is a part of CE/EE additional code
660 if c.rhodecode_edition_id == 'EE':
661 data.update({
662 'repo_access_path': h.route_path(
663 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
664 'repo_fqn_access_path': h.route_url(
665 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
666 })
667
668 return data
657 669
658 670 @LoginRequired()
659 671 @NotAnonymous()
660 672 @HasRepoPermissionAnyDecorator(
661 673 'repository.read', 'repository.write', 'repository.admin')
662 674 @CSRFRequired()
663 675 def repo_commit_comment_delete(self):
664 676 commit_id = self.request.matchdict['commit_id']
665 677 comment_id = self.request.matchdict['comment_id']
666 678
667 679 comment = ChangesetComment.get_or_404(comment_id)
668 680 if not comment:
669 681 log.debug('Comment with id:%s not found, skipping', comment_id)
670 682 # comment already deleted in another call probably
671 683 return True
672 684
673 685 if comment.immutable:
674 686 # don't allow deleting comments that are immutable
675 687 raise HTTPForbidden()
676 688
677 689 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
678 690 super_admin = h.HasPermissionAny('hg.admin')()
679 691 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
680 692 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
681 693 comment_repo_admin = is_repo_admin and is_repo_comment
682 694
683 695 if comment.draft and not comment_owner:
684 696 # We never allow to delete draft comments for other than owners
685 697 raise HTTPNotFound()
686 698
687 699 if super_admin or comment_owner or comment_repo_admin:
688 700 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
689 701 Session().commit()
690 702 return True
691 703 else:
692 704 log.warning('No permissions for user %s to delete comment_id: %s',
693 705 self._rhodecode_db_user, comment_id)
694 706 raise HTTPNotFound()
695 707
696 708 @LoginRequired()
697 709 @NotAnonymous()
698 710 @HasRepoPermissionAnyDecorator(
699 711 'repository.read', 'repository.write', 'repository.admin')
700 712 @CSRFRequired()
701 713 def repo_commit_comment_edit(self):
702 714 self.load_default_context()
703 715
704 716 commit_id = self.request.matchdict['commit_id']
705 717 comment_id = self.request.matchdict['comment_id']
706 718 comment = ChangesetComment.get_or_404(comment_id)
707 719
708 720 if comment.immutable:
709 721 # don't allow deleting comments that are immutable
710 722 raise HTTPForbidden()
711 723
712 724 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
713 725 super_admin = h.HasPermissionAny('hg.admin')()
714 726 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
715 727 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
716 728 comment_repo_admin = is_repo_admin and is_repo_comment
717 729
718 730 if super_admin or comment_owner or comment_repo_admin:
719 731 text = self.request.POST.get('text')
720 732 version = self.request.POST.get('version')
721 733 if text == comment.text:
722 734 log.warning(
723 735 'Comment(repo): '
724 736 'Trying to create new version '
725 737 'with the same comment body {}'.format(
726 738 comment_id,
727 739 )
728 740 )
729 741 raise HTTPNotFound()
730 742
731 743 if version.isdigit():
732 744 version = int(version)
733 745 else:
734 746 log.warning(
735 747 'Comment(repo): Wrong version type {} {} '
736 748 'for comment {}'.format(
737 749 version,
738 750 type(version),
739 751 comment_id,
740 752 )
741 753 )
742 754 raise HTTPNotFound()
743 755
744 756 try:
745 757 comment_history = CommentsModel().edit(
746 758 comment_id=comment_id,
747 759 text=text,
748 760 auth_user=self._rhodecode_user,
749 761 version=version,
750 762 )
751 763 except CommentVersionMismatch:
752 764 raise HTTPConflict()
753 765
754 766 if not comment_history:
755 767 raise HTTPNotFound()
756 768
757 769 if not comment.draft:
758 770 commit = self.db_repo.get_commit(commit_id)
759 771 CommentsModel().trigger_commit_comment_hook(
760 772 self.db_repo, self._rhodecode_user, 'edit',
761 773 data={'comment': comment, 'commit': commit})
762 774
763 775 Session().commit()
764 776 return {
765 777 'comment_history_id': comment_history.comment_history_id,
766 778 'comment_id': comment.comment_id,
767 779 'comment_version': comment_history.version,
768 780 'comment_author_username': comment_history.author.username,
769 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
781 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
770 782 'comment_created_on': h.age_component(comment_history.created_on,
771 783 time_is_local=True),
772 784 }
773 785 else:
774 786 log.warning('No permissions for user %s to edit comment_id: %s',
775 787 self._rhodecode_db_user, comment_id)
776 788 raise HTTPNotFound()
777 789
778 790 @LoginRequired()
779 791 @HasRepoPermissionAnyDecorator(
780 792 'repository.read', 'repository.write', 'repository.admin')
781 793 def repo_commit_data(self):
782 794 commit_id = self.request.matchdict['commit_id']
783 795 self.load_default_context()
784 796
785 797 try:
786 798 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 799 except CommitDoesNotExistError as e:
788 800 return EmptyCommit(message=str(e))
789 801
790 802 @LoginRequired()
791 803 @HasRepoPermissionAnyDecorator(
792 804 'repository.read', 'repository.write', 'repository.admin')
793 805 def repo_commit_children(self):
794 806 commit_id = self.request.matchdict['commit_id']
795 807 self.load_default_context()
796 808
797 809 try:
798 810 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
799 811 children = commit.children
800 812 except CommitDoesNotExistError:
801 813 children = []
802 814
803 815 result = {"results": children}
804 816 return result
805 817
806 818 @LoginRequired()
807 819 @HasRepoPermissionAnyDecorator(
808 820 'repository.read', 'repository.write', 'repository.admin')
809 821 def repo_commit_parents(self):
810 822 commit_id = self.request.matchdict['commit_id']
811 823 self.load_default_context()
812 824
813 825 try:
814 826 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
815 827 parents = commit.parents
816 828 except CommitDoesNotExistError:
817 829 parents = []
818 830 result = {"results": parents}
819 831 return result
@@ -1,306 +1,307 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.utils import safe_str
35 35 from rhodecode.lib.utils2 import str2bool
36 36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 39 NodeDoesNotExistError)
40 40 from rhodecode.model.db import Repository, ChangesetStatus
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoCompareView(RepoAppView):
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 49 return c
50 50
51 51 def _get_commit_or_redirect(
52 52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 53 """
54 54 This is a safe way to get a commit. If an error occurs it
55 55 redirects to a commit with a proper message. If partial is set
56 56 then it does not do redirect raise and throws an exception instead.
57 57 """
58 58 _ = self.request.translate
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 if not partial:
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 69 raise HTTPBadRequest()
70 70
71 71 except RepositoryError as e:
72 72 log.exception(safe_str(e))
73 73 h.flash(h.escape(safe_str(e)), category='warning')
74 74 if not partial:
75 75 raise HTTPFound(
76 76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 77 raise HTTPBadRequest()
78 78
79 79 @LoginRequired()
80 80 @HasRepoPermissionAnyDecorator(
81 81 'repository.read', 'repository.write', 'repository.admin')
82 82 def compare_select(self):
83 83 _ = self.request.translate
84 84 c = self.load_default_context()
85 85
86 86 source_repo = self.db_repo_name
87 87 target_repo = self.request.GET.get('target_repo', source_repo)
88 88 c.source_repo = Repository.get_by_repo_name(source_repo)
89 89 c.target_repo = Repository.get_by_repo_name(target_repo)
90 90
91 91 if c.source_repo is None or c.target_repo is None:
92 92 raise HTTPNotFound()
93 93
94 94 c.compare_home = True
95 95 c.commit_ranges = []
96 96 c.collapse_all_commits = False
97 97 c.diffset = None
98 98 c.limited_diff = False
99 99 c.source_ref = c.target_ref = _('Select commit')
100 100 c.source_ref_type = ""
101 101 c.target_ref_type = ""
102 102 c.commit_statuses = ChangesetStatus.STATUSES
103 103 c.preview_mode = False
104 104 c.file_path = None
105 105
106 106 return self._get_template_context(c)
107 107
108 108 @LoginRequired()
109 109 @HasRepoPermissionAnyDecorator(
110 110 'repository.read', 'repository.write', 'repository.admin')
111 111 def compare(self):
112 112 _ = self.request.translate
113 113 c = self.load_default_context()
114 114
115 115 source_ref_type = self.request.matchdict['source_ref_type']
116 116 source_ref = self.request.matchdict['source_ref']
117 117 target_ref_type = self.request.matchdict['target_ref_type']
118 118 target_ref = self.request.matchdict['target_ref']
119 119
120 120 # source_ref will be evaluated in source_repo
121 121 source_repo_name = self.db_repo_name
122 122 source_path, source_id = parse_path_ref(source_ref)
123 123
124 124 # target_ref will be evaluated in target_repo
125 125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 126 target_path, target_id = parse_path_ref(
127 127 target_ref, default_path=self.request.GET.get('f_path', ''))
128 128
129 129 # if merge is True
130 130 # Show what changes since the shared ancestor commit of target/source
131 131 # the source would get if it was merged with target. Only commits
132 132 # which are in target but not in source will be shown.
133 133 merge = str2bool(self.request.GET.get('merge'))
134 134 # if merge is False
135 135 # Show a raw diff of source/target refs even if no ancestor exists
136 136
137 137 # c.fulldiff disables cut_off_limit
138 138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139 139
140 140 # fetch global flags of ignore ws or context lines
141 141 diff_context = diffs.get_diff_context(self.request)
142 142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143 143
144 144 c.file_path = target_path
145 145 c.commit_statuses = ChangesetStatus.STATUSES
146 146
147 147 # if partial, returns just compare_commits.html (commits log)
148 148 partial = self.request.is_xhr
149 149
150 150 # swap url for compare_diff page
151 151 c.swap_url = h.route_path(
152 152 'repo_compare',
153 153 repo_name=target_repo_name,
154 154 source_ref_type=target_ref_type,
155 155 source_ref=target_ref,
156 156 target_repo=source_repo_name,
157 157 target_ref_type=source_ref_type,
158 158 target_ref=source_ref,
159 159 _query=dict(merge=merge and '1' or '', f_path=target_path))
160 160
161 161 source_repo = Repository.get_by_repo_name(source_repo_name)
162 162 target_repo = Repository.get_by_repo_name(target_repo_name)
163 163
164 164 if source_repo is None:
165 165 log.error('Could not find the source repo: {}'
166 166 .format(source_repo_name))
167 167 h.flash(_('Could not find the source repo: `{}`')
168 168 .format(h.escape(source_repo_name)), category='error')
169 169 raise HTTPFound(
170 170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171 171
172 172 if target_repo is None:
173 173 log.error('Could not find the target repo: {}'
174 174 .format(source_repo_name))
175 175 h.flash(_('Could not find the target repo: `{}`')
176 176 .format(h.escape(target_repo_name)), category='error')
177 177 raise HTTPFound(
178 178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179 179
180 180 source_scm = source_repo.scm_instance()
181 181 target_scm = target_repo.scm_instance()
182 182
183 183 source_alias = source_scm.alias
184 184 target_alias = target_scm.alias
185 185 if source_alias != target_alias:
186 186 msg = _('The comparison of two different kinds of remote repos '
187 187 'is not available')
188 188 log.error(msg)
189 189 h.flash(msg, category='error')
190 190 raise HTTPFound(
191 191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192 192
193 193 source_commit = self._get_commit_or_redirect(
194 194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 195 partial=partial)
196 196 target_commit = self._get_commit_or_redirect(
197 197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 198 partial=partial)
199 199
200 200 c.compare_home = False
201 201 c.source_repo = source_repo
202 202 c.target_repo = target_repo
203 203 c.source_ref = source_ref
204 204 c.target_ref = target_ref
205 205 c.source_ref_type = source_ref_type
206 206 c.target_ref_type = target_ref_type
207 207
208 208 pre_load = ["author", "date", "message", "branch"]
209 209 c.ancestor = None
210 210
211 211 try:
212 212 c.commit_ranges = source_scm.compare(
213 213 source_commit.raw_id, target_commit.raw_id,
214 214 target_scm, merge, pre_load=pre_load) or []
215 215 if merge:
216 216 c.ancestor = source_scm.get_common_ancestor(
217 217 source_commit.raw_id, target_commit.raw_id, target_scm)
218 218 except RepositoryRequirementError:
219 219 msg = _('Could not compare repos with different '
220 220 'large file settings')
221 221 log.error(msg)
222 222 if partial:
223 223 return Response(msg)
224 224 h.flash(msg, category='error')
225 225 raise HTTPFound(
226 226 h.route_path('repo_compare_select',
227 227 repo_name=self.db_repo_name))
228 228
229 229 c.statuses = self.db_repo.statuses(
230 230 [x.raw_id for x in c.commit_ranges])
231 231
232 232 # auto collapse if we have more than limit
233 233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235 235
236 236 if partial: # for PR ajax commits loader
237 237 if not c.ancestor:
238 238 return Response('') # cannot merge if there is no ancestor
239 239
240 240 html = render(
241 241 'rhodecode:templates/compare/compare_commits.mako',
242 242 self._get_template_context(c), self.request)
243 243 return Response(html)
244 244
245 245 if c.ancestor:
246 246 # case we want a simple diff without incoming commits,
247 247 # previewing what will be merged.
248 248 # Make the diff on target repo (which is known to have target_ref)
249 249 log.debug('Using ancestor %s as source_ref instead of %s',
250 250 c.ancestor, source_ref)
251 251 source_repo = target_repo
252 252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253 253
254 254 # diff_limit will cut off the whole diff if the limit is applied
255 255 # otherwise it will just hide the big files from the front-end
256 256 diff_limit = c.visual.cut_off_limit_diff
257 257 file_limit = c.visual.cut_off_limit_file
258 258
259 259 log.debug('calculating diff between '
260 260 'source_ref:%s and target_ref:%s for repo `%s`',
261 261 source_commit, target_commit,
262 262 safe_str(source_repo.scm_instance().path))
263 263
264 264 if source_commit.repository != target_commit.repository:
265 265
266 266 msg = _(
267 267 "Repositories unrelated. "
268 268 "Cannot compare commit %(commit1)s from repository %(repo1)s "
269 269 "with commit %(commit2)s from repository %(repo2)s.") % {
270 270 'commit1': h.show_id(source_commit),
271 271 'repo1': source_repo.repo_name,
272 272 'commit2': h.show_id(target_commit),
273 273 'repo2': target_repo.repo_name,
274 274 }
275 275 h.flash(msg, category='error')
276 276 raise HTTPFound(
277 277 h.route_path('repo_compare_select',
278 278 repo_name=self.db_repo_name))
279 279
280 280 txt_diff = source_repo.scm_instance().get_diff(
281 281 commit1=source_commit, commit2=target_commit,
282 282 path=target_path, path1=source_path,
283 283 ignore_whitespace=hide_whitespace_changes, context=diff_context)
284 284
285 diff_processor = diffs.DiffProcessor(
286 txt_diff, format='newdiff', diff_limit=diff_limit,
287 file_limit=file_limit, show_full_diff=c.fulldiff)
285 diff_processor = diffs.DiffProcessor(txt_diff, diff_format='newdiff',
286 diff_limit=diff_limit,
287 file_limit=file_limit,
288 show_full_diff=c.fulldiff)
288 289 _parsed = diff_processor.prepare()
289 290
290 291 diffset = codeblocks.DiffSet(
291 292 repo_name=source_repo.repo_name,
292 293 source_node_getter=codeblocks.diffset_node_getter(source_commit),
293 294 target_repo_name=self.db_repo_name,
294 295 target_node_getter=codeblocks.diffset_node_getter(target_commit),
295 296 )
296 297 c.diffset = self.path_filter.render_patchset_filtered(
297 298 diffset, _parsed, source_ref, target_ref)
298 299
299 300 c.preview_mode = merge
300 301 c.source_commit = source_commit
301 302 c.target_commit = target_commit
302 303
303 304 html = render(
304 305 'rhodecode:templates/compare/compare_diff.mako',
305 306 self._get_template_context(c), self.request)
306 307 return Response(html) No newline at end of file
@@ -1,210 +1,215 b''
1 1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import pytz
20
21 21 import logging
22 import datetime
22 23
23 24 from pyramid.response import Response
24 25
25 26 from rhodecode.apps._base import RepoAppView
26 27 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
27 28 from rhodecode.lib import audit_logger
28 29 from rhodecode.lib import rc_cache
29 30 from rhodecode.lib import helpers as h
30 31 from rhodecode.lib.auth import (
31 32 LoginRequired, HasRepoPermissionAnyDecorator)
32 33 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
33 34 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
34 35 from rhodecode.model.db import UserApiKeys, CacheKey
35 36
36 37 log = logging.getLogger(__name__)
37 38
38 39
39 40 class RepoFeedView(RepoAppView):
40 41 def load_default_context(self):
41 42 c = self._get_local_tmpl_context()
42 43 self._load_defaults()
43 44 return c
44 45
45 46 def _get_config(self):
46 47 import rhodecode
47 48 config = rhodecode.CONFIG
48 49
49 50 return {
50 51 'language': 'en-us',
51 52 'feed_ttl': '5', # TTL of feed,
52 53 'feed_include_diff':
53 54 str2bool(config.get('rss_include_diff', False)),
54 55 'feed_items_per_page':
55 56 safe_int(config.get('rss_items_per_page', 20)),
56 57 'feed_diff_limit':
57 58 # we need to protect from parsing huge diffs here other way
58 59 # we can kill the server
59 60 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
60 61 }
61 62
62 63 def _load_defaults(self):
63 64 _ = self.request.translate
64 65 config = self._get_config()
65 66 # common values for feeds
66 67 self.description = _('Changes on %s repository')
67 68 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
68 69 self.language = config["language"]
69 70 self.ttl = config["feed_ttl"]
70 71 self.feed_include_diff = config['feed_include_diff']
71 72 self.feed_diff_limit = config['feed_diff_limit']
72 73 self.feed_items_per_page = config['feed_items_per_page']
73 74
74 75 def _changes(self, commit):
75 diff_processor = DiffProcessor(
76 commit.diff(), diff_limit=self.feed_diff_limit)
76 diff = commit.diff()
77 diff_processor = DiffProcessor(diff, diff_format='newdiff',
78 diff_limit=self.feed_diff_limit)
77 79 _parsed = diff_processor.prepare(inline_diff=False)
78 80 limited_diff = isinstance(_parsed, LimitedDiffContainer)
79 81
80 82 return diff_processor, _parsed, limited_diff
81 83
82 84 def _get_title(self, commit):
83 85 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
84 86
85 87 def _get_description(self, commit):
86 88 _renderer = self.request.get_partial_renderer(
87 89 'rhodecode:templates/feed/atom_feed_entry.mako')
88 90 diff_processor, parsed_diff, limited_diff = self._changes(commit)
89 91 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
90 92 return _renderer(
91 93 'body',
92 94 commit=commit,
93 95 parsed_diff=filtered_parsed_diff,
94 96 limited_diff=limited_diff,
95 97 feed_include_diff=self.feed_include_diff,
96 98 diff_processor=diff_processor,
97 99 has_hidden_changes=has_hidden_changes
98 100 )
99 101
100 def _set_timezone(self, date, tzinfo=pytz.utc):
102 def _set_timezone(self, date, tzinfo=datetime.timezone.utc):
101 103 if not getattr(date, "tzinfo", None):
102 104 date.replace(tzinfo=tzinfo)
103 105 return date
104 106
105 107 def _get_commits(self):
106 108 pre_load = ['author', 'branch', 'date', 'message', 'parents']
107 109 if self.rhodecode_vcs_repo.is_empty():
108 110 return []
109 111
110 112 collection = self.rhodecode_vcs_repo.get_commits(
111 113 branch_name=None, show_hidden=False, pre_load=pre_load,
112 114 translate_tags=False)
113 115
114 116 return list(collection[-self.feed_items_per_page:])
115 117
116 118 def uid(self, repo_id, commit_id):
117 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
119 return '{}:{}'.format(
120 md5_safe(repo_id, return_type='str'),
121 md5_safe(commit_id, return_type='str')
122 )
118 123
119 124 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
120 125 @HasRepoPermissionAnyDecorator(
121 126 'repository.read', 'repository.write', 'repository.admin')
122 127 def atom(self):
123 128 """
124 129 Produce an atom-1.0 feed via feedgenerator module
125 130 """
126 131 self.load_default_context()
127 132 force_recache = self.get_recache_flag()
128 133
129 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
134 cache_namespace_uid = 'repo_feed.{}'.format(self.db_repo.repo_id)
130 135 condition = not (self.path_filter.is_enabled or force_recache)
131 136 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
132 137
133 138 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
134 139 condition=condition)
135 140 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
136 141 feed = Atom1Feed(
137 142 title=self.title % 'atom',
138 143 link=h.route_url('repo_summary', repo_name=_repo_name),
139 144 description=self.description % _repo_name,
140 145 language=self.language,
141 146 ttl=self.ttl
142 147 )
143 148
144 149 for commit in reversed(self._get_commits()):
145 150 date = self._set_timezone(commit.date)
146 151 feed.add_item(
147 unique_id=self.uid(repo_id, commit.raw_id),
152 unique_id=self.uid(str(repo_id), commit.raw_id),
148 153 title=self._get_title(commit),
149 154 author_name=commit.author,
150 155 description=self._get_description(commit),
151 156 link=h.route_url(
152 157 'repo_commit', repo_name=_repo_name,
153 158 commit_id=commit.raw_id),
154 159 pubdate=date,)
155 160
156 161 return feed.content_type, feed.writeString('utf-8')
157 162
158 163 commit_id = self.db_repo.changeset_cache.get('raw_id')
159 164 content_type, feed = generate_atom_feed(
160 165 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
161 166
162 167 response = Response(feed)
163 168 response.content_type = content_type
164 169 return response
165 170
166 171 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
167 172 @HasRepoPermissionAnyDecorator(
168 173 'repository.read', 'repository.write', 'repository.admin')
169 174 def rss(self):
170 175 """
171 176 Produce an rss2 feed via feedgenerator module
172 177 """
173 178 self.load_default_context()
174 179 force_recache = self.get_recache_flag()
175 180
176 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
181 cache_namespace_uid = 'repo_feed.{}'.format(self.db_repo.repo_id)
177 182 condition = not (self.path_filter.is_enabled or force_recache)
178 183 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
179 184
180 185 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
181 186 condition=condition)
182 187 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
183 188 feed = Rss201rev2Feed(
184 189 title=self.title % 'rss',
185 190 link=h.route_url('repo_summary', repo_name=_repo_name),
186 191 description=self.description % _repo_name,
187 192 language=self.language,
188 193 ttl=self.ttl
189 194 )
190 195
191 196 for commit in reversed(self._get_commits()):
192 197 date = self._set_timezone(commit.date)
193 198 feed.add_item(
194 unique_id=self.uid(repo_id, commit.raw_id),
199 unique_id=self.uid(str(repo_id), commit.raw_id),
195 200 title=self._get_title(commit),
196 201 author_name=commit.author,
197 202 description=self._get_description(commit),
198 203 link=h.route_url(
199 204 'repo_commit', repo_name=_repo_name,
200 205 commit_id=commit.raw_id),
201 206 pubdate=date,)
202 207 return feed.content_type, feed.writeString('utf-8')
203 208
204 209 commit_id = self.db_repo.changeset_cache.get('raw_id')
205 210 content_type, feed = generate_rss_feed(
206 211 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
207 212
208 213 response = Response(feed)
209 214 response.content_type = content_type
210 215 return response
@@ -1,1583 +1,1587 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 import shutil
25 import tempfile
26 24 import collections
27 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
28 28 import pathlib
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 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
43 from rhodecode.lib.str_utils import safe_bytes
41 44 from rhodecode.lib.view_utils import parse_path_ref
42 45 from rhodecode.lib.exceptions import NonRelativePathError
43 46 from rhodecode.lib.codeblocks import (
44 47 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 from rhodecode.lib.utils2 import (
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1)
48 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
49 from rhodecode.lib.type_utils import str2bool
50 from rhodecode.lib.str_utils import safe_str, safe_int
47 51 from rhodecode.lib.auth import (
48 52 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 53 from rhodecode.lib.vcs import path as vcspath
50 54 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 55 from rhodecode.lib.vcs.conf import settings
52 56 from rhodecode.lib.vcs.nodes import FileNode
53 57 from rhodecode.lib.vcs.exceptions import (
54 58 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 59 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 60 NodeDoesNotExistError, CommitError, NodeError)
57 61
58 62 from rhodecode.model.scm import ScmModel
59 63 from rhodecode.model.db import Repository
60 64
61 65 log = logging.getLogger(__name__)
62 66
63 67
68 def get_archive_name(db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
69 # original backward compat name of archive
70 clean_name = safe_str(db_repo_name.replace('/', '_'))
71
72 # e.g vcsserver-sub-1-abcfdef-archive-all.zip
73 # vcsserver-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
74
75 sub_repo = 'sub-1' if subrepos else 'sub-0'
76 commit = commit_sha if with_hash else 'archive'
77 path_marker = (path_sha if with_hash else '') or 'all'
78 archive_name = f'{clean_name}-{sub_repo}-{commit}-{path_marker}{ext}'
79
80 return archive_name
81
82
83 def get_path_sha(at_path):
84 return safe_str(sha1_safe(at_path)[:8])
85
86
87 def _get_archive_spec(fname):
88 log.debug('Detecting archive spec for: `%s`', fname)
89
90 fileformat = None
91 ext = None
92 content_type = None
93 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
94
95 if fname.endswith(extension):
96 fileformat = a_type
97 log.debug('archive is of type: %s', fileformat)
98 ext = extension
99 break
100
101 if not fileformat:
102 raise ValueError()
103
104 # left over part of whole fname is the commit
105 commit_id = fname[:-len(ext)]
106
107 return commit_id, ext, fileformat, content_type
108
109
64 110 class RepoFilesView(RepoAppView):
65 111
66 112 @staticmethod
67 113 def adjust_file_path_for_svn(f_path, repo):
68 114 """
69 115 Computes the relative path of `f_path`.
70 116
71 117 This is mainly based on prefix matching of the recognized tags and
72 118 branches in the underlying repository.
73 119 """
74 120 tags_and_branches = itertools.chain(
75 repo.branches.iterkeys(),
76 repo.tags.iterkeys())
121 repo.branches.keys(),
122 repo.tags.keys())
77 123 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 124
79 125 for name in tags_and_branches:
80 if f_path.startswith('{}/'.format(name)):
126 if f_path.startswith(f'{name}/'):
81 127 f_path = vcspath.relpath(f_path, name)
82 128 break
83 129 return f_path
84 130
85 131 def load_default_context(self):
86 132 c = self._get_local_tmpl_context(include_app_defaults=True)
87 133 c.rhodecode_repo = self.rhodecode_vcs_repo
88 134 c.enable_downloads = self.db_repo.enable_downloads
89 135 return c
90 136
91 137 def _ensure_not_locked(self, commit_id='tip'):
92 138 _ = self.request.translate
93 139
94 140 repo = self.db_repo
95 141 if repo.enable_locking and repo.locked[0]:
96 142 h.flash(_('This repository has been locked by %s on %s')
97 143 % (h.person_by_id(repo.locked[0]),
98 144 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 145 'warning')
100 146 files_url = h.route_path(
101 147 'repo_files:default_path',
102 148 repo_name=self.db_repo_name, commit_id=commit_id)
103 149 raise HTTPFound(files_url)
104 150
105 151 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 152 _ = self.request.translate
107 153
108 154 if not is_head:
109 155 message = _('Cannot modify file. '
110 156 'Given commit `{}` is not head of a branch.').format(commit_id)
111 157 h.flash(message, category='warning')
112 158
113 159 if json_mode:
114 160 return message
115 161
116 162 files_url = h.route_path(
117 163 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 164 f_path=f_path)
119 165 raise HTTPFound(files_url)
120 166
121 167 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 168 _ = self.request.translate
123 169
124 170 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 171 self.db_repo_name, branch_name)
126 172 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 173 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 174 h.escape(branch_name), h.escape(rule))
129 175 h.flash(message, 'warning')
130 176
131 177 if json_mode:
132 178 return message
133 179
134 180 files_url = h.route_path(
135 181 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 182
137 183 raise HTTPFound(files_url)
138 184
139 185 def _get_commit_and_path(self):
140 186 default_commit_id = self.db_repo.landing_ref_name
141 187 default_f_path = '/'
142 188
143 189 commit_id = self.request.matchdict.get(
144 190 'commit_id', default_commit_id)
145 191 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 192 return commit_id, f_path
147 193
148 194 def _get_default_encoding(self, c):
149 195 enc_list = getattr(c, 'default_encodings', [])
150 196 return enc_list[0] if enc_list else 'UTF-8'
151 197
152 198 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 199 """
154 200 This is a safe way to get commit. If an error occurs it redirects to
155 201 tip with proper message
156 202
157 203 :param commit_id: id of commit to fetch
158 204 :param redirect_after: toggle redirection
159 205 """
160 206 _ = self.request.translate
161 207
162 208 try:
163 209 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 210 except EmptyRepositoryError:
165 211 if not redirect_after:
166 212 return None
167 213
168 _url = h.route_path(
169 'repo_files_add_file',
170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171
214 add_new = upload_new = ""
172 215 if h.HasRepoPermissionAny(
173 216 'repository.write', 'repository.admin')(self.db_repo_name):
217 _url = h.route_path(
218 'repo_files_add_file',
219 repo_name=self.db_repo_name, commit_id=0, f_path='')
174 220 add_new = h.link_to(
175 _('Click here to add a new file.'), _url, class_="alert-link")
176 else:
177 add_new = ""
221 _('add a new file'), _url, class_="alert-link")
222
223 _url_upld = h.route_path(
224 'repo_files_upload_file',
225 repo_name=self.db_repo_name, commit_id=0, f_path='')
226 upload_new = h.link_to(
227 _('upload a new file'), _url_upld, class_="alert-link")
178 228
179 229 h.flash(h.literal(
180 _('There are no files yet. %s') % add_new), category='warning')
230 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
181 231 raise HTTPFound(
182 232 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 233
184 234 except (CommitDoesNotExistError, LookupError) as e:
185 235 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 236 h.flash(msg, category='error')
187 237 raise HTTPNotFound()
188 238 except RepositoryError as e:
189 239 h.flash(h.escape(safe_str(e)), category='error')
190 240 raise HTTPNotFound()
191 241
192 def _get_filenode_or_redirect(self, commit_obj, path):
242 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
193 243 """
194 244 Returns file_node, if error occurs or given path is directory,
195 245 it'll redirect to top level path
196 246 """
197 247 _ = self.request.translate
198 248
199 249 try:
200 file_node = commit_obj.get_node(path)
250 file_node = commit_obj.get_node(path, pre_load=pre_load)
201 251 if file_node.is_dir():
202 252 raise RepositoryError('The given path is a directory')
203 253 except CommitDoesNotExistError:
204 254 log.exception('No such commit exists for this repository')
205 255 h.flash(_('No such commit exists for this repository'), category='error')
206 256 raise HTTPNotFound()
207 257 except RepositoryError as e:
208 258 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 259 h.flash(h.escape(safe_str(e)), category='error')
210 260 raise HTTPNotFound()
211 261
212 262 return file_node
213 263
214 264 def _is_valid_head(self, commit_id, repo, landing_ref):
215 265 branch_name = sha_commit_id = ''
216 266 is_head = False
217 267 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 268
219 269 for _branch_name, branch_commit_id in repo.branches.items():
220 270 # simple case we pass in branch name, it's a HEAD
221 271 if commit_id == _branch_name:
222 272 is_head = True
223 273 branch_name = _branch_name
224 274 sha_commit_id = branch_commit_id
225 275 break
226 276 # case when we pass in full sha commit_id, which is a head
227 277 elif commit_id == branch_commit_id:
228 278 is_head = True
229 279 branch_name = _branch_name
230 280 sha_commit_id = branch_commit_id
231 281 break
232 282
233 283 if h.is_svn(repo) and not repo.is_empty():
234 284 # Note: Subversion only has one head.
235 285 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 286 is_head = True
237 287 return branch_name, sha_commit_id, is_head
238 288
239 289 # checked branches, means we only need to try to get the branch/commit_sha
240 290 if repo.is_empty():
241 291 is_head = True
242 292 branch_name = landing_ref
243 293 sha_commit_id = EmptyCommit().raw_id
244 294 else:
245 295 commit = repo.get_commit(commit_id=commit_id)
246 296 if commit:
247 297 branch_name = commit.branch
248 298 sha_commit_id = commit.raw_id
249 299
250 300 return branch_name, sha_commit_id, is_head
251 301
252 302 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
253 303
254 304 repo_id = self.db_repo.repo_id
255 305 force_recache = self.get_recache_flag()
256 306
257 307 cache_seconds = safe_int(
258 308 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
259 309 cache_on = not force_recache and cache_seconds > 0
260 310 log.debug(
261 311 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
262 312 'with caching: %s[TTL: %ss]' % (
263 313 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
264 314
265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
315 cache_namespace_uid = 'repo.{}'.format(repo_id)
266 316 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
267 317
268 318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
269 319 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
270 320 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
271 321 ver, _repo_id, _commit_id, _f_path)
272 322
273 323 c.full_load = _full_load
274 324 return render(
275 325 'rhodecode:templates/files/files_browser_tree.mako',
276 326 self._get_template_context(c), self.request, _at_rev)
277 327
278 328 return compute_file_tree(
279 329 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
280 330 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
281 331
282 def _get_archive_spec(self, fname):
283 log.debug('Detecting archive spec for: `%s`', fname)
284
285 fileformat = None
286 ext = None
287 content_type = None
288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
289
290 if fname.endswith(extension):
291 fileformat = a_type
292 log.debug('archive is of type: %s', fileformat)
293 ext = extension
294 break
295
296 if not fileformat:
297 raise ValueError()
298
299 # left over part of whole fname is the commit
300 commit_id = fname[:-len(ext)]
301
302 return commit_id, ext, fileformat, content_type
303
304 332 def create_pure_path(self, *parts):
305 333 # Split paths and sanitize them, removing any ../ etc
306 334 sanitized_path = [
307 335 x for x in pathlib.PurePath(*parts).parts
308 336 if x not in ['.', '..']]
309 337
310 338 pure_path = pathlib.PurePath(*sanitized_path)
311 339 return pure_path
312 340
313 341 def _is_lf_enabled(self, target_repo):
314 342 lf_enabled = False
315 343
316 344 lf_key_for_vcs_map = {
317 345 'hg': 'extensions_largefiles',
318 346 'git': 'vcs_git_lfs_enabled'
319 347 }
320 348
321 349 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
322 350
323 351 if lf_key_for_vcs:
324 352 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
325 353
326 354 return lf_enabled
327 355
328 def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
329 # original backward compat name of archive
330 clean_name = safe_str(db_repo_name.replace('/', '_'))
331
332 # e.g vcsserver.zip
333 # e.g vcsserver-abcdefgh.zip
334 # e.g vcsserver-abcdefgh-defghijk.zip
335 archive_name = '{}{}{}{}{}{}'.format(
336 clean_name,
337 '-sub' if subrepos else '',
338 commit_sha,
339 '-{}'.format('plain') if not with_hash else '',
340 '-{}'.format(path_sha) if path_sha else '',
341 ext)
342 return archive_name
343
344 356 @LoginRequired()
345 357 @HasRepoPermissionAnyDecorator(
346 358 'repository.read', 'repository.write', 'repository.admin')
347 359 def repo_archivefile(self):
348 360 # archive cache config
349 361 from rhodecode import CONFIG
350 362 _ = self.request.translate
351 363 self.load_default_context()
352 364 default_at_path = '/'
353 365 fname = self.request.matchdict['fname']
354 366 subrepos = self.request.GET.get('subrepos') == 'true'
355 367 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
356 368 at_path = self.request.GET.get('at_path') or default_at_path
357 369
358 370 if not self.db_repo.enable_downloads:
359 371 return Response(_('Downloads disabled'))
360 372
361 373 try:
362 374 commit_id, ext, fileformat, content_type = \
363 self._get_archive_spec(fname)
375 _get_archive_spec(fname)
364 376 except ValueError:
365 377 return Response(_('Unknown archive type for: `{}`').format(
366 378 h.escape(fname)))
367 379
368 380 try:
369 381 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
370 382 except CommitDoesNotExistError:
371 383 return Response(_('Unknown commit_id {}').format(
372 384 h.escape(commit_id)))
373 385 except EmptyRepositoryError:
374 386 return Response(_('Empty repository'))
375 387
376 388 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
377 389 if commit_id != commit.raw_id:
378 390 fname='{}{}'.format(commit.raw_id, ext)
379 391 raise HTTPFound(self.request.current_route_path(fname=fname))
380 392
381 393 try:
382 394 at_path = commit.get_node(at_path).path or default_at_path
383 395 except Exception:
384 396 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
385 397
386 # path sha is part of subdir
387 path_sha = ''
388 if at_path != default_at_path:
389 path_sha = sha1(at_path)[:8]
390 short_sha = '-{}'.format(safe_str(commit.short_id))
391 # used for cache etc
392 archive_name = self._get_archive_name(
393 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
394 path_sha=path_sha, with_hash=with_hash)
398 path_sha = get_path_sha(at_path)
399
400 # used for cache etc, consistent unique archive name
401 archive_name_key = get_archive_name(
402 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 path_sha=path_sha, with_hash=True)
395 404
396 405 if not with_hash:
397 short_sha = ''
398 406 path_sha = ''
399 407
400 408 # what end client gets served
401 response_archive_name = self._get_archive_name(
402 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
409 response_archive_name = get_archive_name(
410 self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 411 path_sha=path_sha, with_hash=with_hash)
412
404 413 # remove extension from our archive directory name
405 414 archive_dir_name = response_archive_name[:-len(ext)]
406 415
407 use_cached_archive = False
408 archive_cache_dir = CONFIG.get('archive_cache_dir')
409 archive_cache_enabled = archive_cache_dir and not self.request.GET.get('no_cache')
410 cached_archive_path = None
416 archive_cache_disable = self.request.GET.get('no_cache')
417
418 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
420 d_cache_conf = get_archival_config(config=CONFIG)
411 421
412 if archive_cache_enabled:
413 # check if we it's ok to write, and re-create the archive cache
414 if not os.path.isdir(CONFIG['archive_cache_dir']):
415 os.makedirs(CONFIG['archive_cache_dir'])
416
417 cached_archive_path = os.path.join(
418 CONFIG['archive_cache_dir'], archive_name)
419 if os.path.isfile(cached_archive_path):
420 log.debug('Found cached archive in %s', cached_archive_path)
421 fd, archive = None, cached_archive_path
422 reentrant_lock_key = archive_name_key + '.lock'
423 with ReentrantLock(d_cache, reentrant_lock_key):
424 # This is also a cache key
425 use_cached_archive = False
426 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)
422 428 use_cached_archive = True
429 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
430 archive_name_key, tag, reader.name)
423 431 else:
424 log.debug('Archive %s is not yet cached', archive_name)
432 reader = None
433 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
425 434
426 # generate new archive, as previous was not found in the cache
427 if not use_cached_archive:
428 _dir = os.path.abspath(archive_cache_dir) if archive_cache_dir else None
429 fd, archive = tempfile.mkstemp(dir=_dir)
430 log.debug('Creating new temp archive in %s', archive)
431 try:
432 commit.archive_repo(archive, archive_dir_name=archive_dir_name,
433 kind=fileformat, subrepos=subrepos,
434 archive_at_path=at_path)
435 except ImproperArchiveTypeError:
436 return _('Unknown archive type')
437 if archive_cache_enabled:
438 # if we generated the archive and we have cache enabled
439 # let's use this for future
440 log.debug('Storing new archive in %s', cached_archive_path)
441 shutil.move(archive, cached_archive_path)
442 archive = cached_archive_path
435 # generate new archive, as previous was not found in the cache
436 if not reader:
437 # first remove expired items, before generating a new one :)
438 # we di this manually because automatic eviction is disabled
439 d_cache.cull(retry=True)
440
441 try:
442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 kind=fileformat, subrepos=subrepos,
444 archive_at_path=at_path, cache_config=d_cache_conf)
445 except ImproperArchiveTypeError:
446 return _('Unknown archive type')
443 447
444 # store download action
445 audit_logger.store_web(
446 'repo.archive.download', action_data={
447 'user_agent': self.request.user_agent,
448 'archive_name': archive_name,
449 'archive_spec': fname,
450 'archive_cached': use_cached_archive},
451 user=self._rhodecode_user,
452 repo=self.db_repo,
453 commit=True
454 )
448 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
449
450 if not reader:
451 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
455 452
456 def get_chunked_archive(archive_path):
457 with open(archive_path, 'rb') as stream:
458 while True:
459 data = stream.read(16 * 1024)
460 if not data:
461 if fd: # fd means we used temporary file
462 os.close(fd)
463 if not archive_cache_enabled:
464 log.debug('Destroying temp archive %s', archive_path)
465 os.remove(archive_path)
466 break
467 yield data
453 def archive_iterator(_reader):
454 while 1:
455 data = _reader.read(1024)
456 if not data:
457 break
458 yield data
468 459
469 response = Response(app_iter=get_chunked_archive(archive))
470 response.content_disposition = str('attachment; filename=%s' % response_archive_name)
460 response = Response(app_iter=archive_iterator(reader))
461 response.content_disposition = f'attachment; filename={response_archive_name}'
471 462 response.content_type = str(content_type)
472 463
473 return response
464 try:
465 return response
466 finally:
467 # store download action
468 audit_logger.store_web(
469 'repo.archive.download', action_data={
470 'user_agent': self.request.user_agent,
471 'archive_name': archive_name_key,
472 'archive_spec': fname,
473 'archive_cached': use_cached_archive},
474 user=self._rhodecode_user,
475 repo=self.db_repo,
476 commit=True
477 )
474 478
475 479 def _get_file_node(self, commit_id, f_path):
476 480 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
477 481 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
478 482 try:
479 483 node = commit.get_node(f_path)
480 484 if node.is_dir():
481 raise NodeError('%s path is a %s not a file'
482 % (node, type(node)))
485 raise NodeError(f'{node} path is a {type(node)} not a file')
483 486 except NodeDoesNotExistError:
484 487 commit = EmptyCommit(
485 488 commit_id=commit_id,
486 489 idx=commit.idx,
487 490 repo=commit.repository,
488 491 alias=commit.repository.alias,
489 492 message=commit.message,
490 493 author=commit.author,
491 494 date=commit.date)
492 node = FileNode(f_path, '', commit=commit)
495 node = FileNode(safe_bytes(f_path), b'', commit=commit)
493 496 else:
494 497 commit = EmptyCommit(
495 498 repo=self.rhodecode_vcs_repo,
496 499 alias=self.rhodecode_vcs_repo.alias)
497 node = FileNode(f_path, '', commit=commit)
500 node = FileNode(safe_bytes(f_path), b'', commit=commit)
498 501 return node
499 502
500 503 @LoginRequired()
501 504 @HasRepoPermissionAnyDecorator(
502 505 'repository.read', 'repository.write', 'repository.admin')
503 506 def repo_files_diff(self):
504 507 c = self.load_default_context()
505 508 f_path = self._get_f_path(self.request.matchdict)
506 509 diff1 = self.request.GET.get('diff1', '')
507 510 diff2 = self.request.GET.get('diff2', '')
508 511
509 512 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
510 513
511 514 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
512 515 line_context = self.request.GET.get('context', 3)
513 516
514 517 if not any((diff1, diff2)):
515 518 h.flash(
516 519 'Need query parameter "diff1" or "diff2" to generate a diff.',
517 520 category='error')
518 521 raise HTTPBadRequest()
519 522
520 523 c.action = self.request.GET.get('diff')
521 524 if c.action not in ['download', 'raw']:
522 525 compare_url = h.route_path(
523 526 'repo_compare',
524 527 repo_name=self.db_repo_name,
525 528 source_ref_type='rev',
526 529 source_ref=diff1,
527 530 target_repo=self.db_repo_name,
528 531 target_ref_type='rev',
529 532 target_ref=diff2,
530 533 _query=dict(f_path=f_path))
531 534 # redirect to new view if we render diff
532 535 raise HTTPFound(compare_url)
533 536
534 537 try:
535 538 node1 = self._get_file_node(diff1, path1)
536 539 node2 = self._get_file_node(diff2, f_path)
537 540 except (RepositoryError, NodeError):
538 541 log.exception("Exception while trying to get node from repository")
539 542 raise HTTPFound(
540 543 h.route_path('repo_files', repo_name=self.db_repo_name,
541 544 commit_id='tip', f_path=f_path))
542 545
543 546 if all(isinstance(node.commit, EmptyCommit)
544 547 for node in (node1, node2)):
545 548 raise HTTPNotFound()
546 549
547 550 c.commit_1 = node1.commit
548 551 c.commit_2 = node2.commit
549 552
550 553 if c.action == 'download':
551 554 _diff = diffs.get_gitdiff(node1, node2,
552 555 ignore_whitespace=ignore_whitespace,
553 556 context=line_context)
554 diff = diffs.DiffProcessor(_diff, format='gitdiff')
557 # NOTE: this was using diff_format='gitdiff'
558 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
555 559
556 560 response = Response(self.path_filter.get_raw_patch(diff))
557 561 response.content_type = 'text/plain'
558 562 response.content_disposition = (
559 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
563 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
560 564 )
561 565 charset = self._get_default_encoding(c)
562 566 if charset:
563 567 response.charset = charset
564 568 return response
565 569
566 570 elif c.action == 'raw':
567 571 _diff = diffs.get_gitdiff(node1, node2,
568 572 ignore_whitespace=ignore_whitespace,
569 573 context=line_context)
570 diff = diffs.DiffProcessor(_diff, format='gitdiff')
574 # NOTE: this was using diff_format='gitdiff'
575 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
571 576
572 577 response = Response(self.path_filter.get_raw_patch(diff))
573 578 response.content_type = 'text/plain'
574 579 charset = self._get_default_encoding(c)
575 580 if charset:
576 581 response.charset = charset
577 582 return response
578 583
579 584 # in case we ever end up here
580 585 raise HTTPNotFound()
581 586
582 587 @LoginRequired()
583 588 @HasRepoPermissionAnyDecorator(
584 589 'repository.read', 'repository.write', 'repository.admin')
585 590 def repo_files_diff_2way_redirect(self):
586 591 """
587 592 Kept only to make OLD links work
588 593 """
589 594 f_path = self._get_f_path_unchecked(self.request.matchdict)
590 595 diff1 = self.request.GET.get('diff1', '')
591 596 diff2 = self.request.GET.get('diff2', '')
592 597
593 598 if not any((diff1, diff2)):
594 599 h.flash(
595 600 'Need query parameter "diff1" or "diff2" to generate a diff.',
596 601 category='error')
597 602 raise HTTPBadRequest()
598 603
599 604 compare_url = h.route_path(
600 605 'repo_compare',
601 606 repo_name=self.db_repo_name,
602 607 source_ref_type='rev',
603 608 source_ref=diff1,
604 609 target_ref_type='rev',
605 610 target_ref=diff2,
606 611 _query=dict(f_path=f_path, diffmode='sideside',
607 612 target_repo=self.db_repo_name,))
608 613 raise HTTPFound(compare_url)
609 614
610 615 @LoginRequired()
611 616 def repo_files_default_commit_redirect(self):
612 617 """
613 618 Special page that redirects to the landing page of files based on the default
614 619 commit for repository
615 620 """
616 621 c = self.load_default_context()
617 622 ref_name = c.rhodecode_db_repo.landing_ref_name
618 623 landing_url = h.repo_files_by_ref_url(
619 624 c.rhodecode_db_repo.repo_name,
620 625 c.rhodecode_db_repo.repo_type,
621 626 f_path='',
622 627 ref_name=ref_name,
623 628 commit_id='tip',
624 629 query=dict(at=ref_name)
625 630 )
626 631
627 632 raise HTTPFound(landing_url)
628 633
629 634 @LoginRequired()
630 635 @HasRepoPermissionAnyDecorator(
631 636 'repository.read', 'repository.write', 'repository.admin')
632 637 def repo_files(self):
633 638 c = self.load_default_context()
634 639
635 640 view_name = getattr(self.request.matched_route, 'name', None)
636 641
637 642 c.annotate = view_name == 'repo_files:annotated'
638 643 # default is false, but .rst/.md files later are auto rendered, we can
639 644 # overwrite auto rendering by setting this GET flag
640 c.renderer = view_name == 'repo_files:rendered' or \
641 not self.request.GET.get('no-render', False)
645 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
642 646
643 647 commit_id, f_path = self._get_commit_and_path()
644 648
645 649 c.commit = self._get_commit_or_redirect(commit_id)
646 650 c.branch = self.request.GET.get('branch', None)
647 651 c.f_path = f_path
648 652 at_rev = self.request.GET.get('at')
649 653
650 654 # prev link
651 655 try:
652 656 prev_commit = c.commit.prev(c.branch)
653 657 c.prev_commit = prev_commit
654 658 c.url_prev = h.route_path(
655 659 'repo_files', repo_name=self.db_repo_name,
656 660 commit_id=prev_commit.raw_id, f_path=f_path)
657 661 if c.branch:
658 662 c.url_prev += '?branch=%s' % c.branch
659 663 except (CommitDoesNotExistError, VCSError):
660 664 c.url_prev = '#'
661 665 c.prev_commit = EmptyCommit()
662 666
663 667 # next link
664 668 try:
665 669 next_commit = c.commit.next(c.branch)
666 670 c.next_commit = next_commit
667 671 c.url_next = h.route_path(
668 672 'repo_files', repo_name=self.db_repo_name,
669 673 commit_id=next_commit.raw_id, f_path=f_path)
670 674 if c.branch:
671 675 c.url_next += '?branch=%s' % c.branch
672 676 except (CommitDoesNotExistError, VCSError):
673 677 c.url_next = '#'
674 678 c.next_commit = EmptyCommit()
675 679
676 680 # files or dirs
677 681 try:
678 c.file = c.commit.get_node(f_path)
682 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
679 683
680 684 c.file_author = True
681 685 c.file_tree = ''
682 686
683 687 # load file content
684 688 if c.file.is_file():
685 689 c.lf_node = {}
686 690
687 691 has_lf_enabled = self._is_lf_enabled(self.db_repo)
688 692 if has_lf_enabled:
689 693 c.lf_node = c.file.get_largefile_node()
690 694
691 695 c.file_source_page = 'true'
692 696 c.file_last_commit = c.file.last_commit
693 697
694 698 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
695 699
696 700 if not (c.file_size_too_big or c.file.is_binary):
697 701 if c.annotate: # annotation has precedence over renderer
698 702 c.annotated_lines = filenode_as_annotated_lines_tokens(
699 703 c.file
700 704 )
701 705 else:
702 706 c.renderer = (
703 707 c.renderer and h.renderer_from_filename(c.file.path)
704 708 )
705 709 if not c.renderer:
706 710 c.lines = filenode_as_lines_tokens(c.file)
707 711
708 712 _branch_name, _sha_commit_id, is_head = \
709 713 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
710 714 landing_ref=self.db_repo.landing_ref_name)
711 715 c.on_branch_head = is_head
712 716
713 717 branch = c.commit.branch if (
714 718 c.commit.branch and '/' not in c.commit.branch) else None
715 719 c.branch_or_raw_id = branch or c.commit.raw_id
716 720 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
717 721
718 722 author = c.file_last_commit.author
719 723 c.authors = [[
720 724 h.email(author),
721 725 h.person(author, 'username_or_name_or_email'),
722 726 1
723 727 ]]
724 728
725 729 else: # load tree content at path
726 730 c.file_source_page = 'false'
727 731 c.authors = []
728 732 # this loads a simple tree without metadata to speed things up
729 733 # later via ajax we call repo_nodetree_full and fetch whole
730 734 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
731 735
732 736 c.readme_data, c.readme_file = \
733 737 self._get_readme_data(self.db_repo, c.visual.default_renderer,
734 738 c.commit.raw_id, f_path)
735 739
736 740 except RepositoryError as e:
737 741 h.flash(h.escape(safe_str(e)), category='error')
738 742 raise HTTPNotFound()
739 743
740 744 if self.request.environ.get('HTTP_X_PJAX'):
741 745 html = render('rhodecode:templates/files/files_pjax.mako',
742 746 self._get_template_context(c), self.request)
743 747 else:
744 748 html = render('rhodecode:templates/files/files.mako',
745 749 self._get_template_context(c), self.request)
746 750 return Response(html)
747 751
748 752 @HasRepoPermissionAnyDecorator(
749 753 'repository.read', 'repository.write', 'repository.admin')
750 754 def repo_files_annotated_previous(self):
751 755 self.load_default_context()
752 756
753 757 commit_id, f_path = self._get_commit_and_path()
754 758 commit = self._get_commit_or_redirect(commit_id)
755 759 prev_commit_id = commit.raw_id
756 760 line_anchor = self.request.GET.get('line_anchor')
757 761 is_file = False
758 762 try:
759 763 _file = commit.get_node(f_path)
760 764 is_file = _file.is_file()
761 765 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
762 766 pass
763 767
764 768 if is_file:
765 769 history = commit.get_path_history(f_path)
766 770 prev_commit_id = history[1].raw_id \
767 771 if len(history) > 1 else prev_commit_id
768 772 prev_url = h.route_path(
769 773 'repo_files:annotated', repo_name=self.db_repo_name,
770 774 commit_id=prev_commit_id, f_path=f_path,
771 775 _anchor='L{}'.format(line_anchor))
772 776
773 777 raise HTTPFound(prev_url)
774 778
775 779 @LoginRequired()
776 780 @HasRepoPermissionAnyDecorator(
777 781 'repository.read', 'repository.write', 'repository.admin')
778 782 def repo_nodetree_full(self):
779 783 """
780 784 Returns rendered html of file tree that contains commit date,
781 785 author, commit_id for the specified combination of
782 786 repo, commit_id and file path
783 787 """
784 788 c = self.load_default_context()
785 789
786 790 commit_id, f_path = self._get_commit_and_path()
787 791 commit = self._get_commit_or_redirect(commit_id)
788 792 try:
789 793 dir_node = commit.get_node(f_path)
790 794 except RepositoryError as e:
791 795 return Response('error: {}'.format(h.escape(safe_str(e))))
792 796
793 797 if dir_node.is_file():
794 798 return Response('')
795 799
796 800 c.file = dir_node
797 801 c.commit = commit
798 802 at_rev = self.request.GET.get('at')
799 803
800 804 html = self._get_tree_at_commit(
801 805 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
802 806
803 807 return Response(html)
804 808
805 809 def _get_attachement_headers(self, f_path):
806 810 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
807 811 safe_path = f_name.replace('"', '\\"')
808 812 encoded_path = urllib.parse.quote(f_name)
809 813
810 814 return "attachment; " \
811 815 "filename=\"{}\"; " \
812 816 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
813 817
814 818 @LoginRequired()
815 819 @HasRepoPermissionAnyDecorator(
816 820 'repository.read', 'repository.write', 'repository.admin')
817 821 def repo_file_raw(self):
818 822 """
819 823 Action for show as raw, some mimetypes are "rendered",
820 824 those include images, icons.
821 825 """
822 826 c = self.load_default_context()
823 827
824 828 commit_id, f_path = self._get_commit_and_path()
825 829 commit = self._get_commit_or_redirect(commit_id)
826 830 file_node = self._get_filenode_or_redirect(commit, f_path)
827 831
828 832 raw_mimetype_mapping = {
829 833 # map original mimetype to a mimetype used for "show as raw"
830 834 # you can also provide a content-disposition to override the
831 835 # default "attachment" disposition.
832 836 # orig_type: (new_type, new_dispo)
833 837
834 838 # show images inline:
835 839 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
836 840 # for example render an SVG with javascript inside or even render
837 841 # HTML.
838 842 'image/x-icon': ('image/x-icon', 'inline'),
839 843 'image/png': ('image/png', 'inline'),
840 844 'image/gif': ('image/gif', 'inline'),
841 845 'image/jpeg': ('image/jpeg', 'inline'),
842 846 'application/pdf': ('application/pdf', 'inline'),
843 847 }
844 848
845 849 mimetype = file_node.mimetype
846 850 try:
847 851 mimetype, disposition = raw_mimetype_mapping[mimetype]
848 852 except KeyError:
849 853 # we don't know anything special about this, handle it safely
850 854 if file_node.is_binary:
851 855 # do same as download raw for binary files
852 856 mimetype, disposition = 'application/octet-stream', 'attachment'
853 857 else:
854 858 # do not just use the original mimetype, but force text/plain,
855 859 # otherwise it would serve text/html and that might be unsafe.
856 860 # Note: underlying vcs library fakes text/plain mimetype if the
857 861 # mimetype can not be determined and it thinks it is not
858 862 # binary.This might lead to erroneous text display in some
859 863 # cases, but helps in other cases, like with text files
860 864 # without extension.
861 865 mimetype, disposition = 'text/plain', 'inline'
862 866
863 867 if disposition == 'attachment':
864 868 disposition = self._get_attachement_headers(f_path)
865 869
866 870 stream_content = file_node.stream_bytes()
867 871
868 872 response = Response(app_iter=stream_content)
869 873 response.content_disposition = disposition
870 874 response.content_type = mimetype
871 875
872 876 charset = self._get_default_encoding(c)
873 877 if charset:
874 878 response.charset = charset
875 879
876 880 return response
877 881
878 882 @LoginRequired()
879 883 @HasRepoPermissionAnyDecorator(
880 884 'repository.read', 'repository.write', 'repository.admin')
881 885 def repo_file_download(self):
882 886 c = self.load_default_context()
883 887
884 888 commit_id, f_path = self._get_commit_and_path()
885 889 commit = self._get_commit_or_redirect(commit_id)
886 890 file_node = self._get_filenode_or_redirect(commit, f_path)
887 891
888 892 if self.request.GET.get('lf'):
889 893 # only if lf get flag is passed, we download this file
890 894 # as LFS/Largefile
891 895 lf_node = file_node.get_largefile_node()
892 896 if lf_node:
893 897 # overwrite our pointer with the REAL large-file
894 898 file_node = lf_node
895 899
896 900 disposition = self._get_attachement_headers(f_path)
897 901
898 902 stream_content = file_node.stream_bytes()
899 903
900 904 response = Response(app_iter=stream_content)
901 905 response.content_disposition = disposition
902 906 response.content_type = file_node.mimetype
903 907
904 908 charset = self._get_default_encoding(c)
905 909 if charset:
906 910 response.charset = charset
907 911
908 912 return response
909 913
910 914 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
911 915
912 916 cache_seconds = safe_int(
913 917 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
914 918 cache_on = cache_seconds > 0
915 919 log.debug(
916 920 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
917 921 'with caching: %s[TTL: %ss]' % (
918 922 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
919 923
920 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
924 cache_namespace_uid = 'repo.{}'.format(repo_id)
921 925 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
922 926
923 927 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
924 928 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
925 929 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
926 930 _repo_id, commit_id, f_path)
927 931 try:
928 932 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
929 933 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
930 934 log.exception(safe_str(e))
931 935 h.flash(h.escape(safe_str(e)), category='error')
932 936 raise HTTPFound(h.route_path(
933 937 'repo_files', repo_name=self.db_repo_name,
934 938 commit_id='tip', f_path='/'))
935 939
936 940 return _d + _f
937 941
938 942 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
939 943 commit_id, f_path)
940 944 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
941 945
942 946 @LoginRequired()
943 947 @HasRepoPermissionAnyDecorator(
944 948 'repository.read', 'repository.write', 'repository.admin')
945 949 def repo_nodelist(self):
946 950 self.load_default_context()
947 951
948 952 commit_id, f_path = self._get_commit_and_path()
949 953 commit = self._get_commit_or_redirect(commit_id)
950 954
951 955 metadata = self._get_nodelist_at_commit(
952 956 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
953 return {'nodes': metadata}
957 return {'nodes': [x for x in metadata]}
954 958
955 959 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
956 960 items = []
957 961 for name, commit_id in branches_or_tags.items():
958 962 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
959 963 items.append((sym_ref, name, ref_type))
960 964 return items
961 965
962 966 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
963 967 return commit_id
964 968
965 969 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
966 970 return commit_id
967 971
968 972 # NOTE(dan): old code we used in "diff" mode compare
969 973 new_f_path = vcspath.join(name, f_path)
970 return u'%s@%s' % (new_f_path, commit_id)
974 return f'{new_f_path}@{commit_id}'
971 975
972 976 def _get_node_history(self, commit_obj, f_path, commits=None):
973 977 """
974 978 get commit history for given node
975 979
976 980 :param commit_obj: commit to calculate history
977 981 :param f_path: path for node to calculate history for
978 982 :param commits: if passed don't calculate history and take
979 983 commits defined in this list
980 984 """
981 985 _ = self.request.translate
982 986
983 987 # calculate history based on tip
984 988 tip = self.rhodecode_vcs_repo.get_commit()
985 989 if commits is None:
986 990 pre_load = ["author", "branch"]
987 991 try:
988 992 commits = tip.get_path_history(f_path, pre_load=pre_load)
989 993 except (NodeDoesNotExistError, CommitError):
990 994 # this node is not present at tip!
991 995 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
992 996
993 997 history = []
994 998 commits_group = ([], _("Changesets"))
995 999 for commit in commits:
996 1000 branch = ' (%s)' % commit.branch if commit.branch else ''
997 1001 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
998 1002 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
999 1003 history.append(commits_group)
1000 1004
1001 1005 symbolic_reference = self._symbolic_reference
1002 1006
1003 1007 if self.rhodecode_vcs_repo.alias == 'svn':
1004 1008 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1005 1009 f_path, self.rhodecode_vcs_repo)
1006 1010 if adjusted_f_path != f_path:
1007 1011 log.debug(
1008 1012 'Recognized svn tag or branch in file "%s", using svn '
1009 1013 'specific symbolic references', f_path)
1010 1014 f_path = adjusted_f_path
1011 1015 symbolic_reference = self._symbolic_reference_svn
1012 1016
1013 1017 branches = self._create_references(
1014 1018 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1015 1019 branches_group = (branches, _("Branches"))
1016 1020
1017 1021 tags = self._create_references(
1018 1022 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1019 1023 tags_group = (tags, _("Tags"))
1020 1024
1021 1025 history.append(branches_group)
1022 1026 history.append(tags_group)
1023 1027
1024 1028 return history, commits
1025 1029
1026 1030 @LoginRequired()
1027 1031 @HasRepoPermissionAnyDecorator(
1028 1032 'repository.read', 'repository.write', 'repository.admin')
1029 1033 def repo_file_history(self):
1030 1034 self.load_default_context()
1031 1035
1032 1036 commit_id, f_path = self._get_commit_and_path()
1033 1037 commit = self._get_commit_or_redirect(commit_id)
1034 1038 file_node = self._get_filenode_or_redirect(commit, f_path)
1035 1039
1036 1040 if file_node.is_file():
1037 1041 file_history, _hist = self._get_node_history(commit, f_path)
1038 1042
1039 1043 res = []
1040 1044 for section_items, section in file_history:
1041 1045 items = []
1042 1046 for obj_id, obj_text, obj_type in section_items:
1043 1047 at_rev = ''
1044 1048 if obj_type in ['branch', 'bookmark', 'tag']:
1045 1049 at_rev = obj_text
1046 1050 entry = {
1047 1051 'id': obj_id,
1048 1052 'text': obj_text,
1049 1053 'type': obj_type,
1050 1054 'at_rev': at_rev
1051 1055 }
1052 1056
1053 1057 items.append(entry)
1054 1058
1055 1059 res.append({
1056 1060 'text': section,
1057 1061 'children': items
1058 1062 })
1059 1063
1060 1064 data = {
1061 1065 'more': False,
1062 1066 'results': res
1063 1067 }
1064 1068 return data
1065 1069
1066 1070 log.warning('Cannot fetch history for directory')
1067 1071 raise HTTPBadRequest()
1068 1072
1069 1073 @LoginRequired()
1070 1074 @HasRepoPermissionAnyDecorator(
1071 1075 'repository.read', 'repository.write', 'repository.admin')
1072 1076 def repo_file_authors(self):
1073 1077 c = self.load_default_context()
1074 1078
1075 1079 commit_id, f_path = self._get_commit_and_path()
1076 1080 commit = self._get_commit_or_redirect(commit_id)
1077 1081 file_node = self._get_filenode_or_redirect(commit, f_path)
1078 1082
1079 1083 if not file_node.is_file():
1080 1084 raise HTTPBadRequest()
1081 1085
1082 1086 c.file_last_commit = file_node.last_commit
1083 1087 if self.request.GET.get('annotate') == '1':
1084 1088 # use _hist from annotation if annotation mode is on
1085 1089 commit_ids = set(x[1] for x in file_node.annotate)
1086 1090 _hist = (
1087 1091 self.rhodecode_vcs_repo.get_commit(commit_id)
1088 1092 for commit_id in commit_ids)
1089 1093 else:
1090 1094 _f_history, _hist = self._get_node_history(commit, f_path)
1091 1095 c.file_author = False
1092 1096
1093 1097 unique = collections.OrderedDict()
1094 1098 for commit in _hist:
1095 1099 author = commit.author
1096 1100 if author not in unique:
1097 1101 unique[commit.author] = [
1098 1102 h.email(author),
1099 1103 h.person(author, 'username_or_name_or_email'),
1100 1104 1 # counter
1101 1105 ]
1102 1106
1103 1107 else:
1104 1108 # increase counter
1105 1109 unique[commit.author][2] += 1
1106 1110
1107 1111 c.authors = [val for val in unique.values()]
1108 1112
1109 1113 return self._get_template_context(c)
1110 1114
1111 1115 @LoginRequired()
1112 1116 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1113 1117 def repo_files_check_head(self):
1114 1118 self.load_default_context()
1115 1119
1116 1120 commit_id, f_path = self._get_commit_and_path()
1117 1121 _branch_name, _sha_commit_id, is_head = \
1118 1122 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1119 1123 landing_ref=self.db_repo.landing_ref_name)
1120 1124
1121 1125 new_path = self.request.POST.get('path')
1122 1126 operation = self.request.POST.get('operation')
1123 1127 path_exist = ''
1124 1128
1125 1129 if new_path and operation in ['create', 'upload']:
1126 1130 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1127 1131 try:
1128 1132 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1129 1133 # NOTE(dan): construct whole path without leading /
1130 1134 file_node = commit_obj.get_node(new_f_path)
1131 1135 if file_node is not None:
1132 1136 path_exist = new_f_path
1133 1137 except EmptyRepositoryError:
1134 1138 pass
1135 1139 except Exception:
1136 1140 pass
1137 1141
1138 1142 return {
1139 1143 'branch': _branch_name,
1140 1144 'sha': _sha_commit_id,
1141 1145 'is_head': is_head,
1142 1146 'path_exists': path_exist
1143 1147 }
1144 1148
1145 1149 @LoginRequired()
1146 1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1147 1151 def repo_files_remove_file(self):
1148 1152 _ = self.request.translate
1149 1153 c = self.load_default_context()
1150 1154 commit_id, f_path = self._get_commit_and_path()
1151 1155
1152 1156 self._ensure_not_locked()
1153 1157 _branch_name, _sha_commit_id, is_head = \
1154 1158 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1155 1159 landing_ref=self.db_repo.landing_ref_name)
1156 1160
1157 1161 self.forbid_non_head(is_head, f_path)
1158 1162 self.check_branch_permission(_branch_name)
1159 1163
1160 1164 c.commit = self._get_commit_or_redirect(commit_id)
1161 1165 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1162 1166
1163 1167 c.default_message = _(
1164 1168 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1165 1169 c.f_path = f_path
1166 1170
1167 1171 return self._get_template_context(c)
1168 1172
1169 1173 @LoginRequired()
1170 1174 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1171 1175 @CSRFRequired()
1172 1176 def repo_files_delete_file(self):
1173 1177 _ = self.request.translate
1174 1178
1175 1179 c = self.load_default_context()
1176 1180 commit_id, f_path = self._get_commit_and_path()
1177 1181
1178 1182 self._ensure_not_locked()
1179 1183 _branch_name, _sha_commit_id, is_head = \
1180 1184 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1181 1185 landing_ref=self.db_repo.landing_ref_name)
1182 1186
1183 1187 self.forbid_non_head(is_head, f_path)
1184 1188 self.check_branch_permission(_branch_name)
1185 1189
1186 1190 c.commit = self._get_commit_or_redirect(commit_id)
1187 1191 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1188 1192
1189 1193 c.default_message = _(
1190 1194 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1191 1195 c.f_path = f_path
1192 1196 node_path = f_path
1193 1197 author = self._rhodecode_db_user.full_contact
1194 1198 message = self.request.POST.get('message') or c.default_message
1195 1199 try:
1196 1200 nodes = {
1197 node_path: {
1198 'content': ''
1201 safe_bytes(node_path): {
1202 'content': b''
1199 1203 }
1200 1204 }
1201 1205 ScmModel().delete_nodes(
1202 1206 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1203 1207 message=message,
1204 1208 nodes=nodes,
1205 1209 parent_commit=c.commit,
1206 1210 author=author,
1207 1211 )
1208 1212
1209 1213 h.flash(
1210 1214 _('Successfully deleted file `{}`').format(
1211 1215 h.escape(f_path)), category='success')
1212 1216 except Exception:
1213 1217 log.exception('Error during commit operation')
1214 1218 h.flash(_('Error occurred during commit'), category='error')
1215 1219 raise HTTPFound(
1216 1220 h.route_path('repo_commit', repo_name=self.db_repo_name,
1217 1221 commit_id='tip'))
1218 1222
1219 1223 @LoginRequired()
1220 1224 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1221 1225 def repo_files_edit_file(self):
1222 1226 _ = self.request.translate
1223 1227 c = self.load_default_context()
1224 1228 commit_id, f_path = self._get_commit_and_path()
1225 1229
1226 1230 self._ensure_not_locked()
1227 1231 _branch_name, _sha_commit_id, is_head = \
1228 1232 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1229 1233 landing_ref=self.db_repo.landing_ref_name)
1230 1234
1231 1235 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1232 1236 self.check_branch_permission(_branch_name, commit_id=commit_id)
1233 1237
1234 1238 c.commit = self._get_commit_or_redirect(commit_id)
1235 1239 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1236 1240
1237 1241 if c.file.is_binary:
1238 1242 files_url = h.route_path(
1239 1243 'repo_files',
1240 1244 repo_name=self.db_repo_name,
1241 1245 commit_id=c.commit.raw_id, f_path=f_path)
1242 1246 raise HTTPFound(files_url)
1243 1247
1244 1248 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1245 1249 c.f_path = f_path
1246 1250
1247 1251 return self._get_template_context(c)
1248 1252
1249 1253 @LoginRequired()
1250 1254 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1251 1255 @CSRFRequired()
1252 1256 def repo_files_update_file(self):
1253 1257 _ = self.request.translate
1254 1258 c = self.load_default_context()
1255 1259 commit_id, f_path = self._get_commit_and_path()
1256 1260
1257 1261 self._ensure_not_locked()
1258 1262
1259 1263 c.commit = self._get_commit_or_redirect(commit_id)
1260 1264 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1261 1265
1262 1266 if c.file.is_binary:
1263 1267 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1264 1268 commit_id=c.commit.raw_id, f_path=f_path))
1265 1269
1266 1270 _branch_name, _sha_commit_id, is_head = \
1267 1271 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1268 1272 landing_ref=self.db_repo.landing_ref_name)
1269 1273
1270 1274 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1271 1275 self.check_branch_permission(_branch_name, commit_id=commit_id)
1272 1276
1273 1277 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1274 1278 c.f_path = f_path
1275 1279
1276 old_content = c.file.content
1280 old_content = c.file.str_content
1277 1281 sl = old_content.splitlines(1)
1278 1282 first_line = sl[0] if sl else ''
1279 1283
1280 1284 r_post = self.request.POST
1281 1285 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1282 1286 line_ending_mode = detect_mode(first_line, 0)
1283 1287 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1284 1288
1285 1289 message = r_post.get('message') or c.default_message
1286 org_node_path = c.file.unicode_path
1290
1291 org_node_path = c.file.str_path
1287 1292 filename = r_post['filename']
1288 1293
1289 1294 root_path = c.file.dir_path
1290 1295 pure_path = self.create_pure_path(root_path, filename)
1291 1296 node_path = pure_path.as_posix()
1292 1297
1293 1298 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1294 1299 commit_id=commit_id)
1295 1300 if content == old_content and node_path == org_node_path:
1296 1301 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1297 1302 category='warning')
1298 1303 raise HTTPFound(default_redirect_url)
1299 1304
1300 1305 try:
1301 1306 mapping = {
1302 org_node_path: {
1307 c.file.bytes_path: {
1303 1308 'org_filename': org_node_path,
1304 'filename': node_path,
1305 'content': content,
1309 'filename': safe_bytes(node_path),
1310 'content': safe_bytes(content),
1306 1311 'lexer': '',
1307 1312 'op': 'mod',
1308 1313 'mode': c.file.mode
1309 1314 }
1310 1315 }
1311 1316
1312 1317 commit = ScmModel().update_nodes(
1313 1318 user=self._rhodecode_db_user.user_id,
1314 1319 repo=self.db_repo,
1315 1320 message=message,
1316 1321 nodes=mapping,
1317 1322 parent_commit=c.commit,
1318 1323 )
1319 1324
1320 1325 h.flash(_('Successfully committed changes to file `{}`').format(
1321 1326 h.escape(f_path)), category='success')
1322 1327 default_redirect_url = h.route_path(
1323 1328 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1324 1329
1325 1330 except Exception:
1326 1331 log.exception('Error occurred during commit')
1327 1332 h.flash(_('Error occurred during commit'), category='error')
1328 1333
1329 1334 raise HTTPFound(default_redirect_url)
1330 1335
1331 1336 @LoginRequired()
1332 1337 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1333 1338 def repo_files_add_file(self):
1334 1339 _ = self.request.translate
1335 1340 c = self.load_default_context()
1336 1341 commit_id, f_path = self._get_commit_and_path()
1337 1342
1338 1343 self._ensure_not_locked()
1339 1344
1340 1345 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1341 1346 if c.commit is None:
1342 1347 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1343 1348
1344 1349 if self.rhodecode_vcs_repo.is_empty():
1345 1350 # for empty repository we cannot check for current branch, we rely on
1346 1351 # c.commit.branch instead
1347 1352 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1348 1353 else:
1349 1354 _branch_name, _sha_commit_id, is_head = \
1350 1355 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1351 1356 landing_ref=self.db_repo.landing_ref_name)
1352 1357
1353 1358 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1354 1359 self.check_branch_permission(_branch_name, commit_id=commit_id)
1355 1360
1356 1361 c.default_message = (_('Added file via RhodeCode Enterprise'))
1357 1362 c.f_path = f_path.lstrip('/') # ensure not relative path
1358 1363
1359 1364 return self._get_template_context(c)
1360 1365
1361 1366 @LoginRequired()
1362 1367 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1363 1368 @CSRFRequired()
1364 1369 def repo_files_create_file(self):
1365 1370 _ = self.request.translate
1366 1371 c = self.load_default_context()
1367 1372 commit_id, f_path = self._get_commit_and_path()
1368 1373
1369 1374 self._ensure_not_locked()
1370 1375
1371 1376 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1372 1377 if c.commit is None:
1373 1378 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1374 1379
1375 1380 # calculate redirect URL
1376 1381 if self.rhodecode_vcs_repo.is_empty():
1377 1382 default_redirect_url = h.route_path(
1378 1383 'repo_summary', repo_name=self.db_repo_name)
1379 1384 else:
1380 1385 default_redirect_url = h.route_path(
1381 1386 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1382 1387
1383 1388 if self.rhodecode_vcs_repo.is_empty():
1384 1389 # for empty repository we cannot check for current branch, we rely on
1385 1390 # c.commit.branch instead
1386 1391 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1387 1392 else:
1388 1393 _branch_name, _sha_commit_id, is_head = \
1389 1394 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1390 1395 landing_ref=self.db_repo.landing_ref_name)
1391 1396
1392 1397 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1393 1398 self.check_branch_permission(_branch_name, commit_id=commit_id)
1394 1399
1395 1400 c.default_message = (_('Added file via RhodeCode Enterprise'))
1396 1401 c.f_path = f_path
1397 1402
1398 1403 r_post = self.request.POST
1399 1404 message = r_post.get('message') or c.default_message
1400 1405 filename = r_post.get('filename')
1401 1406 unix_mode = 0
1402 1407
1403 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1404
1405 1408 if not filename:
1406 1409 # If there's no commit, redirect to repo summary
1407 1410 if type(c.commit) is EmptyCommit:
1408 1411 redirect_url = h.route_path(
1409 1412 'repo_summary', repo_name=self.db_repo_name)
1410 1413 else:
1411 1414 redirect_url = default_redirect_url
1412 1415 h.flash(_('No filename specified'), category='warning')
1413 1416 raise HTTPFound(redirect_url)
1414 1417
1415 1418 root_path = f_path
1416 1419 pure_path = self.create_pure_path(root_path, filename)
1417 1420 node_path = pure_path.as_posix().lstrip('/')
1418 1421
1419 1422 author = self._rhodecode_db_user.full_contact
1423 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1420 1424 nodes = {
1421 node_path: {
1422 'content': content
1425 safe_bytes(node_path): {
1426 'content': safe_bytes(content)
1423 1427 }
1424 1428 }
1425 1429
1426 1430 try:
1427 1431
1428 1432 commit = ScmModel().create_nodes(
1429 1433 user=self._rhodecode_db_user.user_id,
1430 1434 repo=self.db_repo,
1431 1435 message=message,
1432 1436 nodes=nodes,
1433 1437 parent_commit=c.commit,
1434 1438 author=author,
1435 1439 )
1436 1440
1437 1441 h.flash(_('Successfully committed new file `{}`').format(
1438 1442 h.escape(node_path)), category='success')
1439 1443
1440 1444 default_redirect_url = h.route_path(
1441 1445 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1442 1446
1443 1447 except NonRelativePathError:
1444 1448 log.exception('Non Relative path found')
1445 1449 h.flash(_('The location specified must be a relative path and must not '
1446 1450 'contain .. in the path'), category='warning')
1447 1451 raise HTTPFound(default_redirect_url)
1448 1452 except (NodeError, NodeAlreadyExistsError) as e:
1449 1453 h.flash(h.escape(safe_str(e)), category='error')
1450 1454 except Exception:
1451 1455 log.exception('Error occurred during commit')
1452 1456 h.flash(_('Error occurred during commit'), category='error')
1453 1457
1454 1458 raise HTTPFound(default_redirect_url)
1455 1459
1456 1460 @LoginRequired()
1457 1461 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1458 1462 @CSRFRequired()
1459 1463 def repo_files_upload_file(self):
1460 1464 _ = self.request.translate
1461 1465 c = self.load_default_context()
1462 1466 commit_id, f_path = self._get_commit_and_path()
1463 1467
1464 1468 self._ensure_not_locked()
1465 1469
1466 1470 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1467 1471 if c.commit is None:
1468 1472 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1469 1473
1470 1474 # calculate redirect URL
1471 1475 if self.rhodecode_vcs_repo.is_empty():
1472 1476 default_redirect_url = h.route_path(
1473 1477 'repo_summary', repo_name=self.db_repo_name)
1474 1478 else:
1475 1479 default_redirect_url = h.route_path(
1476 1480 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1477 1481
1478 1482 if self.rhodecode_vcs_repo.is_empty():
1479 1483 # for empty repository we cannot check for current branch, we rely on
1480 1484 # c.commit.branch instead
1481 1485 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1482 1486 else:
1483 1487 _branch_name, _sha_commit_id, is_head = \
1484 1488 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1485 1489 landing_ref=self.db_repo.landing_ref_name)
1486 1490
1487 1491 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1488 1492 if error:
1489 1493 return {
1490 1494 'error': error,
1491 1495 'redirect_url': default_redirect_url
1492 1496 }
1493 1497 error = self.check_branch_permission(_branch_name, json_mode=True)
1494 1498 if error:
1495 1499 return {
1496 1500 'error': error,
1497 1501 'redirect_url': default_redirect_url
1498 1502 }
1499 1503
1500 1504 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1501 1505 c.f_path = f_path
1502 1506
1503 1507 r_post = self.request.POST
1504 1508
1505 1509 message = c.default_message
1506 1510 user_message = r_post.getall('message')
1507 1511 if isinstance(user_message, list) and user_message:
1508 1512 # we take the first from duplicated results if it's not empty
1509 1513 message = user_message[0] if user_message[0] else message
1510 1514
1511 1515 nodes = {}
1512 1516
1513 1517 for file_obj in r_post.getall('files_upload') or []:
1514 1518 content = file_obj.file
1515 1519 filename = file_obj.filename
1516 1520
1517 1521 root_path = f_path
1518 1522 pure_path = self.create_pure_path(root_path, filename)
1519 1523 node_path = pure_path.as_posix().lstrip('/')
1520 1524
1521 nodes[node_path] = {
1525 nodes[safe_bytes(node_path)] = {
1522 1526 'content': content
1523 1527 }
1524 1528
1525 1529 if not nodes:
1526 1530 error = 'missing files'
1527 1531 return {
1528 1532 'error': error,
1529 1533 'redirect_url': default_redirect_url
1530 1534 }
1531 1535
1532 1536 author = self._rhodecode_db_user.full_contact
1533 1537
1534 1538 try:
1535 1539 commit = ScmModel().create_nodes(
1536 1540 user=self._rhodecode_db_user.user_id,
1537 1541 repo=self.db_repo,
1538 1542 message=message,
1539 1543 nodes=nodes,
1540 1544 parent_commit=c.commit,
1541 1545 author=author,
1542 1546 )
1543 1547 if len(nodes) == 1:
1544 1548 flash_message = _('Successfully committed {} new files').format(len(nodes))
1545 1549 else:
1546 1550 flash_message = _('Successfully committed 1 new file')
1547 1551
1548 1552 h.flash(flash_message, category='success')
1549 1553
1550 1554 default_redirect_url = h.route_path(
1551 1555 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1552 1556
1553 1557 except NonRelativePathError:
1554 1558 log.exception('Non Relative path found')
1555 1559 error = _('The location specified must be a relative path and must not '
1556 1560 'contain .. in the path')
1557 1561 h.flash(error, category='warning')
1558 1562
1559 1563 return {
1560 1564 'error': error,
1561 1565 'redirect_url': default_redirect_url
1562 1566 }
1563 1567 except (NodeError, NodeAlreadyExistsError) as e:
1564 1568 error = h.escape(e)
1565 1569 h.flash(error, category='error')
1566 1570
1567 1571 return {
1568 1572 'error': error,
1569 1573 'redirect_url': default_redirect_url
1570 1574 }
1571 1575 except Exception:
1572 1576 log.exception('Error occurred during commit')
1573 1577 error = _('Error occurred during commit')
1574 1578 h.flash(error, category='error')
1575 1579 return {
1576 1580 'error': error,
1577 1581 'redirect_url': default_redirect_url
1578 1582 }
1579 1583
1580 1584 return {
1581 1585 'error': None,
1582 1586 'redirect_url': default_redirect_url
1583 1587 }
@@ -1,1877 +1,1876 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib import ext_json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name,
83 83 search_q=search_q, statuses=statuses,
84 84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name,
87 87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, self._rhodecode_user.user_id,
91 91 search_q=search_q, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, self._rhodecode_user.user_id,
95 95 search_q=search_q, statuses=statuses)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments_count = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr,
110 110 include_drafts=False, count_only=True)
111 111
112 112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 114 if review_statuses and review_statuses[4]:
115 115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 116 my_review_status = statuses[0][1].status
117 117
118 118 data.append({
119 119 'name': _render('pullrequest_name',
120 120 pr.pull_request_id, pr.pull_request_state,
121 121 pr.work_in_progress, pr.target_repo.repo_name,
122 122 short=True),
123 123 'name_raw': pr.pull_request_id,
124 124 'status': _render('pullrequest_status',
125 125 pr.calculated_review_status()),
126 126 'my_status': _render('pullrequest_status',
127 127 my_review_status),
128 128 'title': _render('pullrequest_title', pr.title, pr.description),
129 129 'description': h.escape(pr.description),
130 130 'updated_on': _render('pullrequest_updated_on',
131 131 h.datetime_to_time(pr.updated_on),
132 132 pr.versions_count),
133 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 134 'created_on': _render('pullrequest_updated_on',
135 135 h.datetime_to_time(pr.created_on)),
136 136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 137 'state': pr.pull_request_state,
138 138 'author': _render('pullrequest_author',
139 139 pr.author.full_contact, ),
140 140 'author_raw': pr.author.full_name,
141 141 'comments': _render('pullrequest_comments', comments_count),
142 142 'comments_raw': comments_count,
143 143 'closed': pr.is_closed(),
144 144 })
145 145
146 146 data = ({
147 147 'draw': draw,
148 148 'data': data,
149 149 'recordsTotal': pull_requests_total_count,
150 150 'recordsFiltered': pull_requests_total_count,
151 151 })
152 152 return data
153 153
154 154 @LoginRequired()
155 155 @HasRepoPermissionAnyDecorator(
156 156 'repository.read', 'repository.write', 'repository.admin')
157 157 def pull_request_list(self):
158 158 c = self.load_default_context()
159 159
160 160 req_get = self.request.GET
161 161 c.source = str2bool(req_get.get('source'))
162 162 c.closed = str2bool(req_get.get('closed'))
163 163 c.my = str2bool(req_get.get('my'))
164 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 166
167 167 c.active = 'open'
168 168 if c.my:
169 169 c.active = 'my'
170 170 if c.closed:
171 171 c.active = 'closed'
172 172 if c.awaiting_review and not c.source:
173 173 c.active = 'awaiting'
174 174 if c.source and not c.awaiting_review:
175 175 c.active = 'source'
176 176 if c.awaiting_my_review:
177 177 c.active = 'awaiting_my'
178 178
179 179 return self._get_template_context(c)
180 180
181 181 @LoginRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 ancestor_commit,
221 221 source_ref_id, target_ref_id,
222 222 target_commit, source_commit, diff_limit, file_limit,
223 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 224
225 225 target_commit_final = target_commit
226 226 source_commit_final = source_commit
227 227
228 228 if use_ancestor:
229 229 # we might want to not use it for versions
230 230 target_ref_id = ancestor_commit.raw_id
231 231 target_commit_final = ancestor_commit
232 232
233 233 vcs_diff = PullRequestModel().get_diff(
234 234 source_repo, source_ref_id, target_ref_id,
235 235 hide_whitespace_changes, diff_context)
236 236
237 diff_processor = diffs.DiffProcessor(
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
237 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
239 238 file_limit=file_limit, show_full_diff=fulldiff)
240 239
241 240 _parsed = diff_processor.prepare()
242 241
243 242 diffset = codeblocks.DiffSet(
244 243 repo_name=self.db_repo_name,
245 244 source_repo_name=source_repo_name,
246 245 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 246 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 247 )
249 248 diffset = self.path_filter.render_patchset_filtered(
250 249 diffset, _parsed, target_ref_id, source_ref_id)
251 250
252 251 return diffset
253 252
254 253 def _get_range_diffset(self, source_scm, source_repo,
255 254 commit1, commit2, diff_limit, file_limit,
256 255 fulldiff, hide_whitespace_changes, diff_context):
257 256 vcs_diff = source_scm.get_diff(
258 257 commit1, commit2,
259 258 ignore_whitespace=hide_whitespace_changes,
260 259 context=diff_context)
261 260
262 diff_processor = diffs.DiffProcessor(
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 file_limit=file_limit, show_full_diff=fulldiff)
261 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
262 diff_limit=diff_limit,
263 file_limit=file_limit, show_full_diff=fulldiff)
265 264
266 265 _parsed = diff_processor.prepare()
267 266
268 267 diffset = codeblocks.DiffSet(
269 268 repo_name=source_repo.repo_name,
270 269 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 270 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 271
273 272 diffset = self.path_filter.render_patchset_filtered(
274 273 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 274
276 275 return diffset
277 276
278 277 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 278 comments_model = CommentsModel()
280 279
281 280 # GENERAL COMMENTS with versions #
282 281 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 282 q = q.order_by(ChangesetComment.comment_id.asc())
284 283 if not include_drafts:
285 284 q = q.filter(ChangesetComment.draft == false())
286 285 general_comments = q
287 286
288 287 # pick comments we want to render at current version
289 288 c.comment_versions = comments_model.aggregate_comments(
290 289 general_comments, versions, c.at_version_num)
291 290
292 291 # INLINE COMMENTS with versions #
293 292 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 293 q = q.order_by(ChangesetComment.comment_id.asc())
295 294 if not include_drafts:
296 295 q = q.filter(ChangesetComment.draft == false())
297 296 inline_comments = q
298 297
299 298 c.inline_versions = comments_model.aggregate_comments(
300 299 inline_comments, versions, c.at_version_num, inline=True)
301 300
302 301 # Comments inline+general
303 302 if c.at_version:
304 303 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 304 c.comments = c.comment_versions[c.at_version_num]['display']
306 305 else:
307 306 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 307 c.comments = c.comment_versions[c.at_version_num]['until']
309 308
310 309 return general_comments, inline_comments
311 310
312 311 @LoginRequired()
313 312 @HasRepoPermissionAnyDecorator(
314 313 'repository.read', 'repository.write', 'repository.admin')
315 314 def pull_request_show(self):
316 315 _ = self.request.translate
317 316 c = self.load_default_context()
318 317
319 318 pull_request = PullRequest.get_or_404(
320 319 self.request.matchdict['pull_request_id'])
321 320 pull_request_id = pull_request.pull_request_id
322 321
323 322 c.state_progressing = pull_request.is_state_changing()
324 323 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325 324
326 325 _new_state = {
327 326 'created': PullRequest.STATE_CREATED,
328 327 }.get(self.request.GET.get('force_state'))
329 328 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
330 329
331 330 if can_force_state and _new_state:
332 331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
333 332 h.flash(
334 333 _('Pull Request state was force changed to `{}`').format(_new_state),
335 334 category='success')
336 335 Session().commit()
337 336
338 337 raise HTTPFound(h.route_path(
339 338 'pullrequest_show', repo_name=self.db_repo_name,
340 339 pull_request_id=pull_request_id))
341 340
342 341 version = self.request.GET.get('version')
343 342 from_version = self.request.GET.get('from_version') or version
344 343 merge_checks = self.request.GET.get('merge_checks')
345 344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
346 345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
347 346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
348 347
349 348 # fetch global flags of ignore ws or context lines
350 349 diff_context = diffs.get_diff_context(self.request)
351 350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
352 351
353 352 (pull_request_latest,
354 353 pull_request_at_ver,
355 354 pull_request_display_obj,
356 355 at_version) = PullRequestModel().get_pr_version(
357 356 pull_request_id, version=version)
358 357
359 358 pr_closed = pull_request_latest.is_closed()
360 359
361 360 if pr_closed and (version or from_version):
362 361 # not allow to browse versions for closed PR
363 362 raise HTTPFound(h.route_path(
364 363 'pullrequest_show', repo_name=self.db_repo_name,
365 364 pull_request_id=pull_request_id))
366 365
367 366 versions = pull_request_display_obj.versions()
368 367
369 368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
370 369
371 370 # used to store per-commit range diffs
372 371 c.changes = collections.OrderedDict()
373 372
374 373 c.at_version = at_version
375 374 c.at_version_num = (at_version
376 375 if at_version and at_version != PullRequest.LATEST_VER
377 376 else None)
378 377
379 378 c.at_version_index = ChangesetComment.get_index_from_version(
380 379 c.at_version_num, versions)
381 380
382 381 (prev_pull_request_latest,
383 382 prev_pull_request_at_ver,
384 383 prev_pull_request_display_obj,
385 384 prev_at_version) = PullRequestModel().get_pr_version(
386 385 pull_request_id, version=from_version)
387 386
388 387 c.from_version = prev_at_version
389 388 c.from_version_num = (prev_at_version
390 389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
391 390 else None)
392 391 c.from_version_index = ChangesetComment.get_index_from_version(
393 392 c.from_version_num, versions)
394 393
395 394 # define if we're in COMPARE mode or VIEW at version mode
396 395 compare = at_version != prev_at_version
397 396
398 397 # pull_requests repo_name we opened it against
399 398 # ie. target_repo must match
400 399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
401 400 log.warning('Mismatch between the current repo: %s, and target %s',
402 401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
403 402 raise HTTPNotFound()
404 403
405 404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
406 405
407 406 c.pull_request = pull_request_display_obj
408 407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
409 408 c.pull_request_latest = pull_request_latest
410 409
411 410 # inject latest version
412 411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
413 412 c.versions = versions + [latest_ver]
414 413
415 414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
416 415 c.allowed_to_change_status = False
417 416 c.allowed_to_update = False
418 417 c.allowed_to_merge = False
419 418 c.allowed_to_delete = False
420 419 c.allowed_to_comment = False
421 420 c.allowed_to_close = False
422 421 else:
423 422 can_change_status = PullRequestModel().check_user_change_status(
424 423 pull_request_at_ver, self._rhodecode_user)
425 424 c.allowed_to_change_status = can_change_status and not pr_closed
426 425
427 426 c.allowed_to_update = PullRequestModel().check_user_update(
428 427 pull_request_latest, self._rhodecode_user) and not pr_closed
429 428 c.allowed_to_merge = PullRequestModel().check_user_merge(
430 429 pull_request_latest, self._rhodecode_user) and not pr_closed
431 430 c.allowed_to_delete = PullRequestModel().check_user_delete(
432 431 pull_request_latest, self._rhodecode_user) and not pr_closed
433 432 c.allowed_to_comment = not pr_closed
434 433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
435 434
436 435 c.forbid_adding_reviewers = False
437 436
438 437 if pull_request_latest.reviewer_data and \
439 438 'rules' in pull_request_latest.reviewer_data:
440 439 rules = pull_request_latest.reviewer_data['rules'] or {}
441 440 try:
442 441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
443 442 except Exception:
444 443 pass
445 444
446 445 # check merge capabilities
447 446 _merge_check = MergeCheck.validate(
448 447 pull_request_latest, auth_user=self._rhodecode_user,
449 448 translator=self.request.translate,
450 449 force_shadow_repo_refresh=force_refresh)
451 450
452 451 c.pr_merge_errors = _merge_check.error_details
453 452 c.pr_merge_possible = not _merge_check.failed
454 453 c.pr_merge_message = _merge_check.merge_msg
455 454 c.pr_merge_source_commit = _merge_check.source_commit
456 455 c.pr_merge_target_commit = _merge_check.target_commit
457 456
458 457 c.pr_merge_info = MergeCheck.get_merge_conditions(
459 458 pull_request_latest, translator=self.request.translate)
460 459
461 460 c.pull_request_review_status = _merge_check.review_status
462 461 if merge_checks:
463 462 self.request.override_renderer = \
464 463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
465 464 return self._get_template_context(c)
466 465
467 466 c.reviewers_count = pull_request.reviewers_count
468 467 c.observers_count = pull_request.observers_count
469 468
470 469 # reviewers and statuses
471 470 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
472 471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
473 472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
474 473
475 474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
476 475 member_reviewer = h.reviewer_as_json(
477 476 member, reasons=reasons, mandatory=mandatory,
478 477 role=review_obj.role,
479 478 user_group=review_obj.rule_user_group_data()
480 479 )
481 480
482 481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
483 482 member_reviewer['review_status'] = current_review_status
484 483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
485 484 member_reviewer['allowed_to_update'] = c.allowed_to_update
486 485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
487 486
488 487 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
489 488
490 489 for observer_obj, member in pull_request_at_ver.observers():
491 490 member_observer = h.reviewer_as_json(
492 491 member, reasons=[], mandatory=False,
493 492 role=observer_obj.role,
494 493 user_group=observer_obj.rule_user_group_data()
495 494 )
496 495 member_observer['allowed_to_update'] = c.allowed_to_update
497 496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
498 497
499 498 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
500 499
501 500 general_comments, inline_comments = \
502 501 self.register_comments_vars(c, pull_request_latest, versions)
503 502
504 503 # TODOs
505 504 c.unresolved_comments = CommentsModel() \
506 505 .get_pull_request_unresolved_todos(pull_request_latest)
507 506 c.resolved_comments = CommentsModel() \
508 507 .get_pull_request_resolved_todos(pull_request_latest)
509 508
510 509 # Drafts
511 510 c.draft_comments = CommentsModel().get_pull_request_drafts(
512 511 self._rhodecode_db_user.user_id,
513 512 pull_request_latest)
514 513
515 514 # if we use version, then do not show later comments
516 515 # than current version
517 516 display_inline_comments = collections.defaultdict(
518 517 lambda: collections.defaultdict(list))
519 518 for co in inline_comments:
520 519 if c.at_version_num:
521 520 # pick comments that are at least UPTO given version, so we
522 521 # don't render comments for higher version
523 522 should_render = co.pull_request_version_id and \
524 523 co.pull_request_version_id <= c.at_version_num
525 524 else:
526 525 # showing all, for 'latest'
527 526 should_render = True
528 527
529 528 if should_render:
530 529 display_inline_comments[co.f_path][co.line_no].append(co)
531 530
532 531 # load diff data into template context, if we use compare mode then
533 532 # diff is calculated based on changes between versions of PR
534 533
535 534 source_repo = pull_request_at_ver.source_repo
536 535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
537 536
538 537 target_repo = pull_request_at_ver.target_repo
539 538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
540 539
541 540 if compare:
542 541 # in compare switch the diff base to latest commit from prev version
543 542 target_ref_id = prev_pull_request_display_obj.revisions[0]
544 543
545 544 # despite opening commits for bookmarks/branches/tags, we always
546 545 # convert this to rev to prevent changes after bookmark or branch change
547 546 c.source_ref_type = 'rev'
548 547 c.source_ref = source_ref_id
549 548
550 549 c.target_ref_type = 'rev'
551 550 c.target_ref = target_ref_id
552 551
553 552 c.source_repo = source_repo
554 553 c.target_repo = target_repo
555 554
556 555 c.commit_ranges = []
557 556 source_commit = EmptyCommit()
558 557 target_commit = EmptyCommit()
559 558 c.missing_requirements = False
560 559
561 560 source_scm = source_repo.scm_instance()
562 561 target_scm = target_repo.scm_instance()
563 562
564 563 shadow_scm = None
565 564 try:
566 565 shadow_scm = pull_request_latest.get_shadow_repo()
567 566 except Exception:
568 567 log.debug('Failed to get shadow repo', exc_info=True)
569 568 # try first the existing source_repo, and then shadow
570 569 # repo if we can obtain one
571 570 commits_source_repo = source_scm
572 571 if shadow_scm:
573 572 commits_source_repo = shadow_scm
574 573
575 574 c.commits_source_repo = commits_source_repo
576 575 c.ancestor = None # set it to None, to hide it from PR view
577 576
578 577 # empty version means latest, so we keep this to prevent
579 578 # double caching
580 579 version_normalized = version or PullRequest.LATEST_VER
581 580 from_version_normalized = from_version or PullRequest.LATEST_VER
582 581
583 582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
584 583 cache_file_path = diff_cache_exist(
585 584 cache_path, 'pull_request', pull_request_id, version_normalized,
586 585 from_version_normalized, source_ref_id, target_ref_id,
587 586 hide_whitespace_changes, diff_context, c.fulldiff)
588 587
589 588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
590 589 force_recache = self.get_recache_flag()
591 590
592 591 cached_diff = None
593 592 if caching_enabled:
594 593 cached_diff = load_cached_diff(cache_file_path)
595 594
596 595 has_proper_commit_cache = (
597 596 cached_diff and cached_diff.get('commits')
598 597 and len(cached_diff.get('commits', [])) == 5
599 598 and cached_diff.get('commits')[0]
600 599 and cached_diff.get('commits')[3])
601 600
602 601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
603 602 diff_commit_cache = \
604 603 (ancestor_commit, commit_cache, missing_requirements,
605 604 source_commit, target_commit) = cached_diff['commits']
606 605 else:
607 606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
608 607 # merge errors resulting in potentially hidden commits in the shadow repo.
609 608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
610 609 and _merge_check.merge_response
611 610 maybe_unreachable = maybe_unreachable \
612 611 and _merge_check.merge_response.metadata.get('unresolved_files')
613 612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
614 613 diff_commit_cache = \
615 614 (ancestor_commit, commit_cache, missing_requirements,
616 615 source_commit, target_commit) = self.get_commits(
617 616 commits_source_repo,
618 617 pull_request_at_ver,
619 618 source_commit,
620 619 source_ref_id,
621 620 source_scm,
622 621 target_commit,
623 622 target_ref_id,
624 623 target_scm,
625 624 maybe_unreachable=maybe_unreachable)
626 625
627 626 # register our commit range
628 627 for comm in commit_cache.values():
629 628 c.commit_ranges.append(comm)
630 629
631 630 c.missing_requirements = missing_requirements
632 631 c.ancestor_commit = ancestor_commit
633 632 c.statuses = source_repo.statuses(
634 633 [x.raw_id for x in c.commit_ranges])
635 634
636 635 # auto collapse if we have more than limit
637 636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
638 637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
639 638 c.compare_mode = compare
640 639
641 640 # diff_limit is the old behavior, will cut off the whole diff
642 641 # if the limit is applied otherwise will just hide the
643 642 # big files from the front-end
644 643 diff_limit = c.visual.cut_off_limit_diff
645 644 file_limit = c.visual.cut_off_limit_file
646 645
647 646 c.missing_commits = False
648 647 if (c.missing_requirements
649 648 or isinstance(source_commit, EmptyCommit)
650 649 or source_commit == target_commit):
651 650
652 651 c.missing_commits = True
653 652 else:
654 653 c.inline_comments = display_inline_comments
655 654
656 655 use_ancestor = True
657 656 if from_version_normalized != version_normalized:
658 657 use_ancestor = False
659 658
660 659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
661 660 if not force_recache and has_proper_diff_cache:
662 661 c.diffset = cached_diff['diff']
663 662 else:
664 663 try:
665 664 c.diffset = self._get_diffset(
666 665 c.source_repo.repo_name, commits_source_repo,
667 666 c.ancestor_commit,
668 667 source_ref_id, target_ref_id,
669 668 target_commit, source_commit,
670 669 diff_limit, file_limit, c.fulldiff,
671 670 hide_whitespace_changes, diff_context,
672 671 use_ancestor=use_ancestor
673 672 )
674 673
675 674 # save cached diff
676 675 if caching_enabled:
677 676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
678 677 except CommitDoesNotExistError:
679 678 log.exception('Failed to generate diffset')
680 679 c.missing_commits = True
681 680
682 681 if not c.missing_commits:
683 682
684 683 c.limited_diff = c.diffset.limited_diff
685 684
686 685 # calculate removed files that are bound to comments
687 686 comment_deleted_files = [
688 687 fname for fname in display_inline_comments
689 688 if fname not in c.diffset.file_stats]
690 689
691 690 c.deleted_files_comments = collections.defaultdict(dict)
692 691 for fname, per_line_comments in display_inline_comments.items():
693 692 if fname in comment_deleted_files:
694 693 c.deleted_files_comments[fname]['stats'] = 0
695 694 c.deleted_files_comments[fname]['comments'] = list()
696 695 for lno, comments in per_line_comments.items():
697 696 c.deleted_files_comments[fname]['comments'].extend(comments)
698 697
699 698 # maybe calculate the range diff
700 699 if c.range_diff_on:
701 700 # TODO(marcink): set whitespace/context
702 701 context_lcl = 3
703 702 ign_whitespace_lcl = False
704 703
705 704 for commit in c.commit_ranges:
706 705 commit2 = commit
707 706 commit1 = commit.first_parent
708 707
709 708 range_diff_cache_file_path = diff_cache_exist(
710 709 cache_path, 'diff', commit.raw_id,
711 710 ign_whitespace_lcl, context_lcl, c.fulldiff)
712 711
713 712 cached_diff = None
714 713 if caching_enabled:
715 714 cached_diff = load_cached_diff(range_diff_cache_file_path)
716 715
717 716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
718 717 if not force_recache and has_proper_diff_cache:
719 718 diffset = cached_diff['diff']
720 719 else:
721 720 diffset = self._get_range_diffset(
722 721 commits_source_repo, source_repo,
723 722 commit1, commit2, diff_limit, file_limit,
724 723 c.fulldiff, ign_whitespace_lcl, context_lcl
725 724 )
726 725
727 726 # save cached diff
728 727 if caching_enabled:
729 728 cache_diff(range_diff_cache_file_path, diffset, None)
730 729
731 730 c.changes[commit.raw_id] = diffset
732 731
733 732 # this is a hack to properly display links, when creating PR, the
734 733 # compare view and others uses different notation, and
735 734 # compare_commits.mako renders links based on the target_repo.
736 735 # We need to swap that here to generate it properly on the html side
737 736 c.target_repo = c.source_repo
738 737
739 738 c.commit_statuses = ChangesetStatus.STATUSES
740 739
741 740 c.show_version_changes = not pr_closed
742 741 if c.show_version_changes:
743 742 cur_obj = pull_request_at_ver
744 743 prev_obj = prev_pull_request_at_ver
745 744
746 745 old_commit_ids = prev_obj.revisions
747 746 new_commit_ids = cur_obj.revisions
748 747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
749 748 old_commit_ids, new_commit_ids)
750 749 c.commit_changes_summary = commit_changes
751 750
752 751 # calculate the diff for commits between versions
753 752 c.commit_changes = []
754 753
755 754 def mark(cs, fw):
756 755 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
757 756
758 757 for c_type, raw_id in mark(commit_changes.added, 'a') \
759 758 + mark(commit_changes.removed, 'r') \
760 759 + mark(commit_changes.common, 'c'):
761 760
762 761 if raw_id in commit_cache:
763 762 commit = commit_cache[raw_id]
764 763 else:
765 764 try:
766 765 commit = commits_source_repo.get_commit(raw_id)
767 766 except CommitDoesNotExistError:
768 767 # in case we fail extracting still use "dummy" commit
769 768 # for display in commit diff
770 769 commit = h.AttributeDict(
771 770 {'raw_id': raw_id,
772 771 'message': 'EMPTY or MISSING COMMIT'})
773 772 c.commit_changes.append([c_type, commit])
774 773
775 774 # current user review statuses for each version
776 775 c.review_versions = {}
777 776 is_reviewer = PullRequestModel().is_user_reviewer(
778 777 pull_request, self._rhodecode_user)
779 778 if is_reviewer:
780 779 for co in general_comments:
781 780 if co.author.user_id == self._rhodecode_user.user_id:
782 781 status = co.status_change
783 782 if status:
784 783 _ver_pr = status[0].comment.pull_request_version_id
785 784 c.review_versions[_ver_pr] = status[0]
786 785
787 786 return self._get_template_context(c)
788 787
789 788 def get_commits(
790 789 self, commits_source_repo, pull_request_at_ver, source_commit,
791 790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
792 791 maybe_unreachable=False):
793 792
794 793 commit_cache = collections.OrderedDict()
795 794 missing_requirements = False
796 795
797 796 try:
798 797 pre_load = ["author", "date", "message", "branch", "parents"]
799 798
800 799 pull_request_commits = pull_request_at_ver.revisions
801 800 log.debug('Loading %s commits from %s',
802 801 len(pull_request_commits), commits_source_repo)
803 802
804 803 for rev in pull_request_commits:
805 804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
806 805 maybe_unreachable=maybe_unreachable)
807 806 commit_cache[comm.raw_id] = comm
808 807
809 808 # Order here matters, we first need to get target, and then
810 809 # the source
811 810 target_commit = commits_source_repo.get_commit(
812 811 commit_id=safe_str(target_ref_id))
813 812
814 813 source_commit = commits_source_repo.get_commit(
815 814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
816 815 except CommitDoesNotExistError:
817 816 log.warning('Failed to get commit from `{}` repo'.format(
818 817 commits_source_repo), exc_info=True)
819 818 except RepositoryRequirementError:
820 819 log.warning('Failed to get all required data from repo', exc_info=True)
821 820 missing_requirements = True
822 821
823 822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
824 823
825 824 try:
826 825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
827 826 except Exception:
828 827 ancestor_commit = None
829 828
830 829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
831 830
832 831 def assure_not_empty_repo(self):
833 832 _ = self.request.translate
834 833
835 834 try:
836 835 self.db_repo.scm_instance().get_commit()
837 836 except EmptyRepositoryError:
838 837 h.flash(h.literal(_('There are no commits yet')),
839 838 category='warning')
840 839 raise HTTPFound(
841 840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
842 841
843 842 @LoginRequired()
844 843 @NotAnonymous()
845 844 @HasRepoPermissionAnyDecorator(
846 845 'repository.read', 'repository.write', 'repository.admin')
847 846 def pull_request_new(self):
848 847 _ = self.request.translate
849 848 c = self.load_default_context()
850 849
851 850 self.assure_not_empty_repo()
852 851 source_repo = self.db_repo
853 852
854 853 commit_id = self.request.GET.get('commit')
855 854 branch_ref = self.request.GET.get('branch')
856 855 bookmark_ref = self.request.GET.get('bookmark')
857 856
858 857 try:
859 858 source_repo_data = PullRequestModel().generate_repo_data(
860 859 source_repo, commit_id=commit_id,
861 860 branch=branch_ref, bookmark=bookmark_ref,
862 861 translator=self.request.translate)
863 862 except CommitDoesNotExistError as e:
864 863 log.exception(e)
865 864 h.flash(_('Commit does not exist'), 'error')
866 865 raise HTTPFound(
867 866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
868 867
869 868 default_target_repo = source_repo
870 869
871 870 if source_repo.parent and c.has_origin_repo_read_perm:
872 871 parent_vcs_obj = source_repo.parent.scm_instance()
873 872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
874 873 # change default if we have a parent repo
875 874 default_target_repo = source_repo.parent
876 875
877 876 target_repo_data = PullRequestModel().generate_repo_data(
878 877 default_target_repo, translator=self.request.translate)
879 878
880 879 selected_source_ref = source_repo_data['refs']['selected_ref']
881 880 title_source_ref = ''
882 881 if selected_source_ref:
883 882 title_source_ref = selected_source_ref.split(':', 2)[1]
884 883 c.default_title = PullRequestModel().generate_pullrequest_title(
885 884 source=source_repo.repo_name,
886 885 source_ref=title_source_ref,
887 886 target=default_target_repo.repo_name
888 887 )
889 888
890 889 c.default_repo_data = {
891 890 'source_repo_name': source_repo.repo_name,
892 891 'source_refs_json': ext_json.str_json(source_repo_data),
893 892 'target_repo_name': default_target_repo.repo_name,
894 893 'target_refs_json': ext_json.str_json(target_repo_data),
895 894 }
896 895 c.default_source_ref = selected_source_ref
897 896
898 897 return self._get_template_context(c)
899 898
900 899 @LoginRequired()
901 900 @NotAnonymous()
902 901 @HasRepoPermissionAnyDecorator(
903 902 'repository.read', 'repository.write', 'repository.admin')
904 903 def pull_request_repo_refs(self):
905 904 self.load_default_context()
906 905 target_repo_name = self.request.matchdict['target_repo_name']
907 906 repo = Repository.get_by_repo_name(target_repo_name)
908 907 if not repo:
909 908 raise HTTPNotFound()
910 909
911 910 target_perm = HasRepoPermissionAny(
912 911 'repository.read', 'repository.write', 'repository.admin')(
913 912 target_repo_name)
914 913 if not target_perm:
915 914 raise HTTPNotFound()
916 915
917 916 return PullRequestModel().generate_repo_data(
918 917 repo, translator=self.request.translate)
919 918
920 919 @LoginRequired()
921 920 @NotAnonymous()
922 921 @HasRepoPermissionAnyDecorator(
923 922 'repository.read', 'repository.write', 'repository.admin')
924 923 def pullrequest_repo_targets(self):
925 924 _ = self.request.translate
926 925 filter_query = self.request.GET.get('query')
927 926
928 927 # get the parents
929 928 parent_target_repos = []
930 929 if self.db_repo.parent:
931 930 parents_query = Repository.query() \
932 931 .order_by(func.length(Repository.repo_name)) \
933 932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 933
935 934 if filter_query:
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 ilike_expression = u'%{}%'.format(safe_str(filter_query))
937 936 parents_query = parents_query.filter(
938 937 Repository.repo_name.ilike(ilike_expression))
939 938 parents = parents_query.limit(20).all()
940 939
941 940 for parent in parents:
942 941 parent_vcs_obj = parent.scm_instance()
943 942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 943 parent_target_repos.append(parent)
945 944
946 945 # get other forks, and repo itself
947 946 query = Repository.query() \
948 947 .order_by(func.length(Repository.repo_name)) \
949 948 .filter(
950 949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 951 ) \
953 952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 953
955 954 if filter_query:
956 955 ilike_expression = u'%{}%'.format(safe_str(filter_query))
957 956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 957
959 958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 959 target_repos = query.limit(limit).all()
961 960
962 961 all_target_repos = target_repos + parent_target_repos
963 962
964 963 repos = []
965 964 # This checks permissions to the repositories
966 965 for obj in ScmModel().get_repos(all_target_repos):
967 966 repos.append({
968 967 'id': obj['name'],
969 968 'text': obj['name'],
970 969 'type': 'repo',
971 970 'repo_id': obj['dbrepo']['repo_id'],
972 971 'repo_type': obj['dbrepo']['repo_type'],
973 972 'private': obj['dbrepo']['private'],
974 973
975 974 })
976 975
977 976 data = {
978 977 'more': False,
979 978 'results': [{
980 979 'text': _('Repositories'),
981 980 'children': repos
982 981 }] if repos else []
983 982 }
984 983 return data
985 984
986 985 @classmethod
987 986 def get_comment_ids(cls, post_data):
988 987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989 988
990 989 @LoginRequired()
991 990 @NotAnonymous()
992 991 @HasRepoPermissionAnyDecorator(
993 992 'repository.read', 'repository.write', 'repository.admin')
994 993 def pullrequest_comments(self):
995 994 self.load_default_context()
996 995
997 996 pull_request = PullRequest.get_or_404(
998 997 self.request.matchdict['pull_request_id'])
999 998 pull_request_id = pull_request.pull_request_id
1000 999 version = self.request.GET.get('version')
1001 1000
1002 1001 _render = self.request.get_partial_renderer(
1003 1002 'rhodecode:templates/base/sidebar.mako')
1004 1003 c = _render.get_call_context()
1005 1004
1006 1005 (pull_request_latest,
1007 1006 pull_request_at_ver,
1008 1007 pull_request_display_obj,
1009 1008 at_version) = PullRequestModel().get_pr_version(
1010 1009 pull_request_id, version=version)
1011 1010 versions = pull_request_display_obj.versions()
1012 1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1013 1012 c.versions = versions + [latest_ver]
1014 1013
1015 1014 c.at_version = at_version
1016 1015 c.at_version_num = (at_version
1017 1016 if at_version and at_version != PullRequest.LATEST_VER
1018 1017 else None)
1019 1018
1020 1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1021 1020 all_comments = c.inline_comments_flat + c.comments
1022 1021
1023 1022 existing_ids = self.get_comment_ids(self.request.POST)
1024 1023 return _render('comments_table', all_comments, len(all_comments),
1025 1024 existing_ids=existing_ids)
1026 1025
1027 1026 @LoginRequired()
1028 1027 @NotAnonymous()
1029 1028 @HasRepoPermissionAnyDecorator(
1030 1029 'repository.read', 'repository.write', 'repository.admin')
1031 1030 def pullrequest_todos(self):
1032 1031 self.load_default_context()
1033 1032
1034 1033 pull_request = PullRequest.get_or_404(
1035 1034 self.request.matchdict['pull_request_id'])
1036 1035 pull_request_id = pull_request.pull_request_id
1037 1036 version = self.request.GET.get('version')
1038 1037
1039 1038 _render = self.request.get_partial_renderer(
1040 1039 'rhodecode:templates/base/sidebar.mako')
1041 1040 c = _render.get_call_context()
1042 1041 (pull_request_latest,
1043 1042 pull_request_at_ver,
1044 1043 pull_request_display_obj,
1045 1044 at_version) = PullRequestModel().get_pr_version(
1046 1045 pull_request_id, version=version)
1047 1046 versions = pull_request_display_obj.versions()
1048 1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 1048 c.versions = versions + [latest_ver]
1050 1049
1051 1050 c.at_version = at_version
1052 1051 c.at_version_num = (at_version
1053 1052 if at_version and at_version != PullRequest.LATEST_VER
1054 1053 else None)
1055 1054
1056 1055 c.unresolved_comments = CommentsModel() \
1057 1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1058 1057 c.resolved_comments = CommentsModel() \
1059 1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1060 1059
1061 1060 all_comments = c.unresolved_comments + c.resolved_comments
1062 1061 existing_ids = self.get_comment_ids(self.request.POST)
1063 1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 1063 todo_comments=True, existing_ids=existing_ids)
1065 1064
1066 1065 @LoginRequired()
1067 1066 @NotAnonymous()
1068 1067 @HasRepoPermissionAnyDecorator(
1069 1068 'repository.read', 'repository.write', 'repository.admin')
1070 1069 def pullrequest_drafts(self):
1071 1070 self.load_default_context()
1072 1071
1073 1072 pull_request = PullRequest.get_or_404(
1074 1073 self.request.matchdict['pull_request_id'])
1075 1074 pull_request_id = pull_request.pull_request_id
1076 1075 version = self.request.GET.get('version')
1077 1076
1078 1077 _render = self.request.get_partial_renderer(
1079 1078 'rhodecode:templates/base/sidebar.mako')
1080 1079 c = _render.get_call_context()
1081 1080
1082 1081 (pull_request_latest,
1083 1082 pull_request_at_ver,
1084 1083 pull_request_display_obj,
1085 1084 at_version) = PullRequestModel().get_pr_version(
1086 1085 pull_request_id, version=version)
1087 1086 versions = pull_request_display_obj.versions()
1088 1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1089 1088 c.versions = versions + [latest_ver]
1090 1089
1091 1090 c.at_version = at_version
1092 1091 c.at_version_num = (at_version
1093 1092 if at_version and at_version != PullRequest.LATEST_VER
1094 1093 else None)
1095 1094
1096 1095 c.draft_comments = CommentsModel() \
1097 1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1098 1097
1099 1098 all_comments = c.draft_comments
1100 1099
1101 1100 existing_ids = self.get_comment_ids(self.request.POST)
1102 1101 return _render('comments_table', all_comments, len(all_comments),
1103 1102 existing_ids=existing_ids, draft_comments=True)
1104 1103
1105 1104 @LoginRequired()
1106 1105 @NotAnonymous()
1107 1106 @HasRepoPermissionAnyDecorator(
1108 1107 'repository.read', 'repository.write', 'repository.admin')
1109 1108 @CSRFRequired()
1110 1109 def pull_request_create(self):
1111 1110 _ = self.request.translate
1112 1111 self.assure_not_empty_repo()
1113 1112 self.load_default_context()
1114 1113
1115 1114 controls = peppercorn.parse(self.request.POST.items())
1116 1115
1117 1116 try:
1118 1117 form = PullRequestForm(
1119 1118 self.request.translate, self.db_repo.repo_id)()
1120 1119 _form = form.to_python(controls)
1121 1120 except formencode.Invalid as errors:
1122 1121 if errors.error_dict.get('revisions'):
1123 1122 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1124 1123 elif errors.error_dict.get('pullrequest_title'):
1125 1124 msg = errors.error_dict.get('pullrequest_title')
1126 1125 else:
1127 1126 msg = _('Error creating pull request: {}').format(errors)
1128 1127 log.exception(msg)
1129 1128 h.flash(msg, 'error')
1130 1129
1131 1130 # would rather just go back to form ...
1132 1131 raise HTTPFound(
1133 1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1134 1133
1135 1134 source_repo = _form['source_repo']
1136 1135 source_ref = _form['source_ref']
1137 1136 target_repo = _form['target_repo']
1138 1137 target_ref = _form['target_ref']
1139 1138 commit_ids = _form['revisions'][::-1]
1140 1139 common_ancestor_id = _form['common_ancestor']
1141 1140
1142 1141 # find the ancestor for this pr
1143 1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1144 1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1145 1144
1146 1145 if not (source_db_repo or target_db_repo):
1147 1146 h.flash(_('source_repo or target repo not found'), category='error')
1148 1147 raise HTTPFound(
1149 1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1150 1149
1151 1150 # re-check permissions again here
1152 1151 # source_repo we must have read permissions
1153 1152
1154 1153 source_perm = HasRepoPermissionAny(
1155 1154 'repository.read', 'repository.write', 'repository.admin')(
1156 1155 source_db_repo.repo_name)
1157 1156 if not source_perm:
1158 1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1159 1158 source_db_repo.repo_name))
1160 1159 h.flash(msg, category='error')
1161 1160 # copy the args back to redirect
1162 1161 org_query = self.request.GET.mixed()
1163 1162 raise HTTPFound(
1164 1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1165 1164 _query=org_query))
1166 1165
1167 1166 # target repo we must have read permissions, and also later on
1168 1167 # we want to check branch permissions here
1169 1168 target_perm = HasRepoPermissionAny(
1170 1169 'repository.read', 'repository.write', 'repository.admin')(
1171 1170 target_db_repo.repo_name)
1172 1171 if not target_perm:
1173 1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1174 1173 target_db_repo.repo_name))
1175 1174 h.flash(msg, category='error')
1176 1175 # copy the args back to redirect
1177 1176 org_query = self.request.GET.mixed()
1178 1177 raise HTTPFound(
1179 1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1180 1179 _query=org_query))
1181 1180
1182 1181 source_scm = source_db_repo.scm_instance()
1183 1182 target_scm = target_db_repo.scm_instance()
1184 1183
1185 1184 source_ref_obj = unicode_to_reference(source_ref)
1186 1185 target_ref_obj = unicode_to_reference(target_ref)
1187 1186
1188 1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1189 1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1190 1189
1191 1190 ancestor = source_scm.get_common_ancestor(
1192 1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1193 1192
1194 1193 # recalculate target ref based on ancestor
1195 1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1196 1195
1197 1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1198 1197 PullRequestModel().get_reviewer_functions()
1199 1198
1200 1199 # recalculate reviewers logic, to make sure we can validate this
1201 1200 reviewer_rules = get_default_reviewers_data(
1202 1201 self._rhodecode_db_user,
1203 1202 source_db_repo,
1204 1203 source_ref_obj,
1205 1204 target_db_repo,
1206 1205 target_ref_obj,
1207 1206 include_diff_info=False)
1208 1207
1209 1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1210 1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1211 1210
1212 1211 pullrequest_title = _form['pullrequest_title']
1213 1212 title_source_ref = source_ref_obj.name
1214 1213 if not pullrequest_title:
1215 1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1216 1215 source=source_repo,
1217 1216 source_ref=title_source_ref,
1218 1217 target=target_repo
1219 1218 )
1220 1219
1221 1220 description = _form['pullrequest_desc']
1222 1221 description_renderer = _form['description_renderer']
1223 1222
1224 1223 try:
1225 1224 pull_request = PullRequestModel().create(
1226 1225 created_by=self._rhodecode_user.user_id,
1227 1226 source_repo=source_repo,
1228 1227 source_ref=source_ref,
1229 1228 target_repo=target_repo,
1230 1229 target_ref=target_ref,
1231 1230 revisions=commit_ids,
1232 1231 common_ancestor_id=common_ancestor_id,
1233 1232 reviewers=reviewers,
1234 1233 observers=observers,
1235 1234 title=pullrequest_title,
1236 1235 description=description,
1237 1236 description_renderer=description_renderer,
1238 1237 reviewer_data=reviewer_rules,
1239 1238 auth_user=self._rhodecode_user
1240 1239 )
1241 1240 Session().commit()
1242 1241
1243 1242 h.flash(_('Successfully opened new pull request'),
1244 1243 category='success')
1245 1244 except Exception:
1246 1245 msg = _('Error occurred during creation of this pull request.')
1247 1246 log.exception(msg)
1248 1247 h.flash(msg, category='error')
1249 1248
1250 1249 # copy the args back to redirect
1251 1250 org_query = self.request.GET.mixed()
1252 1251 raise HTTPFound(
1253 1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1254 1253 _query=org_query))
1255 1254
1256 1255 raise HTTPFound(
1257 1256 h.route_path('pullrequest_show', repo_name=target_repo,
1258 1257 pull_request_id=pull_request.pull_request_id))
1259 1258
1260 1259 @LoginRequired()
1261 1260 @NotAnonymous()
1262 1261 @HasRepoPermissionAnyDecorator(
1263 1262 'repository.read', 'repository.write', 'repository.admin')
1264 1263 @CSRFRequired()
1265 1264 def pull_request_update(self):
1266 1265 pull_request = PullRequest.get_or_404(
1267 1266 self.request.matchdict['pull_request_id'])
1268 1267 _ = self.request.translate
1269 1268
1270 1269 c = self.load_default_context()
1271 1270 redirect_url = None
1272 1271 # we do this check as first, because we want to know ASAP in the flow that
1273 1272 # pr is updating currently
1274 1273 is_state_changing = pull_request.is_state_changing()
1275 1274
1276 1275 if pull_request.is_closed():
1277 1276 log.debug('update: forbidden because pull request is closed')
1278 1277 msg = _(u'Cannot update closed pull requests.')
1279 1278 h.flash(msg, category='error')
1280 1279 return {'response': True,
1281 1280 'redirect_url': redirect_url}
1282 1281
1283 1282 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1284 1283
1285 1284 # only owner or admin can update it
1286 1285 allowed_to_update = PullRequestModel().check_user_update(
1287 1286 pull_request, self._rhodecode_user)
1288 1287
1289 1288 if allowed_to_update:
1290 1289 controls = peppercorn.parse(self.request.POST.items())
1291 1290 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 1291 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1293 1292
1294 1293 if 'review_members' in controls:
1295 1294 self._update_reviewers(
1296 1295 c,
1297 1296 pull_request, controls['review_members'],
1298 1297 pull_request.reviewer_data,
1299 1298 PullRequestReviewers.ROLE_REVIEWER)
1300 1299 elif 'observer_members' in controls:
1301 1300 self._update_reviewers(
1302 1301 c,
1303 1302 pull_request, controls['observer_members'],
1304 1303 pull_request.reviewer_data,
1305 1304 PullRequestReviewers.ROLE_OBSERVER)
1306 1305 elif do_update_commits:
1307 1306 if is_state_changing:
1308 1307 log.debug('commits update: forbidden because pull request is in state %s',
1309 1308 pull_request.pull_request_state)
1310 1309 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1311 1310 u'Current state is: `{}`').format(
1312 1311 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1313 1312 h.flash(msg, category='error')
1314 1313 return {'response': True,
1315 1314 'redirect_url': redirect_url}
1316 1315
1317 1316 self._update_commits(c, pull_request)
1318 1317 if force_refresh:
1319 1318 redirect_url = h.route_path(
1320 1319 'pullrequest_show', repo_name=self.db_repo_name,
1321 1320 pull_request_id=pull_request.pull_request_id,
1322 1321 _query={"force_refresh": 1})
1323 1322 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1324 1323 self._edit_pull_request(pull_request)
1325 1324 else:
1326 1325 log.error('Unhandled update data.')
1327 1326 raise HTTPBadRequest()
1328 1327
1329 1328 return {'response': True,
1330 1329 'redirect_url': redirect_url}
1331 1330 raise HTTPForbidden()
1332 1331
1333 1332 def _edit_pull_request(self, pull_request):
1334 1333 """
1335 1334 Edit title and description
1336 1335 """
1337 1336 _ = self.request.translate
1338 1337
1339 1338 try:
1340 1339 PullRequestModel().edit(
1341 1340 pull_request,
1342 1341 self.request.POST.get('title'),
1343 1342 self.request.POST.get('description'),
1344 1343 self.request.POST.get('description_renderer'),
1345 1344 self._rhodecode_user)
1346 1345 except ValueError:
1347 1346 msg = _(u'Cannot update closed pull requests.')
1348 1347 h.flash(msg, category='error')
1349 1348 return
1350 1349 else:
1351 1350 Session().commit()
1352 1351
1353 1352 msg = _(u'Pull request title & description updated.')
1354 1353 h.flash(msg, category='success')
1355 1354 return
1356 1355
1357 1356 def _update_commits(self, c, pull_request):
1358 1357 _ = self.request.translate
1359 1358 log.debug('pull-request: running update commits actions')
1360 1359
1361 1360 @retry(exception=Exception, n_tries=3, delay=2)
1362 1361 def commits_update():
1363 1362 return PullRequestModel().update_commits(
1364 1363 pull_request, self._rhodecode_db_user)
1365 1364
1366 1365 with pull_request.set_state(PullRequest.STATE_UPDATING):
1367 1366 resp = commits_update() # retry x3
1368 1367
1369 1368 if resp.executed:
1370 1369
1371 1370 if resp.target_changed and resp.source_changed:
1372 1371 changed = 'target and source repositories'
1373 1372 elif resp.target_changed and not resp.source_changed:
1374 1373 changed = 'target repository'
1375 1374 elif not resp.target_changed and resp.source_changed:
1376 1375 changed = 'source repository'
1377 1376 else:
1378 1377 changed = 'nothing'
1379 1378
1380 1379 msg = _(u'Pull request updated to "{source_commit_id}" with '
1381 1380 u'{count_added} added, {count_removed} removed commits. '
1382 1381 u'Source of changes: {change_source}.')
1383 1382 msg = msg.format(
1384 1383 source_commit_id=pull_request.source_ref_parts.commit_id,
1385 1384 count_added=len(resp.changes.added),
1386 1385 count_removed=len(resp.changes.removed),
1387 1386 change_source=changed)
1388 1387 h.flash(msg, category='success')
1389 1388 channelstream.pr_update_channelstream_push(
1390 1389 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 1390 else:
1392 1391 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1393 1392 warning_reasons = [
1394 1393 UpdateFailureReason.NO_CHANGE,
1395 1394 UpdateFailureReason.WRONG_REF_TYPE,
1396 1395 ]
1397 1396 category = 'warning' if resp.reason in warning_reasons else 'error'
1398 1397 h.flash(msg, category=category)
1399 1398
1400 1399 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1401 1400 _ = self.request.translate
1402 1401
1403 1402 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1404 1403 PullRequestModel().get_reviewer_functions()
1405 1404
1406 1405 if role == PullRequestReviewers.ROLE_REVIEWER:
1407 1406 try:
1408 1407 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1409 1408 except ValueError as e:
1410 1409 log.error('Reviewers Validation: {}'.format(e))
1411 1410 h.flash(e, category='error')
1412 1411 return
1413 1412
1414 1413 old_calculated_status = pull_request.calculated_review_status()
1415 1414 PullRequestModel().update_reviewers(
1416 1415 pull_request, reviewers, self._rhodecode_db_user)
1417 1416
1418 1417 Session().commit()
1419 1418
1420 1419 msg = _('Pull request reviewers updated.')
1421 1420 h.flash(msg, category='success')
1422 1421 channelstream.pr_update_channelstream_push(
1423 1422 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1424 1423
1425 1424 # trigger status changed if change in reviewers changes the status
1426 1425 calculated_status = pull_request.calculated_review_status()
1427 1426 if old_calculated_status != calculated_status:
1428 1427 PullRequestModel().trigger_pull_request_hook(
1429 1428 pull_request, self._rhodecode_user, 'review_status_change',
1430 1429 data={'status': calculated_status})
1431 1430
1432 1431 elif role == PullRequestReviewers.ROLE_OBSERVER:
1433 1432 try:
1434 1433 observers = validate_observers(review_members, reviewer_rules)
1435 1434 except ValueError as e:
1436 1435 log.error('Observers Validation: {}'.format(e))
1437 1436 h.flash(e, category='error')
1438 1437 return
1439 1438
1440 1439 PullRequestModel().update_observers(
1441 1440 pull_request, observers, self._rhodecode_db_user)
1442 1441
1443 1442 Session().commit()
1444 1443 msg = _('Pull request observers updated.')
1445 1444 h.flash(msg, category='success')
1446 1445 channelstream.pr_update_channelstream_push(
1447 1446 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1448 1447
1449 1448 @LoginRequired()
1450 1449 @NotAnonymous()
1451 1450 @HasRepoPermissionAnyDecorator(
1452 1451 'repository.read', 'repository.write', 'repository.admin')
1453 1452 @CSRFRequired()
1454 1453 def pull_request_merge(self):
1455 1454 """
1456 1455 Merge will perform a server-side merge of the specified
1457 1456 pull request, if the pull request is approved and mergeable.
1458 1457 After successful merging, the pull request is automatically
1459 1458 closed, with a relevant comment.
1460 1459 """
1461 1460 pull_request = PullRequest.get_or_404(
1462 1461 self.request.matchdict['pull_request_id'])
1463 1462 _ = self.request.translate
1464 1463
1465 1464 if pull_request.is_state_changing():
1466 1465 log.debug('show: forbidden because pull request is in state %s',
1467 1466 pull_request.pull_request_state)
1468 1467 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1469 1468 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1470 1469 pull_request.pull_request_state)
1471 1470 h.flash(msg, category='error')
1472 1471 raise HTTPFound(
1473 1472 h.route_path('pullrequest_show',
1474 1473 repo_name=pull_request.target_repo.repo_name,
1475 1474 pull_request_id=pull_request.pull_request_id))
1476 1475
1477 1476 self.load_default_context()
1478 1477
1479 1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1480 1479 check = MergeCheck.validate(
1481 1480 pull_request, auth_user=self._rhodecode_user,
1482 1481 translator=self.request.translate)
1483 1482 merge_possible = not check.failed
1484 1483
1485 1484 for err_type, error_msg in check.errors:
1486 1485 h.flash(error_msg, category=err_type)
1487 1486
1488 1487 if merge_possible:
1489 1488 log.debug("Pre-conditions checked, trying to merge.")
1490 1489 extras = vcs_operation_context(
1491 1490 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1492 1491 username=self._rhodecode_db_user.username, action='push',
1493 1492 scm=pull_request.target_repo.repo_type)
1494 1493 with pull_request.set_state(PullRequest.STATE_UPDATING):
1495 1494 self._merge_pull_request(
1496 1495 pull_request, self._rhodecode_db_user, extras)
1497 1496 else:
1498 1497 log.debug("Pre-conditions failed, NOT merging.")
1499 1498
1500 1499 raise HTTPFound(
1501 1500 h.route_path('pullrequest_show',
1502 1501 repo_name=pull_request.target_repo.repo_name,
1503 1502 pull_request_id=pull_request.pull_request_id))
1504 1503
1505 1504 def _merge_pull_request(self, pull_request, user, extras):
1506 1505 _ = self.request.translate
1507 1506 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1508 1507
1509 1508 if merge_resp.executed:
1510 1509 log.debug("The merge was successful, closing the pull request.")
1511 1510 PullRequestModel().close_pull_request(
1512 1511 pull_request.pull_request_id, user)
1513 1512 Session().commit()
1514 1513 msg = _('Pull request was successfully merged and closed.')
1515 1514 h.flash(msg, category='success')
1516 1515 else:
1517 1516 log.debug(
1518 1517 "The merge was not successful. Merge response: %s", merge_resp)
1519 1518 msg = merge_resp.merge_status_message
1520 1519 h.flash(msg, category='error')
1521 1520
1522 1521 @LoginRequired()
1523 1522 @NotAnonymous()
1524 1523 @HasRepoPermissionAnyDecorator(
1525 1524 'repository.read', 'repository.write', 'repository.admin')
1526 1525 @CSRFRequired()
1527 1526 def pull_request_delete(self):
1528 1527 _ = self.request.translate
1529 1528
1530 1529 pull_request = PullRequest.get_or_404(
1531 1530 self.request.matchdict['pull_request_id'])
1532 1531 self.load_default_context()
1533 1532
1534 1533 pr_closed = pull_request.is_closed()
1535 1534 allowed_to_delete = PullRequestModel().check_user_delete(
1536 1535 pull_request, self._rhodecode_user) and not pr_closed
1537 1536
1538 1537 # only owner can delete it !
1539 1538 if allowed_to_delete:
1540 1539 PullRequestModel().delete(pull_request, self._rhodecode_user)
1541 1540 Session().commit()
1542 1541 h.flash(_('Successfully deleted pull request'),
1543 1542 category='success')
1544 1543 raise HTTPFound(h.route_path('pullrequest_show_all',
1545 1544 repo_name=self.db_repo_name))
1546 1545
1547 1546 log.warning('user %s tried to delete pull request without access',
1548 1547 self._rhodecode_user)
1549 1548 raise HTTPNotFound()
1550 1549
1551 1550 def _pull_request_comments_create(self, pull_request, comments):
1552 1551 _ = self.request.translate
1553 1552 data = {}
1554 1553 if not comments:
1555 1554 return
1556 1555 pull_request_id = pull_request.pull_request_id
1557 1556
1558 1557 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1559 1558
1560 1559 for entry in comments:
1561 1560 c = self.load_default_context()
1562 1561 comment_type = entry['comment_type']
1563 1562 text = entry['text']
1564 1563 status = entry['status']
1565 1564 is_draft = str2bool(entry['is_draft'])
1566 1565 resolves_comment_id = entry['resolves_comment_id']
1567 1566 close_pull_request = entry['close_pull_request']
1568 1567 f_path = entry['f_path']
1569 1568 line_no = entry['line']
1570 1569 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
1571 1570
1572 1571 # the logic here should work like following, if we submit close
1573 1572 # pr comment, use `close_pull_request_with_comment` function
1574 1573 # else handle regular comment logic
1575 1574
1576 1575 if close_pull_request:
1577 1576 # only owner or admin or person with write permissions
1578 1577 allowed_to_close = PullRequestModel().check_user_update(
1579 1578 pull_request, self._rhodecode_user)
1580 1579 if not allowed_to_close:
1581 1580 log.debug('comment: forbidden because not allowed to close '
1582 1581 'pull request %s', pull_request_id)
1583 1582 raise HTTPForbidden()
1584 1583
1585 1584 # This also triggers `review_status_change`
1586 1585 comment, status = PullRequestModel().close_pull_request_with_comment(
1587 1586 pull_request, self._rhodecode_user, self.db_repo, message=text,
1588 1587 auth_user=self._rhodecode_user)
1589 1588 Session().flush()
1590 1589 is_inline = comment.is_inline
1591 1590
1592 1591 PullRequestModel().trigger_pull_request_hook(
1593 1592 pull_request, self._rhodecode_user, 'comment',
1594 1593 data={'comment': comment})
1595 1594
1596 1595 else:
1597 1596 # regular comment case, could be inline, or one with status.
1598 1597 # for that one we check also permissions
1599 1598 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1600 1599 allowed_to_change_status = PullRequestModel().check_user_change_status(
1601 1600 pull_request, self._rhodecode_user) and not is_draft
1602 1601
1603 1602 if status and allowed_to_change_status:
1604 1603 message = (_('Status change %(transition_icon)s %(status)s')
1605 1604 % {'transition_icon': '>',
1606 1605 'status': ChangesetStatus.get_status_lbl(status)})
1607 1606 text = text or message
1608 1607
1609 1608 comment = CommentsModel().create(
1610 1609 text=text,
1611 1610 repo=self.db_repo.repo_id,
1612 1611 user=self._rhodecode_user.user_id,
1613 1612 pull_request=pull_request,
1614 1613 f_path=f_path,
1615 1614 line_no=line_no,
1616 1615 status_change=(ChangesetStatus.get_status_lbl(status)
1617 1616 if status and allowed_to_change_status else None),
1618 1617 status_change_type=(status
1619 1618 if status and allowed_to_change_status else None),
1620 1619 comment_type=comment_type,
1621 1620 is_draft=is_draft,
1622 1621 resolves_comment_id=resolves_comment_id,
1623 1622 auth_user=self._rhodecode_user,
1624 1623 send_email=not is_draft, # skip notification for draft comments
1625 1624 )
1626 1625 is_inline = comment.is_inline
1627 1626
1628 1627 if allowed_to_change_status:
1629 1628 # calculate old status before we change it
1630 1629 old_calculated_status = pull_request.calculated_review_status()
1631 1630
1632 1631 # get status if set !
1633 1632 if status:
1634 1633 ChangesetStatusModel().set_status(
1635 1634 self.db_repo.repo_id,
1636 1635 status,
1637 1636 self._rhodecode_user.user_id,
1638 1637 comment,
1639 1638 pull_request=pull_request
1640 1639 )
1641 1640
1642 1641 Session().flush()
1643 1642 # this is somehow required to get access to some relationship
1644 1643 # loaded on comment
1645 1644 Session().refresh(comment)
1646 1645
1647 1646 # skip notifications for drafts
1648 1647 if not is_draft:
1649 1648 PullRequestModel().trigger_pull_request_hook(
1650 1649 pull_request, self._rhodecode_user, 'comment',
1651 1650 data={'comment': comment})
1652 1651
1653 1652 # we now calculate the status of pull request, and based on that
1654 1653 # calculation we set the commits status
1655 1654 calculated_status = pull_request.calculated_review_status()
1656 1655 if old_calculated_status != calculated_status:
1657 1656 PullRequestModel().trigger_pull_request_hook(
1658 1657 pull_request, self._rhodecode_user, 'review_status_change',
1659 1658 data={'status': calculated_status})
1660 1659
1661 1660 comment_id = comment.comment_id
1662 1661 data[comment_id] = {
1663 1662 'target_id': target_elem_id
1664 1663 }
1665 1664 Session().flush()
1666 1665
1667 1666 c.co = comment
1668 1667 c.at_version_num = None
1669 1668 c.is_new = True
1670 1669 rendered_comment = render(
1671 1670 'rhodecode:templates/changeset/changeset_comment_block.mako',
1672 1671 self._get_template_context(c), self.request)
1673 1672
1674 1673 data[comment_id].update(comment.get_dict())
1675 1674 data[comment_id].update({'rendered_text': rendered_comment})
1676 1675
1677 1676 Session().commit()
1678 1677
1679 1678 # skip channelstream for draft comments
1680 1679 if not all_drafts:
1681 1680 comment_broadcast_channel = channelstream.comment_channel(
1682 1681 self.db_repo_name, pull_request_obj=pull_request)
1683 1682
1684 1683 comment_data = data
1685 1684 posted_comment_type = 'inline' if is_inline else 'general'
1686 1685 if len(data) == 1:
1687 1686 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1688 1687 else:
1689 1688 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1690 1689
1691 1690 channelstream.comment_channelstream_push(
1692 1691 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1693 1692 comment_data=comment_data)
1694 1693
1695 1694 return data
1696 1695
1697 1696 @LoginRequired()
1698 1697 @NotAnonymous()
1699 1698 @HasRepoPermissionAnyDecorator(
1700 1699 'repository.read', 'repository.write', 'repository.admin')
1701 1700 @CSRFRequired()
1702 1701 def pull_request_comment_create(self):
1703 1702 _ = self.request.translate
1704 1703
1705 1704 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1706 1705
1707 1706 if pull_request.is_closed():
1708 1707 log.debug('comment: forbidden because pull request is closed')
1709 1708 raise HTTPForbidden()
1710 1709
1711 1710 allowed_to_comment = PullRequestModel().check_user_comment(
1712 1711 pull_request, self._rhodecode_user)
1713 1712 if not allowed_to_comment:
1714 1713 log.debug('comment: forbidden because pull request is from forbidden repo')
1715 1714 raise HTTPForbidden()
1716 1715
1717 1716 comment_data = {
1718 1717 'comment_type': self.request.POST.get('comment_type'),
1719 1718 'text': self.request.POST.get('text'),
1720 1719 'status': self.request.POST.get('changeset_status', None),
1721 1720 'is_draft': self.request.POST.get('draft'),
1722 1721 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1723 1722 'close_pull_request': self.request.POST.get('close_pull_request'),
1724 1723 'f_path': self.request.POST.get('f_path'),
1725 1724 'line': self.request.POST.get('line'),
1726 1725 }
1727 1726 data = self._pull_request_comments_create(pull_request, [comment_data])
1728 1727
1729 1728 return data
1730 1729
1731 1730 @LoginRequired()
1732 1731 @NotAnonymous()
1733 1732 @HasRepoPermissionAnyDecorator(
1734 1733 'repository.read', 'repository.write', 'repository.admin')
1735 1734 @CSRFRequired()
1736 1735 def pull_request_comment_delete(self):
1737 1736 pull_request = PullRequest.get_or_404(
1738 1737 self.request.matchdict['pull_request_id'])
1739 1738
1740 1739 comment = ChangesetComment.get_or_404(
1741 1740 self.request.matchdict['comment_id'])
1742 1741 comment_id = comment.comment_id
1743 1742
1744 1743 if comment.immutable:
1745 1744 # don't allow deleting comments that are immutable
1746 1745 raise HTTPForbidden()
1747 1746
1748 1747 if pull_request.is_closed():
1749 1748 log.debug('comment: forbidden because pull request is closed')
1750 1749 raise HTTPForbidden()
1751 1750
1752 1751 if not comment:
1753 1752 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 1753 # comment already deleted in another call probably
1755 1754 return True
1756 1755
1757 1756 if comment.pull_request.is_closed():
1758 1757 # don't allow deleting comments on closed pull request
1759 1758 raise HTTPForbidden()
1760 1759
1761 1760 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 1761 super_admin = h.HasPermissionAny('hg.admin')()
1763 1762 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 1763 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 1764 comment_repo_admin = is_repo_admin and is_repo_comment
1766 1765
1767 1766 if comment.draft and not comment_owner:
1768 1767 # We never allow to delete draft comments for other than owners
1769 1768 raise HTTPNotFound()
1770 1769
1771 1770 if super_admin or comment_owner or comment_repo_admin:
1772 1771 old_calculated_status = comment.pull_request.calculated_review_status()
1773 1772 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 1773 Session().commit()
1775 1774 calculated_status = comment.pull_request.calculated_review_status()
1776 1775 if old_calculated_status != calculated_status:
1777 1776 PullRequestModel().trigger_pull_request_hook(
1778 1777 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 1778 data={'status': calculated_status})
1780 1779 return True
1781 1780 else:
1782 1781 log.warning('No permissions for user %s to delete comment_id: %s',
1783 1782 self._rhodecode_db_user, comment_id)
1784 1783 raise HTTPNotFound()
1785 1784
1786 1785 @LoginRequired()
1787 1786 @NotAnonymous()
1788 1787 @HasRepoPermissionAnyDecorator(
1789 1788 'repository.read', 'repository.write', 'repository.admin')
1790 1789 @CSRFRequired()
1791 1790 def pull_request_comment_edit(self):
1792 1791 self.load_default_context()
1793 1792
1794 1793 pull_request = PullRequest.get_or_404(
1795 1794 self.request.matchdict['pull_request_id']
1796 1795 )
1797 1796 comment = ChangesetComment.get_or_404(
1798 1797 self.request.matchdict['comment_id']
1799 1798 )
1800 1799 comment_id = comment.comment_id
1801 1800
1802 1801 if comment.immutable:
1803 1802 # don't allow deleting comments that are immutable
1804 1803 raise HTTPForbidden()
1805 1804
1806 1805 if pull_request.is_closed():
1807 1806 log.debug('comment: forbidden because pull request is closed')
1808 1807 raise HTTPForbidden()
1809 1808
1810 1809 if comment.pull_request.is_closed():
1811 1810 # don't allow deleting comments on closed pull request
1812 1811 raise HTTPForbidden()
1813 1812
1814 1813 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 1814 super_admin = h.HasPermissionAny('hg.admin')()
1816 1815 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 1816 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 1817 comment_repo_admin = is_repo_admin and is_repo_comment
1819 1818
1820 1819 if super_admin or comment_owner or comment_repo_admin:
1821 1820 text = self.request.POST.get('text')
1822 1821 version = self.request.POST.get('version')
1823 1822 if text == comment.text:
1824 1823 log.warning(
1825 1824 'Comment(PR): '
1826 1825 'Trying to create new version '
1827 1826 'with the same comment body {}'.format(
1828 1827 comment_id,
1829 1828 )
1830 1829 )
1831 1830 raise HTTPNotFound()
1832 1831
1833 1832 if version.isdigit():
1834 1833 version = int(version)
1835 1834 else:
1836 1835 log.warning(
1837 1836 'Comment(PR): Wrong version type {} {} '
1838 1837 'for comment {}'.format(
1839 1838 version,
1840 1839 type(version),
1841 1840 comment_id,
1842 1841 )
1843 1842 )
1844 1843 raise HTTPNotFound()
1845 1844
1846 1845 try:
1847 1846 comment_history = CommentsModel().edit(
1848 1847 comment_id=comment_id,
1849 1848 text=text,
1850 1849 auth_user=self._rhodecode_user,
1851 1850 version=version,
1852 1851 )
1853 1852 except CommentVersionMismatch:
1854 1853 raise HTTPConflict()
1855 1854
1856 1855 if not comment_history:
1857 1856 raise HTTPNotFound()
1858 1857
1859 1858 Session().commit()
1860 1859 if not comment.draft:
1861 1860 PullRequestModel().trigger_pull_request_hook(
1862 1861 pull_request, self._rhodecode_user, 'comment_edit',
1863 1862 data={'comment': comment})
1864 1863
1865 1864 return {
1866 1865 'comment_history_id': comment_history.comment_history_id,
1867 1866 'comment_id': comment.comment_id,
1868 1867 'comment_version': comment_history.version,
1869 1868 'comment_author_username': comment_history.author.username,
1870 1869 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1871 1870 'comment_created_on': h.age_component(comment_history.created_on,
1872 1871 time_is_local=True),
1873 1872 }
1874 1873 else:
1875 1874 log.warning('No permissions for user %s to edit comment_id: %s',
1876 1875 self._rhodecode_db_user, comment_id)
1877 1876 raise HTTPNotFound()
@@ -1,251 +1,277 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import deform
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode import events
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.permission import PermissionModel
36 36 from rhodecode.model.repo import RepoModel
37 37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 38 from rhodecode.model.validation_schema.schemas import repo_schema
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47
48 48 acl_groups = RepoGroupList(
49 49 RepoGroup.query().all(),
50 50 perm_set=['group.write', 'group.admin'])
51 51 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
52 52 c.repo_groups_choices = list(map(lambda k: k[0], c.repo_groups))
53 53
54 54 # in case someone no longer have a group.write access to a repository
55 55 # pre fill the list with this entry, we don't care if this is the same
56 56 # but it will allow saving repo data properly.
57 57 repo_group = self.db_repo.group
58 58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 59 c.repo_groups_choices.append(repo_group.group_id)
60 60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61 61
62 62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 63 # we might be in missing requirement state, so we load things
64 64 # without touching scm_instance()
65 65 c.landing_revs_choices, c.landing_revs = \
66 66 ScmModel().get_repo_landing_revs(self.request.translate)
67 67 else:
68 68 c.landing_revs_choices, c.landing_revs = \
69 69 ScmModel().get_repo_landing_revs(
70 70 self.request.translate, self.db_repo)
71 71
72 72 c.personal_repo_group = c.auth_user.personal_repo_group
73 73 c.repo_fields = RepositoryField.query()\
74 74 .filter(RepositoryField.repository == self.db_repo).all()
75 75 return c
76 76
77 77 def _get_schema(self, c, old_values=None):
78 78 return repo_schema.RepoSettingsSchema().bind(
79 79 repo_type=self.db_repo.repo_type,
80 80 repo_type_options=[self.db_repo.repo_type],
81 81 repo_ref_options=c.landing_revs_choices,
82 82 repo_ref_items=c.landing_revs,
83 83 repo_repo_group_options=c.repo_groups_choices,
84 84 repo_repo_group_items=c.repo_groups,
85 85 # user caller
86 86 user=self._rhodecode_user,
87 87 old_values=old_values
88 88 )
89 89
90 90 @LoginRequired()
91 91 @HasRepoPermissionAnyDecorator('repository.admin')
92 92 def edit_settings(self):
93 93 c = self.load_default_context()
94 94 c.active = 'settings'
95 95
96 96 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 97 defaults['repo_owner'] = defaults['user']
98 98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99 99
100 100 schema = self._get_schema(c)
101 101 c.form = RcForm(schema, appstruct=defaults)
102 102 return self._get_template_context(c)
103 103
104 104 @LoginRequired()
105 105 @HasRepoPermissionAnyDecorator('repository.admin')
106 106 @CSRFRequired()
107 107 def edit_settings_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'settings'
111 111 old_repo_name = self.db_repo_name
112 112
113 113 old_values = self.db_repo.get_api_data()
114 114 schema = self._get_schema(c, old_values=old_values)
115 115
116 116 c.form = RcForm(schema)
117 117 pstruct = list(self.request.POST.items())
118 118 pstruct.append(('repo_type', self.db_repo.repo_type))
119 119 try:
120 120 schema_data = c.form.validate(pstruct)
121 121 except deform.ValidationFailure as err_form:
122 122 return self._get_template_context(c)
123 123
124 124 # data is now VALID, proceed with updates
125 125 # save validated data back into the updates dict
126 126 validated_updates = dict(
127 127 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 128 repo_group=schema_data['repo_group']['repo_group_id'],
129 129
130 130 user=schema_data['repo_owner'],
131 131 repo_description=schema_data['repo_description'],
132 132 repo_private=schema_data['repo_private'],
133 133 clone_uri=schema_data['repo_clone_uri'],
134 134 push_uri=schema_data['repo_push_uri'],
135 135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 136 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 137 repo_enable_locking=schema_data['repo_enable_locking'],
138 138 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 139 )
140 140 # detect if SYNC URI changed, if we get OLD means we keep old values
141 141 if schema_data['repo_clone_uri_change'] == 'OLD':
142 142 validated_updates['clone_uri'] = self.db_repo.clone_uri
143 143
144 144 if schema_data['repo_push_uri_change'] == 'OLD':
145 145 validated_updates['push_uri'] = self.db_repo.push_uri
146 146
147 147 # use the new full name for redirect
148 148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149 149
150 150 # save extra fields into our validated data
151 151 for key, value in pstruct:
152 152 if key.startswith(RepositoryField.PREFIX):
153 153 validated_updates[key] = value
154 154
155 155 try:
156 156 RepoModel().update(self.db_repo, **validated_updates)
157 157 ScmModel().mark_for_invalidation(new_repo_name)
158 158
159 159 audit_logger.store_web(
160 160 'repo.edit', action_data={'old_data': old_values},
161 161 user=self._rhodecode_user, repo=self.db_repo)
162 162
163 163 Session().commit()
164 164
165 165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 166 category='success')
167 167 except Exception:
168 168 log.exception("Exception during update of repository")
169 169 h.flash(_('Error occurred during update of repository {}').format(
170 170 old_repo_name), category='error')
171 171
172 172 name_changed = old_repo_name != new_repo_name
173 173 if name_changed:
174 174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 175 affected_user_ids = [perm['user_id'] for perm in current_perms]
176 176
177 177 # NOTE(marcink): also add owner maybe it has changed
178 178 owner = User.get_by_username(schema_data['repo_owner'])
179 179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 181 PermissionModel().trigger_permission_flush(affected_user_ids)
182 182
183 183 raise HTTPFound(
184 184 h.route_path('edit_repo', repo_name=new_repo_name))
185 185
186 186 @LoginRequired()
187 187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 188 def toggle_locking(self):
189 189 """
190 190 Toggle locking of repository by simple GET call to url
191 191 """
192 192 _ = self.request.translate
193 193 repo = self.db_repo
194 194
195 195 try:
196 196 if repo.enable_locking:
197 197 if repo.locked[0]:
198 198 Repository.unlock(repo)
199 199 action = _('Unlocked')
200 200 else:
201 201 Repository.lock(
202 202 repo, self._rhodecode_user.user_id,
203 203 lock_reason=Repository.LOCK_WEB)
204 204 action = _('Locked')
205 205
206 206 h.flash(_('Repository has been %s') % action,
207 207 category='success')
208 208 except Exception:
209 209 log.exception("Exception during unlocking")
210 210 h.flash(_('An error occurred during unlocking'),
211 211 category='error')
212 212 raise HTTPFound(
213 213 h.route_path('repo_summary', repo_name=self.db_repo_name))
214 214
215 215 @LoginRequired()
216 216 @HasRepoPermissionAnyDecorator('repository.admin')
217 217 def edit_statistics_form(self):
218 218 c = self.load_default_context()
219 219
220 220 if self.db_repo.stats:
221 221 # this is on what revision we ended up so we add +1 for count
222 222 last_rev = self.db_repo.stats.stat_on_revision + 1
223 223 else:
224 224 last_rev = 0
225 225
226 226 c.active = 'statistics'
227 227 c.stats_revision = last_rev
228 228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229 229
230 230 if last_rev == 0 or c.repo_last_rev == 0:
231 231 c.stats_percentage = 0
232 232 else:
233 233 c.stats_percentage = '%.2f' % (
234 234 (float((last_rev)) / c.repo_last_rev) * 100)
235 235 return self._get_template_context(c)
236 236
237 237 @LoginRequired()
238 238 @HasRepoPermissionAnyDecorator('repository.admin')
239 239 @CSRFRequired()
240 240 def repo_statistics_reset(self):
241 241 _ = self.request.translate
242 242
243 243 try:
244 244 RepoModel().delete_stats(self.db_repo_name)
245 245 Session().commit()
246 246 except Exception:
247 247 log.exception('Edit statistics failure')
248 248 h.flash(_('An error occurred during deletion of repository stats'),
249 249 category='error')
250 250 raise HTTPFound(
251 251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
252
253 @LoginRequired()
254 @HasRepoPermissionAnyDecorator('repository.admin')
255 def repo_settings_quick_actions(self):
256 _ = self.request.translate
257
258 set_lock = self.request.GET.get('set_lock')
259 set_unlock = self.request.GET.get('set_unlock')
260
261 try:
262 if set_lock:
263 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
264 lock_reason=Repository.LOCK_WEB)
265 h.flash(_('Locked repository'), category='success')
266 elif set_unlock:
267 Repository.unlock(self.db_repo)
268 h.flash(_('Unlocked repository'), category='success')
269 except Exception as e:
270 log.exception("Exception during unlocking")
271 h.flash(_('An error occurred during unlocking'), category='error')
272
273 raise HTTPFound(
274 h.route_path('repo_summary', repo_name=self.db_repo_name))
275
276
277
@@ -1,264 +1,264 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import logging
24 24 import datetime
25 25 import configparser
26 26 from sqlalchemy import Table
27 27
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.model.scm import ScmModel
30 30
31 31 from .hg import MercurialServer
32 32 from .git import GitServer
33 33 from .svn import SubversionServer
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class SshWrapper(object):
38 38 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
39 39 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
40 40 svn_cmd_pat = re.compile(r'^svnserve -t')
41 41
42 42 def __init__(self, command, connection_info, mode,
43 43 user, user_id, key_id: int, shell, ini_path: str, env):
44 44 self.command = command
45 45 self.connection_info = connection_info
46 46 self.mode = mode
47 47 self.username = user
48 48 self.user_id = user_id
49 49 self.key_id = key_id
50 50 self.shell = shell
51 51 self.ini_path = ini_path
52 52 self.env = env
53 53
54 54 self.config = self.parse_config(ini_path)
55 55 self.server_impl = None
56 56
57 57 def parse_config(self, config_path):
58 58 parser = configparser.ConfigParser()
59 59 parser.read(config_path)
60 60 return parser
61 61
62 62 def update_key_access_time(self, key_id):
63 63 from rhodecode.model.meta import raw_query_executor, Base
64 64
65 65 table = Table('user_ssh_keys', Base.metadata, autoload=False)
66 atime = datetime.datetime.utcnow()
66 67 stmt = (
67 68 table.update()
68 69 .where(table.c.ssh_key_id == key_id)
69 .values(accessed_on=datetime.datetime.utcnow())
70 .returning(table.c.accessed_on, table.c.ssh_key_fingerprint)
70 .values(accessed_on=atime)
71 # no MySQL Support for .returning :((
72 #.returning(table.c.accessed_on, table.c.ssh_key_fingerprint)
71 73 )
72 74
73 scalar_res = None
75 res_count = None
74 76 with raw_query_executor() as session:
75 77 result = session.execute(stmt)
76 78 if result.rowcount:
77 scalar_res = result.first()
79 res_count = result.rowcount
78 80
79 if scalar_res:
80 atime, ssh_key_fingerprint = scalar_res
81 log.debug('Update key id:`%s` fingerprint:`%s` access time',
82 key_id, ssh_key_fingerprint)
81 if res_count:
82 log.debug('Update key id:`%s` access time', key_id)
83 83
84 84 def get_user(self, user_id):
85 85 user = AttributeDict()
86 86 # lazy load db imports
87 87 from rhodecode.model.db import User
88 88 dbuser = User.get(user_id)
89 89 if not dbuser:
90 90 return None
91 91 user.user_id = dbuser.user_id
92 92 user.username = dbuser.username
93 93 user.auth_user = dbuser.AuthUser()
94 94 return user
95 95
96 96 def get_connection_info(self):
97 97 """
98 98 connection_info
99 99
100 100 Identifies the client and server ends of the connection.
101 101 The variable contains four space-separated values: client IP address,
102 102 client port number, server IP address, and server port number.
103 103 """
104 104 conn = dict(
105 105 client_ip=None,
106 106 client_port=None,
107 107 server_ip=None,
108 108 server_port=None,
109 109 )
110 110
111 111 info = self.connection_info.split(' ')
112 112 if len(info) == 4:
113 113 conn['client_ip'] = info[0]
114 114 conn['client_port'] = info[1]
115 115 conn['server_ip'] = info[2]
116 116 conn['server_port'] = info[3]
117 117
118 118 return conn
119 119
120 120 def maybe_translate_repo_uid(self, repo_name):
121 121 _org_name = repo_name
122 122 if _org_name.startswith('_'):
123 123 # remove format of _ID/subrepo
124 124 _org_name = _org_name.split('/', 1)[0]
125 125
126 126 if repo_name.startswith('_'):
127 127 from rhodecode.model.repo import RepoModel
128 128 org_repo_name = repo_name
129 129 log.debug('translating UID repo %s', org_repo_name)
130 130 by_id_match = RepoModel().get_repo_by_id(repo_name)
131 131 if by_id_match:
132 132 repo_name = by_id_match.repo_name
133 133 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
134 134
135 135 return repo_name, _org_name
136 136
137 137 def get_repo_details(self, mode):
138 138 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
139 139 repo_name = None
140 140
141 141 hg_match = self.hg_cmd_pat.match(self.command)
142 142 if hg_match is not None:
143 143 vcs_type = 'hg'
144 144 repo_id = hg_match.group(1).strip('/')
145 145 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
146 146 return vcs_type, repo_name, mode
147 147
148 148 git_match = self.git_cmd_pat.match(self.command)
149 149 if git_match is not None:
150 150 mode = git_match.group(1)
151 151 vcs_type = 'git'
152 152 repo_id = git_match.group(2).strip('/')
153 153 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
154 154 return vcs_type, repo_name, mode
155 155
156 156 svn_match = self.svn_cmd_pat.match(self.command)
157 157 if svn_match is not None:
158 158 vcs_type = 'svn'
159 159 # Repo name should be extracted from the input stream, we're unable to
160 160 # extract it at this point in execution
161 161 return vcs_type, repo_name, mode
162 162
163 163 return vcs_type, repo_name, mode
164 164
165 165 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
166 166 store = ScmModel().repos_path
167 167
168 168 check_branch_perms = False
169 169 detect_force_push = False
170 170
171 171 if branch_permissions:
172 172 check_branch_perms = True
173 173 detect_force_push = True
174 174
175 175 log.debug(
176 176 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
177 177 vcs, mode, repo, check_branch_perms)
178 178
179 179 # detect if we have to check branch permissions
180 180 extras = {
181 181 'detect_force_push': detect_force_push,
182 182 'check_branch_perms': check_branch_perms,
183 183 }
184 184
185 185 if vcs == 'hg':
186 186 server = MercurialServer(
187 187 store=store, ini_path=self.ini_path,
188 188 repo_name=repo, user=user,
189 189 user_permissions=permissions, config=self.config, env=self.env)
190 190 self.server_impl = server
191 191 return server.run(tunnel_extras=extras)
192 192
193 193 elif vcs == 'git':
194 194 server = GitServer(
195 195 store=store, ini_path=self.ini_path,
196 196 repo_name=repo, repo_mode=mode, user=user,
197 197 user_permissions=permissions, config=self.config, env=self.env)
198 198 self.server_impl = server
199 199 return server.run(tunnel_extras=extras)
200 200
201 201 elif vcs == 'svn':
202 202 server = SubversionServer(
203 203 store=store, ini_path=self.ini_path,
204 204 repo_name=None, user=user,
205 205 user_permissions=permissions, config=self.config, env=self.env)
206 206 self.server_impl = server
207 207 return server.run(tunnel_extras=extras)
208 208
209 209 else:
210 210 raise Exception('Unrecognised VCS: {}'.format(vcs))
211 211
212 212 def wrap(self):
213 213 mode = self.mode
214 214 username = self.username
215 215 user_id = self.user_id
216 216 key_id = self.key_id
217 217 shell = self.shell
218 218
219 219 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
220 220
221 221 log.debug(
222 222 'Mode: `%s` User: `name:%s : id:%s` Shell: `%s` SSH Command: `\"%s\"` '
223 223 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
224 224 mode, username, user_id, shell, self.command,
225 225 scm_detected, scm_mode, scm_repo)
226 226
227 227 log.debug('SSH Connection info %s', self.get_connection_info())
228 228
229 229 # update last access time for this key
230 230 if key_id:
231 231 self.update_key_access_time(key_id)
232 232
233 233 if shell and self.command is None:
234 234 log.info('Dropping to shell, no command given and shell is allowed')
235 235 os.execl('/bin/bash', '-l')
236 236 exit_code = 1
237 237
238 238 elif scm_detected:
239 239 user = self.get_user(user_id)
240 240 if not user:
241 241 log.warning('User with id %s not found', user_id)
242 242 exit_code = -1
243 243 return exit_code
244 244
245 245 auth_user = user.auth_user
246 246 permissions = auth_user.permissions['repositories']
247 247 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
248 248 try:
249 249 exit_code, is_updated = self.serve(
250 250 scm_detected, scm_repo, scm_mode, user, permissions,
251 251 repo_branch_permissions)
252 252 except Exception:
253 253 log.exception('Error occurred during execution of SshWrapper')
254 254 exit_code = -1
255 255
256 256 elif self.command is None and shell is False:
257 257 log.error('No Command given.')
258 258 exit_code = -1
259 259
260 260 else:
261 261 log.error('Unhandled Command: "%s" Aborting.', self.command)
262 262 exit_code = -1
263 263
264 264 return exit_code
@@ -1,133 +1,138 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import stat
23 23 import logging
24 24 import tempfile
25 25 import datetime
26 26
27 27 from . import config_keys
28 28 from rhodecode.model.db import true, joinedload, User, UserSshKeys
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33 HEADER = \
34 34 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
35 35 "# Current entries: {}, create date: UTC:{}.\n"
36 36
37 37 # Default SSH options for authorized_keys file, can be override via .ini
38 38 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
39 39
40 40
41 41 def get_all_active_keys():
42 42 result = UserSshKeys.query() \
43 .options(joinedload(UserSshKeys.user)) \
44 .filter(UserSshKeys.user != User.get_default_user()) \
43 .join(User) \
44 .filter(User != User.get_default_user()) \
45 45 .filter(User.active == true()) \
46 46 .all()
47 47 return result
48 48
49 49
50 50 def _generate_ssh_authorized_keys_file(
51 51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52 52 import rhodecode
53 53
54 54 authorized_keys_file_path = os.path.abspath(
55 55 os.path.expanduser(authorized_keys_file_path))
56 56 tmp_file_dir = os.path.dirname(authorized_keys_file_path)
57 57
58 if not os.path.exists(tmp_file_dir):
59 log.debug('SSH authorized_keys file dir does not exist, creating one now...')
60 os.makedirs(tmp_file_dir)
61
58 62 all_active_keys = get_all_active_keys()
59 63
60 64 if allow_shell:
61 65 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
62 66 if debug:
63 67 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
64 68
65 69 if not os.path.isfile(authorized_keys_file_path):
66 70 log.debug('Creating file at %s', authorized_keys_file_path)
67 71 with open(authorized_keys_file_path, 'w'):
72 # create a file with write access
68 73 pass
69 74
70 75 if not os.access(authorized_keys_file_path, os.R_OK):
71 76 raise OSError('Access to file {} is without read access'.format(
72 77 authorized_keys_file_path))
73 78
74 79 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
75 80
76 81 fd, tmp_authorized_keys = tempfile.mkstemp(
77 82 '.authorized_keys_write_operation',
78 83 dir=tmp_file_dir)
79 84
80 85 now = datetime.datetime.utcnow().isoformat()
81 keys_file = os.fdopen(fd, 'wb')
86 keys_file = os.fdopen(fd, 'wt')
82 87 keys_file.write(HEADER.format(len(all_active_keys), now))
83 88 ini_path = rhodecode.CONFIG['__file__']
84 89
85 90 for user_key in all_active_keys:
86 91 username = user_key.user.username
87 92 user_id = user_key.user.user_id
88 93 # replace all newline from ends and inside
89 94 safe_key_data = user_key.ssh_key_data\
90 95 .strip()\
91 96 .replace('\n', ' ') \
92 97 .replace('\t', ' ') \
93 98 .replace('\r', ' ')
94 99
95 100 line = line_tmpl.format(
96 101 ssh_opts=ssh_opts or SSH_OPTS,
97 102 wrapper_command=ssh_wrapper_cmd,
98 103 ini_path=ini_path,
99 104 user_id=user_id,
100 105 user=username,
101 106 user_key_id=user_key.ssh_key_id,
102 107 key=safe_key_data)
103 108
104 109 keys_file.write(line)
105 110 log.debug('addkey: Key added for user: `%s`', username)
106 111 keys_file.close()
107 112
108 113 # Explicitly setting read-only permissions to authorized_keys
109 114 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
110 115 # Rename is atomic operation
111 116 os.rename(tmp_authorized_keys, authorized_keys_file_path)
112 117
113 118
114 119 def generate_ssh_authorized_keys_file(registry):
115 120 log.info('Generating new authorized key file')
116 121
117 122 authorized_keys_file_path = registry.settings.get(
118 123 config_keys.authorized_keys_file_path)
119 124
120 125 ssh_wrapper_cmd = registry.settings.get(
121 126 config_keys.wrapper_cmd)
122 127 allow_shell = registry.settings.get(
123 128 config_keys.wrapper_allow_shell)
124 129 ssh_opts = registry.settings.get(
125 130 config_keys.authorized_keys_line_ssh_opts)
126 131 debug = registry.settings.get(
127 132 config_keys.enable_debug_logging)
128 133
129 134 _generate_ssh_authorized_keys_file(
130 135 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
131 136 debug)
132 137
133 138 return 0
@@ -1,514 +1,514 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import peppercorn
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.exceptions import (
33 33 RepoGroupAssignmentError, UserGroupAssignedException)
34 34 from rhodecode.model.forms import (
35 35 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
36 36 UserPermissionsForm)
37 37 from rhodecode.model.permission import PermissionModel
38 38
39 39 from rhodecode.apps._base import UserGroupAppView
40 40 from rhodecode.lib.auth import (
41 41 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
42 42 from rhodecode.lib import helpers as h, audit_logger
43 43 from rhodecode.lib.utils2 import str2bool, safe_int
44 44 from rhodecode.model.db import User, UserGroup
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class UserGroupsView(UserGroupAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55
56 56 PermissionModel().set_global_permission_choices(
57 57 c, gettext_translator=self.request.translate)
58 58
59 59 return c
60 60
61 61 @LoginRequired()
62 62 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
63 63 def user_group_members(self):
64 64 """
65 65 Return members of given user group
66 66 """
67 67 self.load_default_context()
68 68 user_group = self.db_user_group
69 69 group_members_obj = sorted((x.user for x in user_group.members),
70 70 key=lambda u: u.username.lower())
71 71
72 72 group_members = [
73 73 {
74 74 'id': user.user_id,
75 75 'first_name': user.first_name,
76 76 'last_name': user.last_name,
77 77 'username': user.username,
78 'icon_link': h.gravatar_url(user.email, 30),
78 'icon_link': h.gravatar_url(user.email, 30, request=self.request),
79 79 'value_display': h.person(user.email),
80 80 'value': user.username,
81 81 'value_type': 'user',
82 82 'active': user.active,
83 83 }
84 84 for user in group_members_obj
85 85 ]
86 86
87 87 return {
88 88 'members': group_members
89 89 }
90 90
91 91 @LoginRequired()
92 92 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
93 93 def user_group_perms_summary(self):
94 94 c = self.load_default_context()
95 95 c.user_group = self.db_user_group
96 96 c.active = 'perms_summary'
97 97 c.permissions = UserGroupModel().get_perms_summary(
98 98 c.user_group.users_group_id)
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
103 103 def user_group_perms_summary_json(self):
104 104 self.load_default_context()
105 105 user_group = self.db_user_group
106 106 return UserGroupModel().get_perms_summary(user_group.users_group_id)
107 107
108 108 def _revoke_perms_on_yourself(self, form_result):
109 109 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
110 110 form_result['perm_updates'])
111 111 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
112 112 form_result['perm_additions'])
113 113 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
114 114 form_result['perm_deletions'])
115 115 admin_perm = 'usergroup.admin'
116 116 if _updates and _updates[0][1] != admin_perm or \
117 117 _additions and _additions[0][1] != admin_perm or \
118 118 _deletions and _deletions[0][1] != admin_perm:
119 119 return True
120 120 return False
121 121
122 122 @LoginRequired()
123 123 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
124 124 @CSRFRequired()
125 125 def user_group_update(self):
126 126 _ = self.request.translate
127 127
128 128 user_group = self.db_user_group
129 129 user_group_id = user_group.users_group_id
130 130
131 131 old_user_group_name = self.db_user_group_name
132 132 new_user_group_name = old_user_group_name
133 133
134 134 c = self.load_default_context()
135 135 c.user_group = user_group
136 136 c.group_members_obj = [x.user for x in c.user_group.members]
137 137 c.group_members_obj.sort(key=lambda u: u.username.lower())
138 138 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
139 139 c.active = 'settings'
140 140
141 141 users_group_form = UserGroupForm(
142 142 self.request.translate, edit=True,
143 143 old_data=c.user_group.get_dict(), allow_disabled=True)()
144 144
145 145 old_values = c.user_group.get_api_data()
146 146
147 147 try:
148 148 form_result = users_group_form.to_python(self.request.POST)
149 149 pstruct = peppercorn.parse(self.request.POST.items())
150 150 form_result['users_group_members'] = pstruct['user_group_members']
151 151
152 152 user_group, added_members, removed_members = \
153 153 UserGroupModel().update(c.user_group, form_result)
154 154 new_user_group_name = form_result['users_group_name']
155 155
156 156 for user_id in added_members:
157 157 user = User.get(user_id)
158 158 user_data = user.get_api_data()
159 159 audit_logger.store_web(
160 160 'user_group.edit.member.add',
161 161 action_data={'user': user_data, 'old_data': old_values},
162 162 user=self._rhodecode_user)
163 163
164 164 for user_id in removed_members:
165 165 user = User.get(user_id)
166 166 user_data = user.get_api_data()
167 167 audit_logger.store_web(
168 168 'user_group.edit.member.delete',
169 169 action_data={'user': user_data, 'old_data': old_values},
170 170 user=self._rhodecode_user)
171 171
172 172 audit_logger.store_web(
173 173 'user_group.edit', action_data={'old_data': old_values},
174 174 user=self._rhodecode_user)
175 175
176 176 h.flash(_('Updated user group %s') % new_user_group_name,
177 177 category='success')
178 178
179 179 affected_user_ids = []
180 180 for user_id in added_members + removed_members:
181 181 affected_user_ids.append(user_id)
182 182
183 183 name_changed = old_user_group_name != new_user_group_name
184 184 if name_changed:
185 185 owner = User.get_by_username(form_result['user'])
186 186 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
187 187 affected_user_ids.append(self._rhodecode_user.user_id)
188 188 affected_user_ids.append(owner_id)
189 189
190 190 PermissionModel().trigger_permission_flush(affected_user_ids)
191 191
192 192 Session().commit()
193 193 except formencode.Invalid as errors:
194 194 defaults = errors.value
195 195 e = errors.error_dict or {}
196 196
197 197 data = render(
198 198 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
199 199 self._get_template_context(c), self.request)
200 200 html = formencode.htmlfill.render(
201 201 data,
202 202 defaults=defaults,
203 203 errors=e,
204 204 prefix_error=False,
205 205 encoding="UTF-8",
206 206 force_defaults=False
207 207 )
208 208 return Response(html)
209 209
210 210 except Exception:
211 211 log.exception("Exception during update of user group")
212 212 h.flash(_('Error occurred during update of user group %s')
213 213 % new_user_group_name, category='error')
214 214
215 215 raise HTTPFound(
216 216 h.route_path('edit_user_group', user_group_id=user_group_id))
217 217
218 218 @LoginRequired()
219 219 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
220 220 @CSRFRequired()
221 221 def user_group_delete(self):
222 222 _ = self.request.translate
223 223 user_group = self.db_user_group
224 224
225 225 self.load_default_context()
226 226 force = str2bool(self.request.POST.get('force'))
227 227
228 228 old_values = user_group.get_api_data()
229 229 try:
230 230 UserGroupModel().delete(user_group, force=force)
231 231 audit_logger.store_web(
232 232 'user.delete', action_data={'old_data': old_values},
233 233 user=self._rhodecode_user)
234 234 Session().commit()
235 235 h.flash(_('Successfully deleted user group'), category='success')
236 236 except UserGroupAssignedException as e:
237 237 h.flash(str(e), category='error')
238 238 except Exception:
239 239 log.exception("Exception during deletion of user group")
240 240 h.flash(_('An error occurred during deletion of user group'),
241 241 category='error')
242 242 raise HTTPFound(h.route_path('user_groups'))
243 243
244 244 @LoginRequired()
245 245 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
246 246 def user_group_edit(self):
247 247 user_group = self.db_user_group
248 248
249 249 c = self.load_default_context()
250 250 c.user_group = user_group
251 251 c.group_members_obj = [x.user for x in c.user_group.members]
252 252 c.group_members_obj.sort(key=lambda u: u.username.lower())
253 253 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
254 254
255 255 c.active = 'settings'
256 256
257 257 defaults = user_group.get_dict()
258 258 # fill owner
259 259 if user_group.user:
260 260 defaults.update({'user': user_group.user.username})
261 261 else:
262 262 replacement_user = User.get_first_super_admin().username
263 263 defaults.update({'user': replacement_user})
264 264
265 265 data = render(
266 266 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
267 267 self._get_template_context(c), self.request)
268 268 html = formencode.htmlfill.render(
269 269 data,
270 270 defaults=defaults,
271 271 encoding="UTF-8",
272 272 force_defaults=False
273 273 )
274 274 return Response(html)
275 275
276 276 @LoginRequired()
277 277 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
278 278 def user_group_edit_perms(self):
279 279 user_group = self.db_user_group
280 280 c = self.load_default_context()
281 281 c.user_group = user_group
282 282 c.active = 'perms'
283 283
284 284 defaults = {}
285 285 # fill user group users
286 286 for p in c.user_group.user_user_group_to_perm:
287 287 defaults.update({'u_perm_%s' % p.user.user_id:
288 288 p.permission.permission_name})
289 289
290 290 for p in c.user_group.user_group_user_group_to_perm:
291 291 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
292 292 p.permission.permission_name})
293 293
294 294 data = render(
295 295 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
296 296 self._get_template_context(c), self.request)
297 297 html = formencode.htmlfill.render(
298 298 data,
299 299 defaults=defaults,
300 300 encoding="UTF-8",
301 301 force_defaults=False
302 302 )
303 303 return Response(html)
304 304
305 305 @LoginRequired()
306 306 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
307 307 @CSRFRequired()
308 308 def user_group_update_perms(self):
309 309 """
310 310 grant permission for given user group
311 311 """
312 312 _ = self.request.translate
313 313
314 314 user_group = self.db_user_group
315 315 user_group_id = user_group.users_group_id
316 316 c = self.load_default_context()
317 317 c.user_group = user_group
318 318 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
319 319
320 320 if not self._rhodecode_user.is_admin:
321 321 if self._revoke_perms_on_yourself(form):
322 322 msg = _('Cannot change permission for yourself as admin')
323 323 h.flash(msg, category='warning')
324 324 raise HTTPFound(
325 325 h.route_path('edit_user_group_perms',
326 326 user_group_id=user_group_id))
327 327
328 328 try:
329 329 changes = UserGroupModel().update_permissions(
330 330 user_group,
331 331 form['perm_additions'], form['perm_updates'],
332 332 form['perm_deletions'])
333 333
334 334 except RepoGroupAssignmentError:
335 335 h.flash(_('Target group cannot be the same'), category='error')
336 336 raise HTTPFound(
337 337 h.route_path('edit_user_group_perms',
338 338 user_group_id=user_group_id))
339 339
340 340 action_data = {
341 341 'added': changes['added'],
342 342 'updated': changes['updated'],
343 343 'deleted': changes['deleted'],
344 344 }
345 345 audit_logger.store_web(
346 346 'user_group.edit.permissions', action_data=action_data,
347 347 user=self._rhodecode_user)
348 348
349 349 Session().commit()
350 350 h.flash(_('User Group permissions updated'), category='success')
351 351
352 352 affected_user_ids = []
353 353 for change in changes['added'] + changes['updated'] + changes['deleted']:
354 354 if change['type'] == 'user':
355 355 affected_user_ids.append(change['id'])
356 356 if change['type'] == 'user_group':
357 357 user_group = UserGroup.get(safe_int(change['id']))
358 358 if user_group:
359 359 group_members_ids = [x.user_id for x in user_group.members]
360 360 affected_user_ids.extend(group_members_ids)
361 361
362 362 PermissionModel().trigger_permission_flush(affected_user_ids)
363 363
364 364 raise HTTPFound(
365 365 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
366 366
367 367 @LoginRequired()
368 368 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
369 369 def user_group_global_perms_edit(self):
370 370 user_group = self.db_user_group
371 371 c = self.load_default_context()
372 372 c.user_group = user_group
373 373 c.active = 'global_perms'
374 374
375 375 c.default_user = User.get_default_user()
376 376 defaults = c.user_group.get_dict()
377 377 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
378 378 defaults.update(c.user_group.get_default_perms())
379 379
380 380 data = render(
381 381 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
382 382 self._get_template_context(c), self.request)
383 383 html = formencode.htmlfill.render(
384 384 data,
385 385 defaults=defaults,
386 386 encoding="UTF-8",
387 387 force_defaults=False
388 388 )
389 389 return Response(html)
390 390
391 391 @LoginRequired()
392 392 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
393 393 @CSRFRequired()
394 394 def user_group_global_perms_update(self):
395 395 _ = self.request.translate
396 396 user_group = self.db_user_group
397 397 user_group_id = self.db_user_group.users_group_id
398 398
399 399 c = self.load_default_context()
400 400 c.user_group = user_group
401 401 c.active = 'global_perms'
402 402
403 403 try:
404 404 # first stage that verifies the checkbox
405 405 _form = UserIndividualPermissionsForm(self.request.translate)
406 406 form_result = _form.to_python(dict(self.request.POST))
407 407 inherit_perms = form_result['inherit_default_permissions']
408 408 user_group.inherit_default_permissions = inherit_perms
409 409 Session().add(user_group)
410 410
411 411 if not inherit_perms:
412 412 # only update the individual ones if we un check the flag
413 413 _form = UserPermissionsForm(
414 414 self.request.translate,
415 415 [x[0] for x in c.repo_create_choices],
416 416 [x[0] for x in c.repo_create_on_write_choices],
417 417 [x[0] for x in c.repo_group_create_choices],
418 418 [x[0] for x in c.user_group_create_choices],
419 419 [x[0] for x in c.fork_choices],
420 420 [x[0] for x in c.inherit_default_permission_choices])()
421 421
422 422 form_result = _form.to_python(dict(self.request.POST))
423 423 form_result.update(
424 424 {'perm_user_group_id': user_group.users_group_id})
425 425
426 426 PermissionModel().update_user_group_permissions(form_result)
427 427
428 428 Session().commit()
429 429 h.flash(_('User Group global permissions updated successfully'),
430 430 category='success')
431 431
432 432 except formencode.Invalid as errors:
433 433 defaults = errors.value
434 434
435 435 data = render(
436 436 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
437 437 self._get_template_context(c), self.request)
438 438 html = formencode.htmlfill.render(
439 439 data,
440 440 defaults=defaults,
441 441 errors=errors.error_dict or {},
442 442 prefix_error=False,
443 443 encoding="UTF-8",
444 444 force_defaults=False
445 445 )
446 446 return Response(html)
447 447 except Exception:
448 448 log.exception("Exception during permissions saving")
449 449 h.flash(_('An error occurred during permissions saving'),
450 450 category='error')
451 451
452 452 raise HTTPFound(
453 453 h.route_path('edit_user_group_global_perms',
454 454 user_group_id=user_group_id))
455 455
456 456 @LoginRequired()
457 457 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
458 458 def user_group_edit_advanced(self):
459 459 user_group = self.db_user_group
460 460
461 461 c = self.load_default_context()
462 462 c.user_group = user_group
463 463 c.active = 'advanced'
464 464 c.group_members_obj = sorted(
465 465 (x.user for x in c.user_group.members),
466 466 key=lambda u: u.username.lower())
467 467
468 468 c.group_to_repos = sorted(
469 469 (x.repository for x in c.user_group.users_group_repo_to_perm),
470 470 key=lambda u: u.repo_name.lower())
471 471
472 472 c.group_to_repo_groups = sorted(
473 473 (x.group for x in c.user_group.users_group_repo_group_to_perm),
474 474 key=lambda u: u.group_name.lower())
475 475
476 476 c.group_to_review_rules = sorted(
477 477 (x.users_group for x in c.user_group.user_group_review_rules),
478 478 key=lambda u: u.users_group_name.lower())
479 479
480 480 return self._get_template_context(c)
481 481
482 482 @LoginRequired()
483 483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 484 @CSRFRequired()
485 485 def user_group_edit_advanced_set_synchronization(self):
486 486 _ = self.request.translate
487 487 user_group = self.db_user_group
488 488 user_group_id = user_group.users_group_id
489 489
490 490 existing = user_group.group_data.get('extern_type')
491 491
492 492 if existing:
493 493 new_state = user_group.group_data
494 494 new_state['extern_type'] = None
495 495 else:
496 496 new_state = user_group.group_data
497 497 new_state['extern_type'] = 'manual'
498 498 new_state['extern_type_set_by'] = self._rhodecode_user.username
499 499
500 500 try:
501 501 user_group.group_data = new_state
502 502 Session().add(user_group)
503 503 Session().commit()
504 504
505 505 h.flash(_('User Group synchronization updated successfully'),
506 506 category='success')
507 507 except Exception:
508 508 log.exception("Exception during sync settings saving")
509 509 h.flash(_('An error occurred during synchronization update'),
510 510 category='error')
511 511
512 512 raise HTTPFound(
513 513 h.route_path('edit_user_group_advanced',
514 514 user_group_id=user_group_id))
General Comments 0
You need to be logged in to leave comments. Login now