##// END OF EJS Templates
caches: allow cache disable for file tree
ergo -
r3469:41f317da default
parent child Browse files
Show More
@@ -1,694 +1,701 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 from pyramid import compat
25 from pyramid import compat
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27
27
28 from rhodecode.lib import helpers as h, diffs
28 from rhodecode.lib import helpers as h, diffs
29 from rhodecode.lib.utils2 import (
29 from rhodecode.lib.utils2 import (
30 StrictAttributeDict, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
32 from rhodecode.model import repo
32 from rhodecode.model import repo
33 from rhodecode.model import repo_group
33 from rhodecode.model import repo_group
34 from rhodecode.model import user_group
34 from rhodecode.model import user_group
35 from rhodecode.model import user
35 from rhodecode.model import user
36 from rhodecode.model.db import User
36 from rhodecode.model.db import User
37 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.settings import VcsSettingsModel
38 from rhodecode.model.settings import VcsSettingsModel
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 ADMIN_PREFIX = '/_admin'
43 ADMIN_PREFIX = '/_admin'
44 STATIC_FILE_PREFIX = '/_static'
44 STATIC_FILE_PREFIX = '/_static'
45
45
46 URL_NAME_REQUIREMENTS = {
46 URL_NAME_REQUIREMENTS = {
47 # group name can have a slash in them, but they must not end with a slash
47 # group name can have a slash in them, but they must not end with a slash
48 'group_name': r'.*?[^/]',
48 'group_name': r'.*?[^/]',
49 'repo_group_name': r'.*?[^/]',
49 'repo_group_name': r'.*?[^/]',
50 # repo names can have a slash in them, but they must not end with a slash
50 # repo names can have a slash in them, but they must not end with a slash
51 'repo_name': r'.*?[^/]',
51 'repo_name': r'.*?[^/]',
52 # file path eats up everything at the end
52 # file path eats up everything at the end
53 'f_path': r'.*',
53 'f_path': r'.*',
54 # reference types
54 # reference types
55 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
55 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
56 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
56 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
57 }
57 }
58
58
59
59
60 def add_route_with_slash(config,name, pattern, **kw):
60 def add_route_with_slash(config,name, pattern, **kw):
61 config.add_route(name, pattern, **kw)
61 config.add_route(name, pattern, **kw)
62 if not pattern.endswith('/'):
62 if not pattern.endswith('/'):
63 config.add_route(name + '_slash', pattern + '/', **kw)
63 config.add_route(name + '_slash', pattern + '/', **kw)
64
64
65
65
66 def add_route_requirements(route_path, requirements=None):
66 def add_route_requirements(route_path, requirements=None):
67 """
67 """
68 Adds regex requirements to pyramid routes using a mapping dict
68 Adds regex requirements to pyramid routes using a mapping dict
69 e.g::
69 e.g::
70 add_route_requirements('{repo_name}/settings')
70 add_route_requirements('{repo_name}/settings')
71 """
71 """
72 requirements = requirements or URL_NAME_REQUIREMENTS
72 requirements = requirements or URL_NAME_REQUIREMENTS
73 for key, regex in requirements.items():
73 for key, regex in requirements.items():
74 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
74 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
75 return route_path
75 return route_path
76
76
77
77
78 def get_format_ref_id(repo):
78 def get_format_ref_id(repo):
79 """Returns a `repo` specific reference formatter function"""
79 """Returns a `repo` specific reference formatter function"""
80 if h.is_svn(repo):
80 if h.is_svn(repo):
81 return _format_ref_id_svn
81 return _format_ref_id_svn
82 else:
82 else:
83 return _format_ref_id
83 return _format_ref_id
84
84
85
85
86 def _format_ref_id(name, raw_id):
86 def _format_ref_id(name, raw_id):
87 """Default formatting of a given reference `name`"""
87 """Default formatting of a given reference `name`"""
88 return name
88 return name
89
89
90
90
91 def _format_ref_id_svn(name, raw_id):
91 def _format_ref_id_svn(name, raw_id):
92 """Special way of formatting a reference for Subversion including path"""
92 """Special way of formatting a reference for Subversion including path"""
93 return '%s@%s' % (name, raw_id)
93 return '%s@%s' % (name, raw_id)
94
94
95
95
96 class TemplateArgs(StrictAttributeDict):
96 class TemplateArgs(StrictAttributeDict):
97 pass
97 pass
98
98
99
99
100 class BaseAppView(object):
100 class BaseAppView(object):
101
101
102 def __init__(self, context, request):
102 def __init__(self, context, request):
103 self.request = request
103 self.request = request
104 self.context = context
104 self.context = context
105 self.session = request.session
105 self.session = request.session
106 if not hasattr(request, 'user'):
106 if not hasattr(request, 'user'):
107 # NOTE(marcink): edge case, we ended up in matched route
107 # NOTE(marcink): edge case, we ended up in matched route
108 # but probably of web-app context, e.g API CALL/VCS CALL
108 # but probably of web-app context, e.g API CALL/VCS CALL
109 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
109 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
110 log.warning('Unable to process request `%s` in this scope', request)
110 log.warning('Unable to process request `%s` in this scope', request)
111 raise HTTPBadRequest()
111 raise HTTPBadRequest()
112
112
113 self._rhodecode_user = request.user # auth user
113 self._rhodecode_user = request.user # auth user
114 self._rhodecode_db_user = self._rhodecode_user.get_instance()
114 self._rhodecode_db_user = self._rhodecode_user.get_instance()
115 self._maybe_needs_password_change(
115 self._maybe_needs_password_change(
116 request.matched_route.name, self._rhodecode_db_user)
116 request.matched_route.name, self._rhodecode_db_user)
117
117
118 def _maybe_needs_password_change(self, view_name, user_obj):
118 def _maybe_needs_password_change(self, view_name, user_obj):
119 log.debug('Checking if user %s needs password change on view %s',
119 log.debug('Checking if user %s needs password change on view %s',
120 user_obj, view_name)
120 user_obj, view_name)
121 skip_user_views = [
121 skip_user_views = [
122 'logout', 'login',
122 'logout', 'login',
123 'my_account_password', 'my_account_password_update'
123 'my_account_password', 'my_account_password_update'
124 ]
124 ]
125
125
126 if not user_obj:
126 if not user_obj:
127 return
127 return
128
128
129 if user_obj.username == User.DEFAULT_USER:
129 if user_obj.username == User.DEFAULT_USER:
130 return
130 return
131
131
132 now = time.time()
132 now = time.time()
133 should_change = user_obj.user_data.get('force_password_change')
133 should_change = user_obj.user_data.get('force_password_change')
134 change_after = safe_int(should_change) or 0
134 change_after = safe_int(should_change) or 0
135 if should_change and now > change_after:
135 if should_change and now > change_after:
136 log.debug('User %s requires password change', user_obj)
136 log.debug('User %s requires password change', user_obj)
137 h.flash('You are required to change your password', 'warning',
137 h.flash('You are required to change your password', 'warning',
138 ignore_duplicate=True)
138 ignore_duplicate=True)
139
139
140 if view_name not in skip_user_views:
140 if view_name not in skip_user_views:
141 raise HTTPFound(
141 raise HTTPFound(
142 self.request.route_path('my_account_password'))
142 self.request.route_path('my_account_password'))
143
143
144 def _log_creation_exception(self, e, repo_name):
144 def _log_creation_exception(self, e, repo_name):
145 _ = self.request.translate
145 _ = self.request.translate
146 reason = None
146 reason = None
147 if len(e.args) == 2:
147 if len(e.args) == 2:
148 reason = e.args[1]
148 reason = e.args[1]
149
149
150 if reason == 'INVALID_CERTIFICATE':
150 if reason == 'INVALID_CERTIFICATE':
151 log.exception(
151 log.exception(
152 'Exception creating a repository: invalid certificate')
152 'Exception creating a repository: invalid certificate')
153 msg = (_('Error creating repository %s: invalid certificate')
153 msg = (_('Error creating repository %s: invalid certificate')
154 % repo_name)
154 % repo_name)
155 else:
155 else:
156 log.exception("Exception creating a repository")
156 log.exception("Exception creating a repository")
157 msg = (_('Error creating repository %s')
157 msg = (_('Error creating repository %s')
158 % repo_name)
158 % repo_name)
159 return msg
159 return msg
160
160
161 def _get_local_tmpl_context(self, include_app_defaults=True):
161 def _get_local_tmpl_context(self, include_app_defaults=True):
162 c = TemplateArgs()
162 c = TemplateArgs()
163 c.auth_user = self.request.user
163 c.auth_user = self.request.user
164 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
164 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
165 c.rhodecode_user = self.request.user
165 c.rhodecode_user = self.request.user
166
166
167 if include_app_defaults:
167 if include_app_defaults:
168 from rhodecode.lib.base import attach_context_attributes
168 from rhodecode.lib.base import attach_context_attributes
169 attach_context_attributes(c, self.request, self.request.user.user_id)
169 attach_context_attributes(c, self.request, self.request.user.user_id)
170
170
171 return c
171 return c
172
172
173 def _get_template_context(self, tmpl_args, **kwargs):
173 def _get_template_context(self, tmpl_args, **kwargs):
174
174
175 local_tmpl_args = {
175 local_tmpl_args = {
176 'defaults': {},
176 'defaults': {},
177 'errors': {},
177 'errors': {},
178 'c': tmpl_args
178 'c': tmpl_args
179 }
179 }
180 local_tmpl_args.update(kwargs)
180 local_tmpl_args.update(kwargs)
181 return local_tmpl_args
181 return local_tmpl_args
182
182
183 def load_default_context(self):
183 def load_default_context(self):
184 """
184 """
185 example:
185 example:
186
186
187 def load_default_context(self):
187 def load_default_context(self):
188 c = self._get_local_tmpl_context()
188 c = self._get_local_tmpl_context()
189 c.custom_var = 'foobar'
189 c.custom_var = 'foobar'
190
190
191 return c
191 return c
192 """
192 """
193 raise NotImplementedError('Needs implementation in view class')
193 raise NotImplementedError('Needs implementation in view class')
194
194
195
195
196 class RepoAppView(BaseAppView):
196 class RepoAppView(BaseAppView):
197
197
198 def __init__(self, context, request):
198 def __init__(self, context, request):
199 super(RepoAppView, self).__init__(context, request)
199 super(RepoAppView, self).__init__(context, request)
200 self.db_repo = request.db_repo
200 self.db_repo = request.db_repo
201 self.db_repo_name = self.db_repo.repo_name
201 self.db_repo_name = self.db_repo.repo_name
202 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
202 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
203
203
204 def _handle_missing_requirements(self, error):
204 def _handle_missing_requirements(self, error):
205 log.error(
205 log.error(
206 'Requirements are missing for repository %s: %s',
206 'Requirements are missing for repository %s: %s',
207 self.db_repo_name, safe_unicode(error))
207 self.db_repo_name, safe_unicode(error))
208
208
209 def _get_local_tmpl_context(self, include_app_defaults=True):
209 def _get_local_tmpl_context(self, include_app_defaults=True):
210 _ = self.request.translate
210 _ = self.request.translate
211 c = super(RepoAppView, self)._get_local_tmpl_context(
211 c = super(RepoAppView, self)._get_local_tmpl_context(
212 include_app_defaults=include_app_defaults)
212 include_app_defaults=include_app_defaults)
213
213
214 # register common vars for this type of view
214 # register common vars for this type of view
215 c.rhodecode_db_repo = self.db_repo
215 c.rhodecode_db_repo = self.db_repo
216 c.repo_name = self.db_repo_name
216 c.repo_name = self.db_repo_name
217 c.repository_pull_requests = self.db_repo_pull_requests
217 c.repository_pull_requests = self.db_repo_pull_requests
218 self.path_filter = PathFilter(None)
218 self.path_filter = PathFilter(None)
219
219
220 c.repository_requirements_missing = {}
220 c.repository_requirements_missing = {}
221 try:
221 try:
222 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
222 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
223 if self.rhodecode_vcs_repo:
223 if self.rhodecode_vcs_repo:
224 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
224 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
225 c.auth_user.username)
225 c.auth_user.username)
226 self.path_filter = PathFilter(path_perms)
226 self.path_filter = PathFilter(path_perms)
227 except RepositoryRequirementError as e:
227 except RepositoryRequirementError as e:
228 c.repository_requirements_missing = {'error': str(e)}
228 c.repository_requirements_missing = {'error': str(e)}
229 self._handle_missing_requirements(e)
229 self._handle_missing_requirements(e)
230 self.rhodecode_vcs_repo = None
230 self.rhodecode_vcs_repo = None
231
231
232 c.path_filter = self.path_filter # used by atom_feed_entry.mako
232 c.path_filter = self.path_filter # used by atom_feed_entry.mako
233
233
234 if self.rhodecode_vcs_repo is None:
234 if self.rhodecode_vcs_repo is None:
235 # unable to fetch this repo as vcs instance, report back to user
235 # unable to fetch this repo as vcs instance, report back to user
236 h.flash(_(
236 h.flash(_(
237 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
237 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
238 "Please check if it exist, or is not damaged.") %
238 "Please check if it exist, or is not damaged.") %
239 {'repo_name': c.repo_name},
239 {'repo_name': c.repo_name},
240 category='error', ignore_duplicate=True)
240 category='error', ignore_duplicate=True)
241 if c.repository_requirements_missing:
241 if c.repository_requirements_missing:
242 route = self.request.matched_route.name
242 route = self.request.matched_route.name
243 if route.startswith(('edit_repo', 'repo_summary')):
243 if route.startswith(('edit_repo', 'repo_summary')):
244 # allow summary and edit repo on missing requirements
244 # allow summary and edit repo on missing requirements
245 return c
245 return c
246
246
247 raise HTTPFound(
247 raise HTTPFound(
248 h.route_path('repo_summary', repo_name=self.db_repo_name))
248 h.route_path('repo_summary', repo_name=self.db_repo_name))
249
249
250 else: # redirect if we don't show missing requirements
250 else: # redirect if we don't show missing requirements
251 raise HTTPFound(h.route_path('home'))
251 raise HTTPFound(h.route_path('home'))
252
252
253 c.has_origin_repo_read_perm = False
253 c.has_origin_repo_read_perm = False
254 if self.db_repo.fork:
254 if self.db_repo.fork:
255 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
255 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
256 'repository.write', 'repository.read', 'repository.admin')(
256 'repository.write', 'repository.read', 'repository.admin')(
257 self.db_repo.fork.repo_name, 'summary fork link')
257 self.db_repo.fork.repo_name, 'summary fork link')
258
258
259 return c
259 return c
260
260
261 def _get_f_path_unchecked(self, matchdict, default=None):
261 def _get_f_path_unchecked(self, matchdict, default=None):
262 """
262 """
263 Should only be used by redirects, everything else should call _get_f_path
263 Should only be used by redirects, everything else should call _get_f_path
264 """
264 """
265 f_path = matchdict.get('f_path')
265 f_path = matchdict.get('f_path')
266 if f_path:
266 if f_path:
267 # fix for multiple initial slashes that causes errors for GIT
267 # fix for multiple initial slashes that causes errors for GIT
268 return f_path.lstrip('/')
268 return f_path.lstrip('/')
269
269
270 return default
270 return default
271
271
272 def _get_f_path(self, matchdict, default=None):
272 def _get_f_path(self, matchdict, default=None):
273 f_path_match = self._get_f_path_unchecked(matchdict, default)
273 f_path_match = self._get_f_path_unchecked(matchdict, default)
274 return self.path_filter.assert_path_permissions(f_path_match)
274 return self.path_filter.assert_path_permissions(f_path_match)
275
275
276 def _get_general_setting(self, target_repo, settings_key, default=False):
276 def _get_general_setting(self, target_repo, settings_key, default=False):
277 settings_model = VcsSettingsModel(repo=target_repo)
277 settings_model = VcsSettingsModel(repo=target_repo)
278 settings = settings_model.get_general_settings()
278 settings = settings_model.get_general_settings()
279 return settings.get(settings_key, default)
279 return settings.get(settings_key, default)
280
280
281 def get_recache_flag(self):
282 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
283 flag_val = self.request.GET.get(flag_name)
284 if str2bool(flag_val):
285 return True
286 return False
287
281
288
282 class PathFilter(object):
289 class PathFilter(object):
283
290
284 # Expects and instance of BasePathPermissionChecker or None
291 # Expects and instance of BasePathPermissionChecker or None
285 def __init__(self, permission_checker):
292 def __init__(self, permission_checker):
286 self.permission_checker = permission_checker
293 self.permission_checker = permission_checker
287
294
288 def assert_path_permissions(self, path):
295 def assert_path_permissions(self, path):
289 if path and self.permission_checker and not self.permission_checker.has_access(path):
296 if path and self.permission_checker and not self.permission_checker.has_access(path):
290 raise HTTPForbidden()
297 raise HTTPForbidden()
291 return path
298 return path
292
299
293 def filter_patchset(self, patchset):
300 def filter_patchset(self, patchset):
294 if not self.permission_checker or not patchset:
301 if not self.permission_checker or not patchset:
295 return patchset, False
302 return patchset, False
296 had_filtered = False
303 had_filtered = False
297 filtered_patchset = []
304 filtered_patchset = []
298 for patch in patchset:
305 for patch in patchset:
299 filename = patch.get('filename', None)
306 filename = patch.get('filename', None)
300 if not filename or self.permission_checker.has_access(filename):
307 if not filename or self.permission_checker.has_access(filename):
301 filtered_patchset.append(patch)
308 filtered_patchset.append(patch)
302 else:
309 else:
303 had_filtered = True
310 had_filtered = True
304 if had_filtered:
311 if had_filtered:
305 if isinstance(patchset, diffs.LimitedDiffContainer):
312 if isinstance(patchset, diffs.LimitedDiffContainer):
306 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
313 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
307 return filtered_patchset, True
314 return filtered_patchset, True
308 else:
315 else:
309 return patchset, False
316 return patchset, False
310
317
311 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
318 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
312 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
319 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
313 result = diffset.render_patchset(
320 result = diffset.render_patchset(
314 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
321 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
315 result.has_hidden_changes = has_hidden_changes
322 result.has_hidden_changes = has_hidden_changes
316 return result
323 return result
317
324
318 def get_raw_patch(self, diff_processor):
325 def get_raw_patch(self, diff_processor):
319 if self.permission_checker is None:
326 if self.permission_checker is None:
320 return diff_processor.as_raw()
327 return diff_processor.as_raw()
321 elif self.permission_checker.has_full_access:
328 elif self.permission_checker.has_full_access:
322 return diff_processor.as_raw()
329 return diff_processor.as_raw()
323 else:
330 else:
324 return '# Repository has user-specific filters, raw patch generation is disabled.'
331 return '# Repository has user-specific filters, raw patch generation is disabled.'
325
332
326 @property
333 @property
327 def is_enabled(self):
334 def is_enabled(self):
328 return self.permission_checker is not None
335 return self.permission_checker is not None
329
336
330
337
331 class RepoGroupAppView(BaseAppView):
338 class RepoGroupAppView(BaseAppView):
332 def __init__(self, context, request):
339 def __init__(self, context, request):
333 super(RepoGroupAppView, self).__init__(context, request)
340 super(RepoGroupAppView, self).__init__(context, request)
334 self.db_repo_group = request.db_repo_group
341 self.db_repo_group = request.db_repo_group
335 self.db_repo_group_name = self.db_repo_group.group_name
342 self.db_repo_group_name = self.db_repo_group.group_name
336
343
337 def _get_local_tmpl_context(self, include_app_defaults=True):
344 def _get_local_tmpl_context(self, include_app_defaults=True):
338 _ = self.request.translate
345 _ = self.request.translate
339 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
346 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
340 include_app_defaults=include_app_defaults)
347 include_app_defaults=include_app_defaults)
341 c.repo_group = self.db_repo_group
348 c.repo_group = self.db_repo_group
342 return c
349 return c
343
350
344 def _revoke_perms_on_yourself(self, form_result):
351 def _revoke_perms_on_yourself(self, form_result):
345 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
352 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
346 form_result['perm_updates'])
353 form_result['perm_updates'])
347 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
354 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
348 form_result['perm_additions'])
355 form_result['perm_additions'])
349 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
356 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
350 form_result['perm_deletions'])
357 form_result['perm_deletions'])
351 admin_perm = 'group.admin'
358 admin_perm = 'group.admin'
352 if _updates and _updates[0][1] != admin_perm or \
359 if _updates and _updates[0][1] != admin_perm or \
353 _additions and _additions[0][1] != admin_perm or \
360 _additions and _additions[0][1] != admin_perm or \
354 _deletions and _deletions[0][1] != admin_perm:
361 _deletions and _deletions[0][1] != admin_perm:
355 return True
362 return True
356 return False
363 return False
357
364
358
365
359 class UserGroupAppView(BaseAppView):
366 class UserGroupAppView(BaseAppView):
360 def __init__(self, context, request):
367 def __init__(self, context, request):
361 super(UserGroupAppView, self).__init__(context, request)
368 super(UserGroupAppView, self).__init__(context, request)
362 self.db_user_group = request.db_user_group
369 self.db_user_group = request.db_user_group
363 self.db_user_group_name = self.db_user_group.users_group_name
370 self.db_user_group_name = self.db_user_group.users_group_name
364
371
365
372
366 class UserAppView(BaseAppView):
373 class UserAppView(BaseAppView):
367 def __init__(self, context, request):
374 def __init__(self, context, request):
368 super(UserAppView, self).__init__(context, request)
375 super(UserAppView, self).__init__(context, request)
369 self.db_user = request.db_user
376 self.db_user = request.db_user
370 self.db_user_id = self.db_user.user_id
377 self.db_user_id = self.db_user.user_id
371
378
372 _ = self.request.translate
379 _ = self.request.translate
373 if not request.db_user_supports_default:
380 if not request.db_user_supports_default:
374 if self.db_user.username == User.DEFAULT_USER:
381 if self.db_user.username == User.DEFAULT_USER:
375 h.flash(_("Editing user `{}` is disabled.".format(
382 h.flash(_("Editing user `{}` is disabled.".format(
376 User.DEFAULT_USER)), category='warning')
383 User.DEFAULT_USER)), category='warning')
377 raise HTTPFound(h.route_path('users'))
384 raise HTTPFound(h.route_path('users'))
378
385
379
386
380 class DataGridAppView(object):
387 class DataGridAppView(object):
381 """
388 """
382 Common class to have re-usable grid rendering components
389 Common class to have re-usable grid rendering components
383 """
390 """
384
391
385 def _extract_ordering(self, request, column_map=None):
392 def _extract_ordering(self, request, column_map=None):
386 column_map = column_map or {}
393 column_map = column_map or {}
387 column_index = safe_int(request.GET.get('order[0][column]'))
394 column_index = safe_int(request.GET.get('order[0][column]'))
388 order_dir = request.GET.get(
395 order_dir = request.GET.get(
389 'order[0][dir]', 'desc')
396 'order[0][dir]', 'desc')
390 order_by = request.GET.get(
397 order_by = request.GET.get(
391 'columns[%s][data][sort]' % column_index, 'name_raw')
398 'columns[%s][data][sort]' % column_index, 'name_raw')
392
399
393 # translate datatable to DB columns
400 # translate datatable to DB columns
394 order_by = column_map.get(order_by) or order_by
401 order_by = column_map.get(order_by) or order_by
395
402
396 search_q = request.GET.get('search[value]')
403 search_q = request.GET.get('search[value]')
397 return search_q, order_by, order_dir
404 return search_q, order_by, order_dir
398
405
399 def _extract_chunk(self, request):
406 def _extract_chunk(self, request):
400 start = safe_int(request.GET.get('start'), 0)
407 start = safe_int(request.GET.get('start'), 0)
401 length = safe_int(request.GET.get('length'), 25)
408 length = safe_int(request.GET.get('length'), 25)
402 draw = safe_int(request.GET.get('draw'))
409 draw = safe_int(request.GET.get('draw'))
403 return draw, start, length
410 return draw, start, length
404
411
405 def _get_order_col(self, order_by, model):
412 def _get_order_col(self, order_by, model):
406 if isinstance(order_by, compat.string_types):
413 if isinstance(order_by, compat.string_types):
407 try:
414 try:
408 return operator.attrgetter(order_by)(model)
415 return operator.attrgetter(order_by)(model)
409 except AttributeError:
416 except AttributeError:
410 return None
417 return None
411 else:
418 else:
412 return order_by
419 return order_by
413
420
414
421
415 class BaseReferencesView(RepoAppView):
422 class BaseReferencesView(RepoAppView):
416 """
423 """
417 Base for reference view for branches, tags and bookmarks.
424 Base for reference view for branches, tags and bookmarks.
418 """
425 """
419 def load_default_context(self):
426 def load_default_context(self):
420 c = self._get_local_tmpl_context()
427 c = self._get_local_tmpl_context()
421
428
422
429
423 return c
430 return c
424
431
425 def load_refs_context(self, ref_items, partials_template):
432 def load_refs_context(self, ref_items, partials_template):
426 _render = self.request.get_partial_renderer(partials_template)
433 _render = self.request.get_partial_renderer(partials_template)
427 pre_load = ["author", "date", "message"]
434 pre_load = ["author", "date", "message"]
428
435
429 is_svn = h.is_svn(self.rhodecode_vcs_repo)
436 is_svn = h.is_svn(self.rhodecode_vcs_repo)
430 is_hg = h.is_hg(self.rhodecode_vcs_repo)
437 is_hg = h.is_hg(self.rhodecode_vcs_repo)
431
438
432 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
439 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
433
440
434 closed_refs = {}
441 closed_refs = {}
435 if is_hg:
442 if is_hg:
436 closed_refs = self.rhodecode_vcs_repo.branches_closed
443 closed_refs = self.rhodecode_vcs_repo.branches_closed
437
444
438 data = []
445 data = []
439 for ref_name, commit_id in ref_items:
446 for ref_name, commit_id in ref_items:
440 commit = self.rhodecode_vcs_repo.get_commit(
447 commit = self.rhodecode_vcs_repo.get_commit(
441 commit_id=commit_id, pre_load=pre_load)
448 commit_id=commit_id, pre_load=pre_load)
442 closed = ref_name in closed_refs
449 closed = ref_name in closed_refs
443
450
444 # TODO: johbo: Unify generation of reference links
451 # TODO: johbo: Unify generation of reference links
445 use_commit_id = '/' in ref_name or is_svn
452 use_commit_id = '/' in ref_name or is_svn
446
453
447 if use_commit_id:
454 if use_commit_id:
448 files_url = h.route_path(
455 files_url = h.route_path(
449 'repo_files',
456 'repo_files',
450 repo_name=self.db_repo_name,
457 repo_name=self.db_repo_name,
451 f_path=ref_name if is_svn else '',
458 f_path=ref_name if is_svn else '',
452 commit_id=commit_id)
459 commit_id=commit_id)
453
460
454 else:
461 else:
455 files_url = h.route_path(
462 files_url = h.route_path(
456 'repo_files',
463 'repo_files',
457 repo_name=self.db_repo_name,
464 repo_name=self.db_repo_name,
458 f_path=ref_name if is_svn else '',
465 f_path=ref_name if is_svn else '',
459 commit_id=ref_name,
466 commit_id=ref_name,
460 _query=dict(at=ref_name))
467 _query=dict(at=ref_name))
461
468
462 data.append({
469 data.append({
463 "name": _render('name', ref_name, files_url, closed),
470 "name": _render('name', ref_name, files_url, closed),
464 "name_raw": ref_name,
471 "name_raw": ref_name,
465 "date": _render('date', commit.date),
472 "date": _render('date', commit.date),
466 "date_raw": datetime_to_time(commit.date),
473 "date_raw": datetime_to_time(commit.date),
467 "author": _render('author', commit.author),
474 "author": _render('author', commit.author),
468 "commit": _render(
475 "commit": _render(
469 'commit', commit.message, commit.raw_id, commit.idx),
476 'commit', commit.message, commit.raw_id, commit.idx),
470 "commit_raw": commit.idx,
477 "commit_raw": commit.idx,
471 "compare": _render(
478 "compare": _render(
472 'compare', format_ref_id(ref_name, commit.raw_id)),
479 'compare', format_ref_id(ref_name, commit.raw_id)),
473 })
480 })
474
481
475 return data
482 return data
476
483
477
484
478 class RepoRoutePredicate(object):
485 class RepoRoutePredicate(object):
479 def __init__(self, val, config):
486 def __init__(self, val, config):
480 self.val = val
487 self.val = val
481
488
482 def text(self):
489 def text(self):
483 return 'repo_route = %s' % self.val
490 return 'repo_route = %s' % self.val
484
491
485 phash = text
492 phash = text
486
493
487 def __call__(self, info, request):
494 def __call__(self, info, request):
488 if hasattr(request, 'vcs_call'):
495 if hasattr(request, 'vcs_call'):
489 # skip vcs calls
496 # skip vcs calls
490 return
497 return
491
498
492 repo_name = info['match']['repo_name']
499 repo_name = info['match']['repo_name']
493 repo_model = repo.RepoModel()
500 repo_model = repo.RepoModel()
494
501
495 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
502 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
496
503
497 def redirect_if_creating(route_info, db_repo):
504 def redirect_if_creating(route_info, db_repo):
498 skip_views = ['edit_repo_advanced_delete']
505 skip_views = ['edit_repo_advanced_delete']
499 route = route_info['route']
506 route = route_info['route']
500 # we should skip delete view so we can actually "remove" repositories
507 # we should skip delete view so we can actually "remove" repositories
501 # if they get stuck in creating state.
508 # if they get stuck in creating state.
502 if route.name in skip_views:
509 if route.name in skip_views:
503 return
510 return
504
511
505 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
512 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
506 repo_creating_url = request.route_path(
513 repo_creating_url = request.route_path(
507 'repo_creating', repo_name=db_repo.repo_name)
514 'repo_creating', repo_name=db_repo.repo_name)
508 raise HTTPFound(repo_creating_url)
515 raise HTTPFound(repo_creating_url)
509
516
510 if by_name_match:
517 if by_name_match:
511 # register this as request object we can re-use later
518 # register this as request object we can re-use later
512 request.db_repo = by_name_match
519 request.db_repo = by_name_match
513 redirect_if_creating(info, by_name_match)
520 redirect_if_creating(info, by_name_match)
514 return True
521 return True
515
522
516 by_id_match = repo_model.get_repo_by_id(repo_name)
523 by_id_match = repo_model.get_repo_by_id(repo_name)
517 if by_id_match:
524 if by_id_match:
518 request.db_repo = by_id_match
525 request.db_repo = by_id_match
519 redirect_if_creating(info, by_id_match)
526 redirect_if_creating(info, by_id_match)
520 return True
527 return True
521
528
522 return False
529 return False
523
530
524
531
525 class RepoForbidArchivedRoutePredicate(object):
532 class RepoForbidArchivedRoutePredicate(object):
526 def __init__(self, val, config):
533 def __init__(self, val, config):
527 self.val = val
534 self.val = val
528
535
529 def text(self):
536 def text(self):
530 return 'repo_forbid_archived = %s' % self.val
537 return 'repo_forbid_archived = %s' % self.val
531
538
532 phash = text
539 phash = text
533
540
534 def __call__(self, info, request):
541 def __call__(self, info, request):
535 _ = request.translate
542 _ = request.translate
536 rhodecode_db_repo = request.db_repo
543 rhodecode_db_repo = request.db_repo
537
544
538 log.debug(
545 log.debug(
539 '%s checking if archived flag for repo for %s',
546 '%s checking if archived flag for repo for %s',
540 self.__class__.__name__, rhodecode_db_repo.repo_name)
547 self.__class__.__name__, rhodecode_db_repo.repo_name)
541
548
542 if rhodecode_db_repo.archived:
549 if rhodecode_db_repo.archived:
543 log.warning('Current view is not supported for archived repo:%s',
550 log.warning('Current view is not supported for archived repo:%s',
544 rhodecode_db_repo.repo_name)
551 rhodecode_db_repo.repo_name)
545
552
546 h.flash(
553 h.flash(
547 h.literal(_('Action not supported for archived repository.')),
554 h.literal(_('Action not supported for archived repository.')),
548 category='warning')
555 category='warning')
549 summary_url = request.route_path(
556 summary_url = request.route_path(
550 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
557 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
551 raise HTTPFound(summary_url)
558 raise HTTPFound(summary_url)
552 return True
559 return True
553
560
554
561
555 class RepoTypeRoutePredicate(object):
562 class RepoTypeRoutePredicate(object):
556 def __init__(self, val, config):
563 def __init__(self, val, config):
557 self.val = val or ['hg', 'git', 'svn']
564 self.val = val or ['hg', 'git', 'svn']
558
565
559 def text(self):
566 def text(self):
560 return 'repo_accepted_type = %s' % self.val
567 return 'repo_accepted_type = %s' % self.val
561
568
562 phash = text
569 phash = text
563
570
564 def __call__(self, info, request):
571 def __call__(self, info, request):
565 if hasattr(request, 'vcs_call'):
572 if hasattr(request, 'vcs_call'):
566 # skip vcs calls
573 # skip vcs calls
567 return
574 return
568
575
569 rhodecode_db_repo = request.db_repo
576 rhodecode_db_repo = request.db_repo
570
577
571 log.debug(
578 log.debug(
572 '%s checking repo type for %s in %s',
579 '%s checking repo type for %s in %s',
573 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
580 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
574
581
575 if rhodecode_db_repo.repo_type in self.val:
582 if rhodecode_db_repo.repo_type in self.val:
576 return True
583 return True
577 else:
584 else:
578 log.warning('Current view is not supported for repo type:%s',
585 log.warning('Current view is not supported for repo type:%s',
579 rhodecode_db_repo.repo_type)
586 rhodecode_db_repo.repo_type)
580 return False
587 return False
581
588
582
589
583 class RepoGroupRoutePredicate(object):
590 class RepoGroupRoutePredicate(object):
584 def __init__(self, val, config):
591 def __init__(self, val, config):
585 self.val = val
592 self.val = val
586
593
587 def text(self):
594 def text(self):
588 return 'repo_group_route = %s' % self.val
595 return 'repo_group_route = %s' % self.val
589
596
590 phash = text
597 phash = text
591
598
592 def __call__(self, info, request):
599 def __call__(self, info, request):
593 if hasattr(request, 'vcs_call'):
600 if hasattr(request, 'vcs_call'):
594 # skip vcs calls
601 # skip vcs calls
595 return
602 return
596
603
597 repo_group_name = info['match']['repo_group_name']
604 repo_group_name = info['match']['repo_group_name']
598 repo_group_model = repo_group.RepoGroupModel()
605 repo_group_model = repo_group.RepoGroupModel()
599 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
606 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
600
607
601 if by_name_match:
608 if by_name_match:
602 # register this as request object we can re-use later
609 # register this as request object we can re-use later
603 request.db_repo_group = by_name_match
610 request.db_repo_group = by_name_match
604 return True
611 return True
605
612
606 return False
613 return False
607
614
608
615
609 class UserGroupRoutePredicate(object):
616 class UserGroupRoutePredicate(object):
610 def __init__(self, val, config):
617 def __init__(self, val, config):
611 self.val = val
618 self.val = val
612
619
613 def text(self):
620 def text(self):
614 return 'user_group_route = %s' % self.val
621 return 'user_group_route = %s' % self.val
615
622
616 phash = text
623 phash = text
617
624
618 def __call__(self, info, request):
625 def __call__(self, info, request):
619 if hasattr(request, 'vcs_call'):
626 if hasattr(request, 'vcs_call'):
620 # skip vcs calls
627 # skip vcs calls
621 return
628 return
622
629
623 user_group_id = info['match']['user_group_id']
630 user_group_id = info['match']['user_group_id']
624 user_group_model = user_group.UserGroup()
631 user_group_model = user_group.UserGroup()
625 by_id_match = user_group_model.get(user_group_id, cache=False)
632 by_id_match = user_group_model.get(user_group_id, cache=False)
626
633
627 if by_id_match:
634 if by_id_match:
628 # register this as request object we can re-use later
635 # register this as request object we can re-use later
629 request.db_user_group = by_id_match
636 request.db_user_group = by_id_match
630 return True
637 return True
631
638
632 return False
639 return False
633
640
634
641
635 class UserRoutePredicateBase(object):
642 class UserRoutePredicateBase(object):
636 supports_default = None
643 supports_default = None
637
644
638 def __init__(self, val, config):
645 def __init__(self, val, config):
639 self.val = val
646 self.val = val
640
647
641 def text(self):
648 def text(self):
642 raise NotImplementedError()
649 raise NotImplementedError()
643
650
644 def __call__(self, info, request):
651 def __call__(self, info, request):
645 if hasattr(request, 'vcs_call'):
652 if hasattr(request, 'vcs_call'):
646 # skip vcs calls
653 # skip vcs calls
647 return
654 return
648
655
649 user_id = info['match']['user_id']
656 user_id = info['match']['user_id']
650 user_model = user.User()
657 user_model = user.User()
651 by_id_match = user_model.get(user_id, cache=False)
658 by_id_match = user_model.get(user_id, cache=False)
652
659
653 if by_id_match:
660 if by_id_match:
654 # register this as request object we can re-use later
661 # register this as request object we can re-use later
655 request.db_user = by_id_match
662 request.db_user = by_id_match
656 request.db_user_supports_default = self.supports_default
663 request.db_user_supports_default = self.supports_default
657 return True
664 return True
658
665
659 return False
666 return False
660
667
661
668
662 class UserRoutePredicate(UserRoutePredicateBase):
669 class UserRoutePredicate(UserRoutePredicateBase):
663 supports_default = False
670 supports_default = False
664
671
665 def text(self):
672 def text(self):
666 return 'user_route = %s' % self.val
673 return 'user_route = %s' % self.val
667
674
668 phash = text
675 phash = text
669
676
670
677
671 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
678 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
672 supports_default = True
679 supports_default = True
673
680
674 def text(self):
681 def text(self):
675 return 'user_with_default_route = %s' % self.val
682 return 'user_with_default_route = %s' % self.val
676
683
677 phash = text
684 phash = text
678
685
679
686
680 def includeme(config):
687 def includeme(config):
681 config.add_route_predicate(
688 config.add_route_predicate(
682 'repo_route', RepoRoutePredicate)
689 'repo_route', RepoRoutePredicate)
683 config.add_route_predicate(
690 config.add_route_predicate(
684 'repo_accepted_types', RepoTypeRoutePredicate)
691 'repo_accepted_types', RepoTypeRoutePredicate)
685 config.add_route_predicate(
692 config.add_route_predicate(
686 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
693 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
687 config.add_route_predicate(
694 config.add_route_predicate(
688 'repo_group_route', RepoGroupRoutePredicate)
695 'repo_group_route', RepoGroupRoutePredicate)
689 config.add_route_predicate(
696 config.add_route_predicate(
690 'user_group_route', UserGroupRoutePredicate)
697 'user_group_route', UserGroupRoutePredicate)
691 config.add_route_predicate(
698 config.add_route_predicate(
692 'user_route_with_default', UserRouteWithDefaultPredicate)
699 'user_route_with_default', UserRouteWithDefaultPredicate)
693 config.add_route_predicate(
700 config.add_route_predicate(
694 'user_route', UserRoutePredicate)
701 'user_route', UserRoutePredicate)
@@ -1,1394 +1,1395 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import itertools
21 import itertools
22 import logging
22 import logging
23 import os
23 import os
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27 import urllib
27 import urllib
28
28
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from pyramid.renderers import render
31 from pyramid.renderers import render
32 from pyramid.response import Response
32 from pyramid.response import Response
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.apps._base import RepoAppView
35 from rhodecode.apps._base import RepoAppView
36
36
37
37
38 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import diffs, helpers as h, rc_cache
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.view_utils import parse_path_ref
40 from rhodecode.lib.view_utils import parse_path_ref
41 from rhodecode.lib.exceptions import NonRelativePathError
41 from rhodecode.lib.exceptions import NonRelativePathError
42 from rhodecode.lib.codeblocks import (
42 from rhodecode.lib.codeblocks import (
43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
46 from rhodecode.lib.auth import (
46 from rhodecode.lib.auth import (
47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs import path as vcspath
49 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.conf import settings
51 from rhodecode.lib.vcs.nodes import FileNode
51 from rhodecode.lib.vcs.nodes import FileNode
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 NodeDoesNotExistError, CommitError, NodeError)
55 NodeDoesNotExistError, CommitError, NodeError)
56
56
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class RepoFilesView(RepoAppView):
63 class RepoFilesView(RepoAppView):
64
64
65 @staticmethod
65 @staticmethod
66 def adjust_file_path_for_svn(f_path, repo):
66 def adjust_file_path_for_svn(f_path, repo):
67 """
67 """
68 Computes the relative path of `f_path`.
68 Computes the relative path of `f_path`.
69
69
70 This is mainly based on prefix matching of the recognized tags and
70 This is mainly based on prefix matching of the recognized tags and
71 branches in the underlying repository.
71 branches in the underlying repository.
72 """
72 """
73 tags_and_branches = itertools.chain(
73 tags_and_branches = itertools.chain(
74 repo.branches.iterkeys(),
74 repo.branches.iterkeys(),
75 repo.tags.iterkeys())
75 repo.tags.iterkeys())
76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77
77
78 for name in tags_and_branches:
78 for name in tags_and_branches:
79 if f_path.startswith('{}/'.format(name)):
79 if f_path.startswith('{}/'.format(name)):
80 f_path = vcspath.relpath(f_path, name)
80 f_path = vcspath.relpath(f_path, name)
81 break
81 break
82 return f_path
82 return f_path
83
83
84 def load_default_context(self):
84 def load_default_context(self):
85 c = self._get_local_tmpl_context(include_app_defaults=True)
85 c = self._get_local_tmpl_context(include_app_defaults=True)
86 c.rhodecode_repo = self.rhodecode_vcs_repo
86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 c.enable_downloads = self.db_repo.enable_downloads
87 c.enable_downloads = self.db_repo.enable_downloads
88 return c
88 return c
89
89
90 def _ensure_not_locked(self):
90 def _ensure_not_locked(self):
91 _ = self.request.translate
91 _ = self.request.translate
92
92
93 repo = self.db_repo
93 repo = self.db_repo
94 if repo.enable_locking and repo.locked[0]:
94 if repo.enable_locking and repo.locked[0]:
95 h.flash(_('This repository has been locked by %s on %s')
95 h.flash(_('This repository has been locked by %s on %s')
96 % (h.person_by_id(repo.locked[0]),
96 % (h.person_by_id(repo.locked[0]),
97 h.format_date(h.time_to_datetime(repo.locked[1]))),
97 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 'warning')
98 'warning')
99 files_url = h.route_path(
99 files_url = h.route_path(
100 'repo_files:default_path',
100 'repo_files:default_path',
101 repo_name=self.db_repo_name, commit_id='tip')
101 repo_name=self.db_repo_name, commit_id='tip')
102 raise HTTPFound(files_url)
102 raise HTTPFound(files_url)
103
103
104 def check_branch_permission(self, branch_name):
104 def check_branch_permission(self, branch_name):
105 _ = self.request.translate
105 _ = self.request.translate
106
106
107 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
107 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
108 self.db_repo_name, branch_name)
108 self.db_repo_name, branch_name)
109 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
109 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
110 h.flash(
110 h.flash(
111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
112 'warning')
112 'warning')
113 files_url = h.route_path(
113 files_url = h.route_path(
114 'repo_files:default_path',
114 'repo_files:default_path',
115 repo_name=self.db_repo_name, commit_id='tip')
115 repo_name=self.db_repo_name, commit_id='tip')
116 raise HTTPFound(files_url)
116 raise HTTPFound(files_url)
117
117
118 def _get_commit_and_path(self):
118 def _get_commit_and_path(self):
119 default_commit_id = self.db_repo.landing_rev[1]
119 default_commit_id = self.db_repo.landing_rev[1]
120 default_f_path = '/'
120 default_f_path = '/'
121
121
122 commit_id = self.request.matchdict.get(
122 commit_id = self.request.matchdict.get(
123 'commit_id', default_commit_id)
123 'commit_id', default_commit_id)
124 f_path = self._get_f_path(self.request.matchdict, default_f_path)
124 f_path = self._get_f_path(self.request.matchdict, default_f_path)
125 return commit_id, f_path
125 return commit_id, f_path
126
126
127 def _get_default_encoding(self, c):
127 def _get_default_encoding(self, c):
128 enc_list = getattr(c, 'default_encodings', [])
128 enc_list = getattr(c, 'default_encodings', [])
129 return enc_list[0] if enc_list else 'UTF-8'
129 return enc_list[0] if enc_list else 'UTF-8'
130
130
131 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
131 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
132 """
132 """
133 This is a safe way to get commit. If an error occurs it redirects to
133 This is a safe way to get commit. If an error occurs it redirects to
134 tip with proper message
134 tip with proper message
135
135
136 :param commit_id: id of commit to fetch
136 :param commit_id: id of commit to fetch
137 :param redirect_after: toggle redirection
137 :param redirect_after: toggle redirection
138 """
138 """
139 _ = self.request.translate
139 _ = self.request.translate
140
140
141 try:
141 try:
142 return self.rhodecode_vcs_repo.get_commit(commit_id)
142 return self.rhodecode_vcs_repo.get_commit(commit_id)
143 except EmptyRepositoryError:
143 except EmptyRepositoryError:
144 if not redirect_after:
144 if not redirect_after:
145 return None
145 return None
146
146
147 _url = h.route_path(
147 _url = h.route_path(
148 'repo_files_add_file',
148 'repo_files_add_file',
149 repo_name=self.db_repo_name, commit_id=0, f_path='',
149 repo_name=self.db_repo_name, commit_id=0, f_path='',
150 _anchor='edit')
150 _anchor='edit')
151
151
152 if h.HasRepoPermissionAny(
152 if h.HasRepoPermissionAny(
153 'repository.write', 'repository.admin')(self.db_repo_name):
153 'repository.write', 'repository.admin')(self.db_repo_name):
154 add_new = h.link_to(
154 add_new = h.link_to(
155 _('Click here to add a new file.'), _url, class_="alert-link")
155 _('Click here to add a new file.'), _url, class_="alert-link")
156 else:
156 else:
157 add_new = ""
157 add_new = ""
158
158
159 h.flash(h.literal(
159 h.flash(h.literal(
160 _('There are no files yet. %s') % add_new), category='warning')
160 _('There are no files yet. %s') % add_new), category='warning')
161 raise HTTPFound(
161 raise HTTPFound(
162 h.route_path('repo_summary', repo_name=self.db_repo_name))
162 h.route_path('repo_summary', repo_name=self.db_repo_name))
163
163
164 except (CommitDoesNotExistError, LookupError):
164 except (CommitDoesNotExistError, LookupError):
165 msg = _('No such commit exists for this repository')
165 msg = _('No such commit exists for this repository')
166 h.flash(msg, category='error')
166 h.flash(msg, category='error')
167 raise HTTPNotFound()
167 raise HTTPNotFound()
168 except RepositoryError as e:
168 except RepositoryError as e:
169 h.flash(safe_str(h.escape(e)), category='error')
169 h.flash(safe_str(h.escape(e)), category='error')
170 raise HTTPNotFound()
170 raise HTTPNotFound()
171
171
172 def _get_filenode_or_redirect(self, commit_obj, path):
172 def _get_filenode_or_redirect(self, commit_obj, path):
173 """
173 """
174 Returns file_node, if error occurs or given path is directory,
174 Returns file_node, if error occurs or given path is directory,
175 it'll redirect to top level path
175 it'll redirect to top level path
176 """
176 """
177 _ = self.request.translate
177 _ = self.request.translate
178
178
179 try:
179 try:
180 file_node = commit_obj.get_node(path)
180 file_node = commit_obj.get_node(path)
181 if file_node.is_dir():
181 if file_node.is_dir():
182 raise RepositoryError('The given path is a directory')
182 raise RepositoryError('The given path is a directory')
183 except CommitDoesNotExistError:
183 except CommitDoesNotExistError:
184 log.exception('No such commit exists for this repository')
184 log.exception('No such commit exists for this repository')
185 h.flash(_('No such commit exists for this repository'), category='error')
185 h.flash(_('No such commit exists for this repository'), category='error')
186 raise HTTPNotFound()
186 raise HTTPNotFound()
187 except RepositoryError as e:
187 except RepositoryError as e:
188 log.warning('Repository error while fetching '
188 log.warning('Repository error while fetching '
189 'filenode `%s`. Err:%s', path, e)
189 'filenode `%s`. Err:%s', path, e)
190 h.flash(safe_str(h.escape(e)), category='error')
190 h.flash(safe_str(h.escape(e)), category='error')
191 raise HTTPNotFound()
191 raise HTTPNotFound()
192
192
193 return file_node
193 return file_node
194
194
195 def _is_valid_head(self, commit_id, repo):
195 def _is_valid_head(self, commit_id, repo):
196 branch_name = sha_commit_id = ''
196 branch_name = sha_commit_id = ''
197 is_head = False
197 is_head = False
198
198
199 if h.is_svn(repo) and not repo.is_empty():
199 if h.is_svn(repo) and not repo.is_empty():
200 # Note: Subversion only has one head.
200 # Note: Subversion only has one head.
201 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
201 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
202 is_head = True
202 is_head = True
203 return branch_name, sha_commit_id, is_head
203 return branch_name, sha_commit_id, is_head
204
204
205 for _branch_name, branch_commit_id in repo.branches.items():
205 for _branch_name, branch_commit_id in repo.branches.items():
206 # simple case we pass in branch name, it's a HEAD
206 # simple case we pass in branch name, it's a HEAD
207 if commit_id == _branch_name:
207 if commit_id == _branch_name:
208 is_head = True
208 is_head = True
209 branch_name = _branch_name
209 branch_name = _branch_name
210 sha_commit_id = branch_commit_id
210 sha_commit_id = branch_commit_id
211 break
211 break
212 # case when we pass in full sha commit_id, which is a head
212 # case when we pass in full sha commit_id, which is a head
213 elif commit_id == branch_commit_id:
213 elif commit_id == branch_commit_id:
214 is_head = True
214 is_head = True
215 branch_name = _branch_name
215 branch_name = _branch_name
216 sha_commit_id = branch_commit_id
216 sha_commit_id = branch_commit_id
217 break
217 break
218
218
219 # checked branches, means we only need to try to get the branch/commit_sha
219 # checked branches, means we only need to try to get the branch/commit_sha
220 if not repo.is_empty:
220 if not repo.is_empty:
221 commit = repo.get_commit(commit_id=commit_id)
221 commit = repo.get_commit(commit_id=commit_id)
222 if commit:
222 if commit:
223 branch_name = commit.branch
223 branch_name = commit.branch
224 sha_commit_id = commit.raw_id
224 sha_commit_id = commit.raw_id
225
225
226 return branch_name, sha_commit_id, is_head
226 return branch_name, sha_commit_id, is_head
227
227
228 def _get_tree_at_commit(
228 def _get_tree_at_commit(
229 self, c, commit_id, f_path, full_load=False):
229 self, c, commit_id, f_path, full_load=False):
230
230
231 repo_id = self.db_repo.repo_id
231 repo_id = self.db_repo.repo_id
232 force_recache = self.get_recache_flag()
232
233
233 cache_seconds = safe_int(
234 cache_seconds = safe_int(
234 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
235 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
235 cache_on = cache_seconds > 0
236 cache_on = not force_recache and cache_seconds > 0
236 log.debug(
237 log.debug(
237 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
238 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
238 'with caching: %s[TTL: %ss]' % (
239 'with caching: %s[TTL: %ss]' % (
239 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
240 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
240
241
241 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
242 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
242 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
243 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
243
244
244 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
245 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
245 condition=cache_on)
246 condition=cache_on)
246 def compute_file_tree(repo_id, commit_id, f_path, full_load):
247 def compute_file_tree(repo_id, commit_id, f_path, full_load):
247 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
248 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
248 repo_id, commit_id, f_path)
249 repo_id, commit_id, f_path)
249
250
250 c.full_load = full_load
251 c.full_load = full_load
251 return render(
252 return render(
252 'rhodecode:templates/files/files_browser_tree.mako',
253 'rhodecode:templates/files/files_browser_tree.mako',
253 self._get_template_context(c), self.request)
254 self._get_template_context(c), self.request)
254
255
255 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
256 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
256
257
257 def _get_archive_spec(self, fname):
258 def _get_archive_spec(self, fname):
258 log.debug('Detecting archive spec for: `%s`', fname)
259 log.debug('Detecting archive spec for: `%s`', fname)
259
260
260 fileformat = None
261 fileformat = None
261 ext = None
262 ext = None
262 content_type = None
263 content_type = None
263 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
264 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
264 content_type, extension = ext_data
265 content_type, extension = ext_data
265
266
266 if fname.endswith(extension):
267 if fname.endswith(extension):
267 fileformat = a_type
268 fileformat = a_type
268 log.debug('archive is of type: %s', fileformat)
269 log.debug('archive is of type: %s', fileformat)
269 ext = extension
270 ext = extension
270 break
271 break
271
272
272 if not fileformat:
273 if not fileformat:
273 raise ValueError()
274 raise ValueError()
274
275
275 # left over part of whole fname is the commit
276 # left over part of whole fname is the commit
276 commit_id = fname[:-len(ext)]
277 commit_id = fname[:-len(ext)]
277
278
278 return commit_id, ext, fileformat, content_type
279 return commit_id, ext, fileformat, content_type
279
280
280 @LoginRequired()
281 @LoginRequired()
281 @HasRepoPermissionAnyDecorator(
282 @HasRepoPermissionAnyDecorator(
282 'repository.read', 'repository.write', 'repository.admin')
283 'repository.read', 'repository.write', 'repository.admin')
283 @view_config(
284 @view_config(
284 route_name='repo_archivefile', request_method='GET',
285 route_name='repo_archivefile', request_method='GET',
285 renderer=None)
286 renderer=None)
286 def repo_archivefile(self):
287 def repo_archivefile(self):
287 # archive cache config
288 # archive cache config
288 from rhodecode import CONFIG
289 from rhodecode import CONFIG
289 _ = self.request.translate
290 _ = self.request.translate
290 self.load_default_context()
291 self.load_default_context()
291
292
292 fname = self.request.matchdict['fname']
293 fname = self.request.matchdict['fname']
293 subrepos = self.request.GET.get('subrepos') == 'true'
294 subrepos = self.request.GET.get('subrepos') == 'true'
294
295
295 if not self.db_repo.enable_downloads:
296 if not self.db_repo.enable_downloads:
296 return Response(_('Downloads disabled'))
297 return Response(_('Downloads disabled'))
297
298
298 try:
299 try:
299 commit_id, ext, fileformat, content_type = \
300 commit_id, ext, fileformat, content_type = \
300 self._get_archive_spec(fname)
301 self._get_archive_spec(fname)
301 except ValueError:
302 except ValueError:
302 return Response(_('Unknown archive type for: `{}`').format(
303 return Response(_('Unknown archive type for: `{}`').format(
303 h.escape(fname)))
304 h.escape(fname)))
304
305
305 try:
306 try:
306 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
307 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
307 except CommitDoesNotExistError:
308 except CommitDoesNotExistError:
308 return Response(_('Unknown commit_id {}').format(
309 return Response(_('Unknown commit_id {}').format(
309 h.escape(commit_id)))
310 h.escape(commit_id)))
310 except EmptyRepositoryError:
311 except EmptyRepositoryError:
311 return Response(_('Empty repository'))
312 return Response(_('Empty repository'))
312
313
313 archive_name = '%s-%s%s%s' % (
314 archive_name = '%s-%s%s%s' % (
314 safe_str(self.db_repo_name.replace('/', '_')),
315 safe_str(self.db_repo_name.replace('/', '_')),
315 '-sub' if subrepos else '',
316 '-sub' if subrepos else '',
316 safe_str(commit.short_id), ext)
317 safe_str(commit.short_id), ext)
317
318
318 use_cached_archive = False
319 use_cached_archive = False
319 archive_cache_enabled = CONFIG.get(
320 archive_cache_enabled = CONFIG.get(
320 'archive_cache_dir') and not self.request.GET.get('no_cache')
321 'archive_cache_dir') and not self.request.GET.get('no_cache')
321 cached_archive_path = None
322 cached_archive_path = None
322
323
323 if archive_cache_enabled:
324 if archive_cache_enabled:
324 # check if we it's ok to write
325 # check if we it's ok to write
325 if not os.path.isdir(CONFIG['archive_cache_dir']):
326 if not os.path.isdir(CONFIG['archive_cache_dir']):
326 os.makedirs(CONFIG['archive_cache_dir'])
327 os.makedirs(CONFIG['archive_cache_dir'])
327 cached_archive_path = os.path.join(
328 cached_archive_path = os.path.join(
328 CONFIG['archive_cache_dir'], archive_name)
329 CONFIG['archive_cache_dir'], archive_name)
329 if os.path.isfile(cached_archive_path):
330 if os.path.isfile(cached_archive_path):
330 log.debug('Found cached archive in %s', cached_archive_path)
331 log.debug('Found cached archive in %s', cached_archive_path)
331 fd, archive = None, cached_archive_path
332 fd, archive = None, cached_archive_path
332 use_cached_archive = True
333 use_cached_archive = True
333 else:
334 else:
334 log.debug('Archive %s is not yet cached', archive_name)
335 log.debug('Archive %s is not yet cached', archive_name)
335
336
336 if not use_cached_archive:
337 if not use_cached_archive:
337 # generate new archive
338 # generate new archive
338 fd, archive = tempfile.mkstemp()
339 fd, archive = tempfile.mkstemp()
339 log.debug('Creating new temp archive in %s', archive)
340 log.debug('Creating new temp archive in %s', archive)
340 try:
341 try:
341 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
342 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
342 except ImproperArchiveTypeError:
343 except ImproperArchiveTypeError:
343 return _('Unknown archive type')
344 return _('Unknown archive type')
344 if archive_cache_enabled:
345 if archive_cache_enabled:
345 # if we generated the archive and we have cache enabled
346 # if we generated the archive and we have cache enabled
346 # let's use this for future
347 # let's use this for future
347 log.debug('Storing new archive in %s', cached_archive_path)
348 log.debug('Storing new archive in %s', cached_archive_path)
348 shutil.move(archive, cached_archive_path)
349 shutil.move(archive, cached_archive_path)
349 archive = cached_archive_path
350 archive = cached_archive_path
350
351
351 # store download action
352 # store download action
352 audit_logger.store_web(
353 audit_logger.store_web(
353 'repo.archive.download', action_data={
354 'repo.archive.download', action_data={
354 'user_agent': self.request.user_agent,
355 'user_agent': self.request.user_agent,
355 'archive_name': archive_name,
356 'archive_name': archive_name,
356 'archive_spec': fname,
357 'archive_spec': fname,
357 'archive_cached': use_cached_archive},
358 'archive_cached': use_cached_archive},
358 user=self._rhodecode_user,
359 user=self._rhodecode_user,
359 repo=self.db_repo,
360 repo=self.db_repo,
360 commit=True
361 commit=True
361 )
362 )
362
363
363 def get_chunked_archive(archive_path):
364 def get_chunked_archive(archive_path):
364 with open(archive_path, 'rb') as stream:
365 with open(archive_path, 'rb') as stream:
365 while True:
366 while True:
366 data = stream.read(16 * 1024)
367 data = stream.read(16 * 1024)
367 if not data:
368 if not data:
368 if fd: # fd means we used temporary file
369 if fd: # fd means we used temporary file
369 os.close(fd)
370 os.close(fd)
370 if not archive_cache_enabled:
371 if not archive_cache_enabled:
371 log.debug('Destroying temp archive %s', archive_path)
372 log.debug('Destroying temp archive %s', archive_path)
372 os.remove(archive_path)
373 os.remove(archive_path)
373 break
374 break
374 yield data
375 yield data
375
376
376 response = Response(app_iter=get_chunked_archive(archive))
377 response = Response(app_iter=get_chunked_archive(archive))
377 response.content_disposition = str(
378 response.content_disposition = str(
378 'attachment; filename=%s' % archive_name)
379 'attachment; filename=%s' % archive_name)
379 response.content_type = str(content_type)
380 response.content_type = str(content_type)
380
381
381 return response
382 return response
382
383
383 def _get_file_node(self, commit_id, f_path):
384 def _get_file_node(self, commit_id, f_path):
384 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
385 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
385 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
386 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
386 try:
387 try:
387 node = commit.get_node(f_path)
388 node = commit.get_node(f_path)
388 if node.is_dir():
389 if node.is_dir():
389 raise NodeError('%s path is a %s not a file'
390 raise NodeError('%s path is a %s not a file'
390 % (node, type(node)))
391 % (node, type(node)))
391 except NodeDoesNotExistError:
392 except NodeDoesNotExistError:
392 commit = EmptyCommit(
393 commit = EmptyCommit(
393 commit_id=commit_id,
394 commit_id=commit_id,
394 idx=commit.idx,
395 idx=commit.idx,
395 repo=commit.repository,
396 repo=commit.repository,
396 alias=commit.repository.alias,
397 alias=commit.repository.alias,
397 message=commit.message,
398 message=commit.message,
398 author=commit.author,
399 author=commit.author,
399 date=commit.date)
400 date=commit.date)
400 node = FileNode(f_path, '', commit=commit)
401 node = FileNode(f_path, '', commit=commit)
401 else:
402 else:
402 commit = EmptyCommit(
403 commit = EmptyCommit(
403 repo=self.rhodecode_vcs_repo,
404 repo=self.rhodecode_vcs_repo,
404 alias=self.rhodecode_vcs_repo.alias)
405 alias=self.rhodecode_vcs_repo.alias)
405 node = FileNode(f_path, '', commit=commit)
406 node = FileNode(f_path, '', commit=commit)
406 return node
407 return node
407
408
408 @LoginRequired()
409 @LoginRequired()
409 @HasRepoPermissionAnyDecorator(
410 @HasRepoPermissionAnyDecorator(
410 'repository.read', 'repository.write', 'repository.admin')
411 'repository.read', 'repository.write', 'repository.admin')
411 @view_config(
412 @view_config(
412 route_name='repo_files_diff', request_method='GET',
413 route_name='repo_files_diff', request_method='GET',
413 renderer=None)
414 renderer=None)
414 def repo_files_diff(self):
415 def repo_files_diff(self):
415 c = self.load_default_context()
416 c = self.load_default_context()
416 f_path = self._get_f_path(self.request.matchdict)
417 f_path = self._get_f_path(self.request.matchdict)
417 diff1 = self.request.GET.get('diff1', '')
418 diff1 = self.request.GET.get('diff1', '')
418 diff2 = self.request.GET.get('diff2', '')
419 diff2 = self.request.GET.get('diff2', '')
419
420
420 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
421 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
421
422
422 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
423 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
423 line_context = self.request.GET.get('context', 3)
424 line_context = self.request.GET.get('context', 3)
424
425
425 if not any((diff1, diff2)):
426 if not any((diff1, diff2)):
426 h.flash(
427 h.flash(
427 'Need query parameter "diff1" or "diff2" to generate a diff.',
428 'Need query parameter "diff1" or "diff2" to generate a diff.',
428 category='error')
429 category='error')
429 raise HTTPBadRequest()
430 raise HTTPBadRequest()
430
431
431 c.action = self.request.GET.get('diff')
432 c.action = self.request.GET.get('diff')
432 if c.action not in ['download', 'raw']:
433 if c.action not in ['download', 'raw']:
433 compare_url = h.route_path(
434 compare_url = h.route_path(
434 'repo_compare',
435 'repo_compare',
435 repo_name=self.db_repo_name,
436 repo_name=self.db_repo_name,
436 source_ref_type='rev',
437 source_ref_type='rev',
437 source_ref=diff1,
438 source_ref=diff1,
438 target_repo=self.db_repo_name,
439 target_repo=self.db_repo_name,
439 target_ref_type='rev',
440 target_ref_type='rev',
440 target_ref=diff2,
441 target_ref=diff2,
441 _query=dict(f_path=f_path))
442 _query=dict(f_path=f_path))
442 # redirect to new view if we render diff
443 # redirect to new view if we render diff
443 raise HTTPFound(compare_url)
444 raise HTTPFound(compare_url)
444
445
445 try:
446 try:
446 node1 = self._get_file_node(diff1, path1)
447 node1 = self._get_file_node(diff1, path1)
447 node2 = self._get_file_node(diff2, f_path)
448 node2 = self._get_file_node(diff2, f_path)
448 except (RepositoryError, NodeError):
449 except (RepositoryError, NodeError):
449 log.exception("Exception while trying to get node from repository")
450 log.exception("Exception while trying to get node from repository")
450 raise HTTPFound(
451 raise HTTPFound(
451 h.route_path('repo_files', repo_name=self.db_repo_name,
452 h.route_path('repo_files', repo_name=self.db_repo_name,
452 commit_id='tip', f_path=f_path))
453 commit_id='tip', f_path=f_path))
453
454
454 if all(isinstance(node.commit, EmptyCommit)
455 if all(isinstance(node.commit, EmptyCommit)
455 for node in (node1, node2)):
456 for node in (node1, node2)):
456 raise HTTPNotFound()
457 raise HTTPNotFound()
457
458
458 c.commit_1 = node1.commit
459 c.commit_1 = node1.commit
459 c.commit_2 = node2.commit
460 c.commit_2 = node2.commit
460
461
461 if c.action == 'download':
462 if c.action == 'download':
462 _diff = diffs.get_gitdiff(node1, node2,
463 _diff = diffs.get_gitdiff(node1, node2,
463 ignore_whitespace=ignore_whitespace,
464 ignore_whitespace=ignore_whitespace,
464 context=line_context)
465 context=line_context)
465 diff = diffs.DiffProcessor(_diff, format='gitdiff')
466 diff = diffs.DiffProcessor(_diff, format='gitdiff')
466
467
467 response = Response(self.path_filter.get_raw_patch(diff))
468 response = Response(self.path_filter.get_raw_patch(diff))
468 response.content_type = 'text/plain'
469 response.content_type = 'text/plain'
469 response.content_disposition = (
470 response.content_disposition = (
470 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
471 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
471 )
472 )
472 charset = self._get_default_encoding(c)
473 charset = self._get_default_encoding(c)
473 if charset:
474 if charset:
474 response.charset = charset
475 response.charset = charset
475 return response
476 return response
476
477
477 elif c.action == 'raw':
478 elif c.action == 'raw':
478 _diff = diffs.get_gitdiff(node1, node2,
479 _diff = diffs.get_gitdiff(node1, node2,
479 ignore_whitespace=ignore_whitespace,
480 ignore_whitespace=ignore_whitespace,
480 context=line_context)
481 context=line_context)
481 diff = diffs.DiffProcessor(_diff, format='gitdiff')
482 diff = diffs.DiffProcessor(_diff, format='gitdiff')
482
483
483 response = Response(self.path_filter.get_raw_patch(diff))
484 response = Response(self.path_filter.get_raw_patch(diff))
484 response.content_type = 'text/plain'
485 response.content_type = 'text/plain'
485 charset = self._get_default_encoding(c)
486 charset = self._get_default_encoding(c)
486 if charset:
487 if charset:
487 response.charset = charset
488 response.charset = charset
488 return response
489 return response
489
490
490 # in case we ever end up here
491 # in case we ever end up here
491 raise HTTPNotFound()
492 raise HTTPNotFound()
492
493
493 @LoginRequired()
494 @LoginRequired()
494 @HasRepoPermissionAnyDecorator(
495 @HasRepoPermissionAnyDecorator(
495 'repository.read', 'repository.write', 'repository.admin')
496 'repository.read', 'repository.write', 'repository.admin')
496 @view_config(
497 @view_config(
497 route_name='repo_files_diff_2way_redirect', request_method='GET',
498 route_name='repo_files_diff_2way_redirect', request_method='GET',
498 renderer=None)
499 renderer=None)
499 def repo_files_diff_2way_redirect(self):
500 def repo_files_diff_2way_redirect(self):
500 """
501 """
501 Kept only to make OLD links work
502 Kept only to make OLD links work
502 """
503 """
503 f_path = self._get_f_path_unchecked(self.request.matchdict)
504 f_path = self._get_f_path_unchecked(self.request.matchdict)
504 diff1 = self.request.GET.get('diff1', '')
505 diff1 = self.request.GET.get('diff1', '')
505 diff2 = self.request.GET.get('diff2', '')
506 diff2 = self.request.GET.get('diff2', '')
506
507
507 if not any((diff1, diff2)):
508 if not any((diff1, diff2)):
508 h.flash(
509 h.flash(
509 'Need query parameter "diff1" or "diff2" to generate a diff.',
510 'Need query parameter "diff1" or "diff2" to generate a diff.',
510 category='error')
511 category='error')
511 raise HTTPBadRequest()
512 raise HTTPBadRequest()
512
513
513 compare_url = h.route_path(
514 compare_url = h.route_path(
514 'repo_compare',
515 'repo_compare',
515 repo_name=self.db_repo_name,
516 repo_name=self.db_repo_name,
516 source_ref_type='rev',
517 source_ref_type='rev',
517 source_ref=diff1,
518 source_ref=diff1,
518 target_ref_type='rev',
519 target_ref_type='rev',
519 target_ref=diff2,
520 target_ref=diff2,
520 _query=dict(f_path=f_path, diffmode='sideside',
521 _query=dict(f_path=f_path, diffmode='sideside',
521 target_repo=self.db_repo_name,))
522 target_repo=self.db_repo_name,))
522 raise HTTPFound(compare_url)
523 raise HTTPFound(compare_url)
523
524
524 @LoginRequired()
525 @LoginRequired()
525 @HasRepoPermissionAnyDecorator(
526 @HasRepoPermissionAnyDecorator(
526 'repository.read', 'repository.write', 'repository.admin')
527 'repository.read', 'repository.write', 'repository.admin')
527 @view_config(
528 @view_config(
528 route_name='repo_files', request_method='GET',
529 route_name='repo_files', request_method='GET',
529 renderer=None)
530 renderer=None)
530 @view_config(
531 @view_config(
531 route_name='repo_files:default_path', request_method='GET',
532 route_name='repo_files:default_path', request_method='GET',
532 renderer=None)
533 renderer=None)
533 @view_config(
534 @view_config(
534 route_name='repo_files:default_commit', request_method='GET',
535 route_name='repo_files:default_commit', request_method='GET',
535 renderer=None)
536 renderer=None)
536 @view_config(
537 @view_config(
537 route_name='repo_files:rendered', request_method='GET',
538 route_name='repo_files:rendered', request_method='GET',
538 renderer=None)
539 renderer=None)
539 @view_config(
540 @view_config(
540 route_name='repo_files:annotated', request_method='GET',
541 route_name='repo_files:annotated', request_method='GET',
541 renderer=None)
542 renderer=None)
542 def repo_files(self):
543 def repo_files(self):
543 c = self.load_default_context()
544 c = self.load_default_context()
544
545
545 view_name = getattr(self.request.matched_route, 'name', None)
546 view_name = getattr(self.request.matched_route, 'name', None)
546
547
547 c.annotate = view_name == 'repo_files:annotated'
548 c.annotate = view_name == 'repo_files:annotated'
548 # default is false, but .rst/.md files later are auto rendered, we can
549 # default is false, but .rst/.md files later are auto rendered, we can
549 # overwrite auto rendering by setting this GET flag
550 # overwrite auto rendering by setting this GET flag
550 c.renderer = view_name == 'repo_files:rendered' or \
551 c.renderer = view_name == 'repo_files:rendered' or \
551 not self.request.GET.get('no-render', False)
552 not self.request.GET.get('no-render', False)
552
553
553 # redirect to given commit_id from form if given
554 # redirect to given commit_id from form if given
554 get_commit_id = self.request.GET.get('at_rev', None)
555 get_commit_id = self.request.GET.get('at_rev', None)
555 if get_commit_id:
556 if get_commit_id:
556 self._get_commit_or_redirect(get_commit_id)
557 self._get_commit_or_redirect(get_commit_id)
557
558
558 commit_id, f_path = self._get_commit_and_path()
559 commit_id, f_path = self._get_commit_and_path()
559 c.commit = self._get_commit_or_redirect(commit_id)
560 c.commit = self._get_commit_or_redirect(commit_id)
560 c.branch = self.request.GET.get('branch', None)
561 c.branch = self.request.GET.get('branch', None)
561 c.f_path = f_path
562 c.f_path = f_path
562
563
563 # prev link
564 # prev link
564 try:
565 try:
565 prev_commit = c.commit.prev(c.branch)
566 prev_commit = c.commit.prev(c.branch)
566 c.prev_commit = prev_commit
567 c.prev_commit = prev_commit
567 c.url_prev = h.route_path(
568 c.url_prev = h.route_path(
568 'repo_files', repo_name=self.db_repo_name,
569 'repo_files', repo_name=self.db_repo_name,
569 commit_id=prev_commit.raw_id, f_path=f_path)
570 commit_id=prev_commit.raw_id, f_path=f_path)
570 if c.branch:
571 if c.branch:
571 c.url_prev += '?branch=%s' % c.branch
572 c.url_prev += '?branch=%s' % c.branch
572 except (CommitDoesNotExistError, VCSError):
573 except (CommitDoesNotExistError, VCSError):
573 c.url_prev = '#'
574 c.url_prev = '#'
574 c.prev_commit = EmptyCommit()
575 c.prev_commit = EmptyCommit()
575
576
576 # next link
577 # next link
577 try:
578 try:
578 next_commit = c.commit.next(c.branch)
579 next_commit = c.commit.next(c.branch)
579 c.next_commit = next_commit
580 c.next_commit = next_commit
580 c.url_next = h.route_path(
581 c.url_next = h.route_path(
581 'repo_files', repo_name=self.db_repo_name,
582 'repo_files', repo_name=self.db_repo_name,
582 commit_id=next_commit.raw_id, f_path=f_path)
583 commit_id=next_commit.raw_id, f_path=f_path)
583 if c.branch:
584 if c.branch:
584 c.url_next += '?branch=%s' % c.branch
585 c.url_next += '?branch=%s' % c.branch
585 except (CommitDoesNotExistError, VCSError):
586 except (CommitDoesNotExistError, VCSError):
586 c.url_next = '#'
587 c.url_next = '#'
587 c.next_commit = EmptyCommit()
588 c.next_commit = EmptyCommit()
588
589
589 # files or dirs
590 # files or dirs
590 try:
591 try:
591 c.file = c.commit.get_node(f_path)
592 c.file = c.commit.get_node(f_path)
592 c.file_author = True
593 c.file_author = True
593 c.file_tree = ''
594 c.file_tree = ''
594
595
595 # load file content
596 # load file content
596 if c.file.is_file():
597 if c.file.is_file():
597 c.lf_node = c.file.get_largefile_node()
598 c.lf_node = c.file.get_largefile_node()
598
599
599 c.file_source_page = 'true'
600 c.file_source_page = 'true'
600 c.file_last_commit = c.file.last_commit
601 c.file_last_commit = c.file.last_commit
601 if c.file.size < c.visual.cut_off_limit_diff:
602 if c.file.size < c.visual.cut_off_limit_diff:
602 if c.annotate: # annotation has precedence over renderer
603 if c.annotate: # annotation has precedence over renderer
603 c.annotated_lines = filenode_as_annotated_lines_tokens(
604 c.annotated_lines = filenode_as_annotated_lines_tokens(
604 c.file
605 c.file
605 )
606 )
606 else:
607 else:
607 c.renderer = (
608 c.renderer = (
608 c.renderer and h.renderer_from_filename(c.file.path)
609 c.renderer and h.renderer_from_filename(c.file.path)
609 )
610 )
610 if not c.renderer:
611 if not c.renderer:
611 c.lines = filenode_as_lines_tokens(c.file)
612 c.lines = filenode_as_lines_tokens(c.file)
612
613
613 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
614 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
614 commit_id, self.rhodecode_vcs_repo)
615 commit_id, self.rhodecode_vcs_repo)
615 c.on_branch_head = is_head
616 c.on_branch_head = is_head
616
617
617 branch = c.commit.branch if (
618 branch = c.commit.branch if (
618 c.commit.branch and '/' not in c.commit.branch) else None
619 c.commit.branch and '/' not in c.commit.branch) else None
619 c.branch_or_raw_id = branch or c.commit.raw_id
620 c.branch_or_raw_id = branch or c.commit.raw_id
620 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
621 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
621
622
622 author = c.file_last_commit.author
623 author = c.file_last_commit.author
623 c.authors = [[
624 c.authors = [[
624 h.email(author),
625 h.email(author),
625 h.person(author, 'username_or_name_or_email'),
626 h.person(author, 'username_or_name_or_email'),
626 1
627 1
627 ]]
628 ]]
628
629
629 else: # load tree content at path
630 else: # load tree content at path
630 c.file_source_page = 'false'
631 c.file_source_page = 'false'
631 c.authors = []
632 c.authors = []
632 # this loads a simple tree without metadata to speed things up
633 # this loads a simple tree without metadata to speed things up
633 # later via ajax we call repo_nodetree_full and fetch whole
634 # later via ajax we call repo_nodetree_full and fetch whole
634 c.file_tree = self._get_tree_at_commit(
635 c.file_tree = self._get_tree_at_commit(
635 c, c.commit.raw_id, f_path)
636 c, c.commit.raw_id, f_path)
636
637
637 except RepositoryError as e:
638 except RepositoryError as e:
638 h.flash(safe_str(h.escape(e)), category='error')
639 h.flash(safe_str(h.escape(e)), category='error')
639 raise HTTPNotFound()
640 raise HTTPNotFound()
640
641
641 if self.request.environ.get('HTTP_X_PJAX'):
642 if self.request.environ.get('HTTP_X_PJAX'):
642 html = render('rhodecode:templates/files/files_pjax.mako',
643 html = render('rhodecode:templates/files/files_pjax.mako',
643 self._get_template_context(c), self.request)
644 self._get_template_context(c), self.request)
644 else:
645 else:
645 html = render('rhodecode:templates/files/files.mako',
646 html = render('rhodecode:templates/files/files.mako',
646 self._get_template_context(c), self.request)
647 self._get_template_context(c), self.request)
647 return Response(html)
648 return Response(html)
648
649
649 @HasRepoPermissionAnyDecorator(
650 @HasRepoPermissionAnyDecorator(
650 'repository.read', 'repository.write', 'repository.admin')
651 'repository.read', 'repository.write', 'repository.admin')
651 @view_config(
652 @view_config(
652 route_name='repo_files:annotated_previous', request_method='GET',
653 route_name='repo_files:annotated_previous', request_method='GET',
653 renderer=None)
654 renderer=None)
654 def repo_files_annotated_previous(self):
655 def repo_files_annotated_previous(self):
655 self.load_default_context()
656 self.load_default_context()
656
657
657 commit_id, f_path = self._get_commit_and_path()
658 commit_id, f_path = self._get_commit_and_path()
658 commit = self._get_commit_or_redirect(commit_id)
659 commit = self._get_commit_or_redirect(commit_id)
659 prev_commit_id = commit.raw_id
660 prev_commit_id = commit.raw_id
660 line_anchor = self.request.GET.get('line_anchor')
661 line_anchor = self.request.GET.get('line_anchor')
661 is_file = False
662 is_file = False
662 try:
663 try:
663 _file = commit.get_node(f_path)
664 _file = commit.get_node(f_path)
664 is_file = _file.is_file()
665 is_file = _file.is_file()
665 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
666 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
666 pass
667 pass
667
668
668 if is_file:
669 if is_file:
669 history = commit.get_path_history(f_path)
670 history = commit.get_path_history(f_path)
670 prev_commit_id = history[1].raw_id \
671 prev_commit_id = history[1].raw_id \
671 if len(history) > 1 else prev_commit_id
672 if len(history) > 1 else prev_commit_id
672 prev_url = h.route_path(
673 prev_url = h.route_path(
673 'repo_files:annotated', repo_name=self.db_repo_name,
674 'repo_files:annotated', repo_name=self.db_repo_name,
674 commit_id=prev_commit_id, f_path=f_path,
675 commit_id=prev_commit_id, f_path=f_path,
675 _anchor='L{}'.format(line_anchor))
676 _anchor='L{}'.format(line_anchor))
676
677
677 raise HTTPFound(prev_url)
678 raise HTTPFound(prev_url)
678
679
679 @LoginRequired()
680 @LoginRequired()
680 @HasRepoPermissionAnyDecorator(
681 @HasRepoPermissionAnyDecorator(
681 'repository.read', 'repository.write', 'repository.admin')
682 'repository.read', 'repository.write', 'repository.admin')
682 @view_config(
683 @view_config(
683 route_name='repo_nodetree_full', request_method='GET',
684 route_name='repo_nodetree_full', request_method='GET',
684 renderer=None, xhr=True)
685 renderer=None, xhr=True)
685 @view_config(
686 @view_config(
686 route_name='repo_nodetree_full:default_path', request_method='GET',
687 route_name='repo_nodetree_full:default_path', request_method='GET',
687 renderer=None, xhr=True)
688 renderer=None, xhr=True)
688 def repo_nodetree_full(self):
689 def repo_nodetree_full(self):
689 """
690 """
690 Returns rendered html of file tree that contains commit date,
691 Returns rendered html of file tree that contains commit date,
691 author, commit_id for the specified combination of
692 author, commit_id for the specified combination of
692 repo, commit_id and file path
693 repo, commit_id and file path
693 """
694 """
694 c = self.load_default_context()
695 c = self.load_default_context()
695
696
696 commit_id, f_path = self._get_commit_and_path()
697 commit_id, f_path = self._get_commit_and_path()
697 commit = self._get_commit_or_redirect(commit_id)
698 commit = self._get_commit_or_redirect(commit_id)
698 try:
699 try:
699 dir_node = commit.get_node(f_path)
700 dir_node = commit.get_node(f_path)
700 except RepositoryError as e:
701 except RepositoryError as e:
701 return Response('error: {}'.format(h.escape(safe_str(e))))
702 return Response('error: {}'.format(h.escape(safe_str(e))))
702
703
703 if dir_node.is_file():
704 if dir_node.is_file():
704 return Response('')
705 return Response('')
705
706
706 c.file = dir_node
707 c.file = dir_node
707 c.commit = commit
708 c.commit = commit
708
709
709 html = self._get_tree_at_commit(
710 html = self._get_tree_at_commit(
710 c, commit.raw_id, dir_node.path, full_load=True)
711 c, commit.raw_id, dir_node.path, full_load=True)
711
712
712 return Response(html)
713 return Response(html)
713
714
714 def _get_attachement_headers(self, f_path):
715 def _get_attachement_headers(self, f_path):
715 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
716 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
716 safe_path = f_name.replace('"', '\\"')
717 safe_path = f_name.replace('"', '\\"')
717 encoded_path = urllib.quote(f_name)
718 encoded_path = urllib.quote(f_name)
718
719
719 return "attachment; " \
720 return "attachment; " \
720 "filename=\"{}\"; " \
721 "filename=\"{}\"; " \
721 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
722 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
722
723
723 @LoginRequired()
724 @LoginRequired()
724 @HasRepoPermissionAnyDecorator(
725 @HasRepoPermissionAnyDecorator(
725 'repository.read', 'repository.write', 'repository.admin')
726 'repository.read', 'repository.write', 'repository.admin')
726 @view_config(
727 @view_config(
727 route_name='repo_file_raw', request_method='GET',
728 route_name='repo_file_raw', request_method='GET',
728 renderer=None)
729 renderer=None)
729 def repo_file_raw(self):
730 def repo_file_raw(self):
730 """
731 """
731 Action for show as raw, some mimetypes are "rendered",
732 Action for show as raw, some mimetypes are "rendered",
732 those include images, icons.
733 those include images, icons.
733 """
734 """
734 c = self.load_default_context()
735 c = self.load_default_context()
735
736
736 commit_id, f_path = self._get_commit_and_path()
737 commit_id, f_path = self._get_commit_and_path()
737 commit = self._get_commit_or_redirect(commit_id)
738 commit = self._get_commit_or_redirect(commit_id)
738 file_node = self._get_filenode_or_redirect(commit, f_path)
739 file_node = self._get_filenode_or_redirect(commit, f_path)
739
740
740 raw_mimetype_mapping = {
741 raw_mimetype_mapping = {
741 # map original mimetype to a mimetype used for "show as raw"
742 # map original mimetype to a mimetype used for "show as raw"
742 # you can also provide a content-disposition to override the
743 # you can also provide a content-disposition to override the
743 # default "attachment" disposition.
744 # default "attachment" disposition.
744 # orig_type: (new_type, new_dispo)
745 # orig_type: (new_type, new_dispo)
745
746
746 # show images inline:
747 # show images inline:
747 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
748 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
748 # for example render an SVG with javascript inside or even render
749 # for example render an SVG with javascript inside or even render
749 # HTML.
750 # HTML.
750 'image/x-icon': ('image/x-icon', 'inline'),
751 'image/x-icon': ('image/x-icon', 'inline'),
751 'image/png': ('image/png', 'inline'),
752 'image/png': ('image/png', 'inline'),
752 'image/gif': ('image/gif', 'inline'),
753 'image/gif': ('image/gif', 'inline'),
753 'image/jpeg': ('image/jpeg', 'inline'),
754 'image/jpeg': ('image/jpeg', 'inline'),
754 'application/pdf': ('application/pdf', 'inline'),
755 'application/pdf': ('application/pdf', 'inline'),
755 }
756 }
756
757
757 mimetype = file_node.mimetype
758 mimetype = file_node.mimetype
758 try:
759 try:
759 mimetype, disposition = raw_mimetype_mapping[mimetype]
760 mimetype, disposition = raw_mimetype_mapping[mimetype]
760 except KeyError:
761 except KeyError:
761 # we don't know anything special about this, handle it safely
762 # we don't know anything special about this, handle it safely
762 if file_node.is_binary:
763 if file_node.is_binary:
763 # do same as download raw for binary files
764 # do same as download raw for binary files
764 mimetype, disposition = 'application/octet-stream', 'attachment'
765 mimetype, disposition = 'application/octet-stream', 'attachment'
765 else:
766 else:
766 # do not just use the original mimetype, but force text/plain,
767 # do not just use the original mimetype, but force text/plain,
767 # otherwise it would serve text/html and that might be unsafe.
768 # otherwise it would serve text/html and that might be unsafe.
768 # Note: underlying vcs library fakes text/plain mimetype if the
769 # Note: underlying vcs library fakes text/plain mimetype if the
769 # mimetype can not be determined and it thinks it is not
770 # mimetype can not be determined and it thinks it is not
770 # binary.This might lead to erroneous text display in some
771 # binary.This might lead to erroneous text display in some
771 # cases, but helps in other cases, like with text files
772 # cases, but helps in other cases, like with text files
772 # without extension.
773 # without extension.
773 mimetype, disposition = 'text/plain', 'inline'
774 mimetype, disposition = 'text/plain', 'inline'
774
775
775 if disposition == 'attachment':
776 if disposition == 'attachment':
776 disposition = self._get_attachement_headers(f_path)
777 disposition = self._get_attachement_headers(f_path)
777
778
778 def stream_node():
779 def stream_node():
779 yield file_node.raw_bytes
780 yield file_node.raw_bytes
780
781
781 response = Response(app_iter=stream_node())
782 response = Response(app_iter=stream_node())
782 response.content_disposition = disposition
783 response.content_disposition = disposition
783 response.content_type = mimetype
784 response.content_type = mimetype
784
785
785 charset = self._get_default_encoding(c)
786 charset = self._get_default_encoding(c)
786 if charset:
787 if charset:
787 response.charset = charset
788 response.charset = charset
788
789
789 return response
790 return response
790
791
791 @LoginRequired()
792 @LoginRequired()
792 @HasRepoPermissionAnyDecorator(
793 @HasRepoPermissionAnyDecorator(
793 'repository.read', 'repository.write', 'repository.admin')
794 'repository.read', 'repository.write', 'repository.admin')
794 @view_config(
795 @view_config(
795 route_name='repo_file_download', request_method='GET',
796 route_name='repo_file_download', request_method='GET',
796 renderer=None)
797 renderer=None)
797 @view_config(
798 @view_config(
798 route_name='repo_file_download:legacy', request_method='GET',
799 route_name='repo_file_download:legacy', request_method='GET',
799 renderer=None)
800 renderer=None)
800 def repo_file_download(self):
801 def repo_file_download(self):
801 c = self.load_default_context()
802 c = self.load_default_context()
802
803
803 commit_id, f_path = self._get_commit_and_path()
804 commit_id, f_path = self._get_commit_and_path()
804 commit = self._get_commit_or_redirect(commit_id)
805 commit = self._get_commit_or_redirect(commit_id)
805 file_node = self._get_filenode_or_redirect(commit, f_path)
806 file_node = self._get_filenode_or_redirect(commit, f_path)
806
807
807 if self.request.GET.get('lf'):
808 if self.request.GET.get('lf'):
808 # only if lf get flag is passed, we download this file
809 # only if lf get flag is passed, we download this file
809 # as LFS/Largefile
810 # as LFS/Largefile
810 lf_node = file_node.get_largefile_node()
811 lf_node = file_node.get_largefile_node()
811 if lf_node:
812 if lf_node:
812 # overwrite our pointer with the REAL large-file
813 # overwrite our pointer with the REAL large-file
813 file_node = lf_node
814 file_node = lf_node
814
815
815 disposition = self._get_attachement_headers(f_path)
816 disposition = self._get_attachement_headers(f_path)
816
817
817 def stream_node():
818 def stream_node():
818 yield file_node.raw_bytes
819 yield file_node.raw_bytes
819
820
820 response = Response(app_iter=stream_node())
821 response = Response(app_iter=stream_node())
821 response.content_disposition = disposition
822 response.content_disposition = disposition
822 response.content_type = file_node.mimetype
823 response.content_type = file_node.mimetype
823
824
824 charset = self._get_default_encoding(c)
825 charset = self._get_default_encoding(c)
825 if charset:
826 if charset:
826 response.charset = charset
827 response.charset = charset
827
828
828 return response
829 return response
829
830
830 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
831 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
831
832
832 cache_seconds = safe_int(
833 cache_seconds = safe_int(
833 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
834 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
834 cache_on = cache_seconds > 0
835 cache_on = cache_seconds > 0
835 log.debug(
836 log.debug(
836 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
837 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
837 'with caching: %s[TTL: %ss]' % (
838 'with caching: %s[TTL: %ss]' % (
838 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
839 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
839
840
840 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
841 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
841 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
842 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
842
843
843 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
844 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
844 condition=cache_on)
845 condition=cache_on)
845 def compute_file_search(repo_id, commit_id, f_path):
846 def compute_file_search(repo_id, commit_id, f_path):
846 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
847 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
847 repo_id, commit_id, f_path)
848 repo_id, commit_id, f_path)
848 try:
849 try:
849 _d, _f = ScmModel().get_nodes(
850 _d, _f = ScmModel().get_nodes(
850 repo_name, commit_id, f_path, flat=False)
851 repo_name, commit_id, f_path, flat=False)
851 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
852 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
852 log.exception(safe_str(e))
853 log.exception(safe_str(e))
853 h.flash(safe_str(h.escape(e)), category='error')
854 h.flash(safe_str(h.escape(e)), category='error')
854 raise HTTPFound(h.route_path(
855 raise HTTPFound(h.route_path(
855 'repo_files', repo_name=self.db_repo_name,
856 'repo_files', repo_name=self.db_repo_name,
856 commit_id='tip', f_path='/'))
857 commit_id='tip', f_path='/'))
857 return _d + _f
858 return _d + _f
858
859
859 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
860 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
860
861
861 @LoginRequired()
862 @LoginRequired()
862 @HasRepoPermissionAnyDecorator(
863 @HasRepoPermissionAnyDecorator(
863 'repository.read', 'repository.write', 'repository.admin')
864 'repository.read', 'repository.write', 'repository.admin')
864 @view_config(
865 @view_config(
865 route_name='repo_files_nodelist', request_method='GET',
866 route_name='repo_files_nodelist', request_method='GET',
866 renderer='json_ext', xhr=True)
867 renderer='json_ext', xhr=True)
867 def repo_nodelist(self):
868 def repo_nodelist(self):
868 self.load_default_context()
869 self.load_default_context()
869
870
870 commit_id, f_path = self._get_commit_and_path()
871 commit_id, f_path = self._get_commit_and_path()
871 commit = self._get_commit_or_redirect(commit_id)
872 commit = self._get_commit_or_redirect(commit_id)
872
873
873 metadata = self._get_nodelist_at_commit(
874 metadata = self._get_nodelist_at_commit(
874 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
875 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
875 return {'nodes': metadata}
876 return {'nodes': metadata}
876
877
877 def _create_references(
878 def _create_references(
878 self, branches_or_tags, symbolic_reference, f_path):
879 self, branches_or_tags, symbolic_reference, f_path):
879 items = []
880 items = []
880 for name, commit_id in branches_or_tags.items():
881 for name, commit_id in branches_or_tags.items():
881 sym_ref = symbolic_reference(commit_id, name, f_path)
882 sym_ref = symbolic_reference(commit_id, name, f_path)
882 items.append((sym_ref, name))
883 items.append((sym_ref, name))
883 return items
884 return items
884
885
885 def _symbolic_reference(self, commit_id, name, f_path):
886 def _symbolic_reference(self, commit_id, name, f_path):
886 return commit_id
887 return commit_id
887
888
888 def _symbolic_reference_svn(self, commit_id, name, f_path):
889 def _symbolic_reference_svn(self, commit_id, name, f_path):
889 new_f_path = vcspath.join(name, f_path)
890 new_f_path = vcspath.join(name, f_path)
890 return u'%s@%s' % (new_f_path, commit_id)
891 return u'%s@%s' % (new_f_path, commit_id)
891
892
892 def _get_node_history(self, commit_obj, f_path, commits=None):
893 def _get_node_history(self, commit_obj, f_path, commits=None):
893 """
894 """
894 get commit history for given node
895 get commit history for given node
895
896
896 :param commit_obj: commit to calculate history
897 :param commit_obj: commit to calculate history
897 :param f_path: path for node to calculate history for
898 :param f_path: path for node to calculate history for
898 :param commits: if passed don't calculate history and take
899 :param commits: if passed don't calculate history and take
899 commits defined in this list
900 commits defined in this list
900 """
901 """
901 _ = self.request.translate
902 _ = self.request.translate
902
903
903 # calculate history based on tip
904 # calculate history based on tip
904 tip = self.rhodecode_vcs_repo.get_commit()
905 tip = self.rhodecode_vcs_repo.get_commit()
905 if commits is None:
906 if commits is None:
906 pre_load = ["author", "branch"]
907 pre_load = ["author", "branch"]
907 try:
908 try:
908 commits = tip.get_path_history(f_path, pre_load=pre_load)
909 commits = tip.get_path_history(f_path, pre_load=pre_load)
909 except (NodeDoesNotExistError, CommitError):
910 except (NodeDoesNotExistError, CommitError):
910 # this node is not present at tip!
911 # this node is not present at tip!
911 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
912 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
912
913
913 history = []
914 history = []
914 commits_group = ([], _("Changesets"))
915 commits_group = ([], _("Changesets"))
915 for commit in commits:
916 for commit in commits:
916 branch = ' (%s)' % commit.branch if commit.branch else ''
917 branch = ' (%s)' % commit.branch if commit.branch else ''
917 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
918 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
918 commits_group[0].append((commit.raw_id, n_desc,))
919 commits_group[0].append((commit.raw_id, n_desc,))
919 history.append(commits_group)
920 history.append(commits_group)
920
921
921 symbolic_reference = self._symbolic_reference
922 symbolic_reference = self._symbolic_reference
922
923
923 if self.rhodecode_vcs_repo.alias == 'svn':
924 if self.rhodecode_vcs_repo.alias == 'svn':
924 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
925 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
925 f_path, self.rhodecode_vcs_repo)
926 f_path, self.rhodecode_vcs_repo)
926 if adjusted_f_path != f_path:
927 if adjusted_f_path != f_path:
927 log.debug(
928 log.debug(
928 'Recognized svn tag or branch in file "%s", using svn '
929 'Recognized svn tag or branch in file "%s", using svn '
929 'specific symbolic references', f_path)
930 'specific symbolic references', f_path)
930 f_path = adjusted_f_path
931 f_path = adjusted_f_path
931 symbolic_reference = self._symbolic_reference_svn
932 symbolic_reference = self._symbolic_reference_svn
932
933
933 branches = self._create_references(
934 branches = self._create_references(
934 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
935 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
935 branches_group = (branches, _("Branches"))
936 branches_group = (branches, _("Branches"))
936
937
937 tags = self._create_references(
938 tags = self._create_references(
938 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
939 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
939 tags_group = (tags, _("Tags"))
940 tags_group = (tags, _("Tags"))
940
941
941 history.append(branches_group)
942 history.append(branches_group)
942 history.append(tags_group)
943 history.append(tags_group)
943
944
944 return history, commits
945 return history, commits
945
946
946 @LoginRequired()
947 @LoginRequired()
947 @HasRepoPermissionAnyDecorator(
948 @HasRepoPermissionAnyDecorator(
948 'repository.read', 'repository.write', 'repository.admin')
949 'repository.read', 'repository.write', 'repository.admin')
949 @view_config(
950 @view_config(
950 route_name='repo_file_history', request_method='GET',
951 route_name='repo_file_history', request_method='GET',
951 renderer='json_ext')
952 renderer='json_ext')
952 def repo_file_history(self):
953 def repo_file_history(self):
953 self.load_default_context()
954 self.load_default_context()
954
955
955 commit_id, f_path = self._get_commit_and_path()
956 commit_id, f_path = self._get_commit_and_path()
956 commit = self._get_commit_or_redirect(commit_id)
957 commit = self._get_commit_or_redirect(commit_id)
957 file_node = self._get_filenode_or_redirect(commit, f_path)
958 file_node = self._get_filenode_or_redirect(commit, f_path)
958
959
959 if file_node.is_file():
960 if file_node.is_file():
960 file_history, _hist = self._get_node_history(commit, f_path)
961 file_history, _hist = self._get_node_history(commit, f_path)
961
962
962 res = []
963 res = []
963 for obj in file_history:
964 for obj in file_history:
964 res.append({
965 res.append({
965 'text': obj[1],
966 'text': obj[1],
966 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
967 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
967 })
968 })
968
969
969 data = {
970 data = {
970 'more': False,
971 'more': False,
971 'results': res
972 'results': res
972 }
973 }
973 return data
974 return data
974
975
975 log.warning('Cannot fetch history for directory')
976 log.warning('Cannot fetch history for directory')
976 raise HTTPBadRequest()
977 raise HTTPBadRequest()
977
978
978 @LoginRequired()
979 @LoginRequired()
979 @HasRepoPermissionAnyDecorator(
980 @HasRepoPermissionAnyDecorator(
980 'repository.read', 'repository.write', 'repository.admin')
981 'repository.read', 'repository.write', 'repository.admin')
981 @view_config(
982 @view_config(
982 route_name='repo_file_authors', request_method='GET',
983 route_name='repo_file_authors', request_method='GET',
983 renderer='rhodecode:templates/files/file_authors_box.mako')
984 renderer='rhodecode:templates/files/file_authors_box.mako')
984 def repo_file_authors(self):
985 def repo_file_authors(self):
985 c = self.load_default_context()
986 c = self.load_default_context()
986
987
987 commit_id, f_path = self._get_commit_and_path()
988 commit_id, f_path = self._get_commit_and_path()
988 commit = self._get_commit_or_redirect(commit_id)
989 commit = self._get_commit_or_redirect(commit_id)
989 file_node = self._get_filenode_or_redirect(commit, f_path)
990 file_node = self._get_filenode_or_redirect(commit, f_path)
990
991
991 if not file_node.is_file():
992 if not file_node.is_file():
992 raise HTTPBadRequest()
993 raise HTTPBadRequest()
993
994
994 c.file_last_commit = file_node.last_commit
995 c.file_last_commit = file_node.last_commit
995 if self.request.GET.get('annotate') == '1':
996 if self.request.GET.get('annotate') == '1':
996 # use _hist from annotation if annotation mode is on
997 # use _hist from annotation if annotation mode is on
997 commit_ids = set(x[1] for x in file_node.annotate)
998 commit_ids = set(x[1] for x in file_node.annotate)
998 _hist = (
999 _hist = (
999 self.rhodecode_vcs_repo.get_commit(commit_id)
1000 self.rhodecode_vcs_repo.get_commit(commit_id)
1000 for commit_id in commit_ids)
1001 for commit_id in commit_ids)
1001 else:
1002 else:
1002 _f_history, _hist = self._get_node_history(commit, f_path)
1003 _f_history, _hist = self._get_node_history(commit, f_path)
1003 c.file_author = False
1004 c.file_author = False
1004
1005
1005 unique = collections.OrderedDict()
1006 unique = collections.OrderedDict()
1006 for commit in _hist:
1007 for commit in _hist:
1007 author = commit.author
1008 author = commit.author
1008 if author not in unique:
1009 if author not in unique:
1009 unique[commit.author] = [
1010 unique[commit.author] = [
1010 h.email(author),
1011 h.email(author),
1011 h.person(author, 'username_or_name_or_email'),
1012 h.person(author, 'username_or_name_or_email'),
1012 1 # counter
1013 1 # counter
1013 ]
1014 ]
1014
1015
1015 else:
1016 else:
1016 # increase counter
1017 # increase counter
1017 unique[commit.author][2] += 1
1018 unique[commit.author][2] += 1
1018
1019
1019 c.authors = [val for val in unique.values()]
1020 c.authors = [val for val in unique.values()]
1020
1021
1021 return self._get_template_context(c)
1022 return self._get_template_context(c)
1022
1023
1023 @LoginRequired()
1024 @LoginRequired()
1024 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1025 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1025 @view_config(
1026 @view_config(
1026 route_name='repo_files_remove_file', request_method='GET',
1027 route_name='repo_files_remove_file', request_method='GET',
1027 renderer='rhodecode:templates/files/files_delete.mako')
1028 renderer='rhodecode:templates/files/files_delete.mako')
1028 def repo_files_remove_file(self):
1029 def repo_files_remove_file(self):
1029 _ = self.request.translate
1030 _ = self.request.translate
1030 c = self.load_default_context()
1031 c = self.load_default_context()
1031 commit_id, f_path = self._get_commit_and_path()
1032 commit_id, f_path = self._get_commit_and_path()
1032
1033
1033 self._ensure_not_locked()
1034 self._ensure_not_locked()
1034 _branch_name, _sha_commit_id, is_head = \
1035 _branch_name, _sha_commit_id, is_head = \
1035 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1036 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1036
1037
1037 if not is_head:
1038 if not is_head:
1038 h.flash(_('You can only delete files with commit '
1039 h.flash(_('You can only delete files with commit '
1039 'being a valid branch head.'), category='warning')
1040 'being a valid branch head.'), category='warning')
1040 raise HTTPFound(
1041 raise HTTPFound(
1041 h.route_path('repo_files',
1042 h.route_path('repo_files',
1042 repo_name=self.db_repo_name, commit_id='tip',
1043 repo_name=self.db_repo_name, commit_id='tip',
1043 f_path=f_path))
1044 f_path=f_path))
1044
1045
1045 self.check_branch_permission(_branch_name)
1046 self.check_branch_permission(_branch_name)
1046 c.commit = self._get_commit_or_redirect(commit_id)
1047 c.commit = self._get_commit_or_redirect(commit_id)
1047 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1048 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1048
1049
1049 c.default_message = _(
1050 c.default_message = _(
1050 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1051 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1051 c.f_path = f_path
1052 c.f_path = f_path
1052
1053
1053 return self._get_template_context(c)
1054 return self._get_template_context(c)
1054
1055
1055 @LoginRequired()
1056 @LoginRequired()
1056 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1057 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1057 @CSRFRequired()
1058 @CSRFRequired()
1058 @view_config(
1059 @view_config(
1059 route_name='repo_files_delete_file', request_method='POST',
1060 route_name='repo_files_delete_file', request_method='POST',
1060 renderer=None)
1061 renderer=None)
1061 def repo_files_delete_file(self):
1062 def repo_files_delete_file(self):
1062 _ = self.request.translate
1063 _ = self.request.translate
1063
1064
1064 c = self.load_default_context()
1065 c = self.load_default_context()
1065 commit_id, f_path = self._get_commit_and_path()
1066 commit_id, f_path = self._get_commit_and_path()
1066
1067
1067 self._ensure_not_locked()
1068 self._ensure_not_locked()
1068 _branch_name, _sha_commit_id, is_head = \
1069 _branch_name, _sha_commit_id, is_head = \
1069 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1070 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1070
1071
1071 if not is_head:
1072 if not is_head:
1072 h.flash(_('You can only delete files with commit '
1073 h.flash(_('You can only delete files with commit '
1073 'being a valid branch head.'), category='warning')
1074 'being a valid branch head.'), category='warning')
1074 raise HTTPFound(
1075 raise HTTPFound(
1075 h.route_path('repo_files',
1076 h.route_path('repo_files',
1076 repo_name=self.db_repo_name, commit_id='tip',
1077 repo_name=self.db_repo_name, commit_id='tip',
1077 f_path=f_path))
1078 f_path=f_path))
1078 self.check_branch_permission(_branch_name)
1079 self.check_branch_permission(_branch_name)
1079
1080
1080 c.commit = self._get_commit_or_redirect(commit_id)
1081 c.commit = self._get_commit_or_redirect(commit_id)
1081 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1082 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1082
1083
1083 c.default_message = _(
1084 c.default_message = _(
1084 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1085 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1085 c.f_path = f_path
1086 c.f_path = f_path
1086 node_path = f_path
1087 node_path = f_path
1087 author = self._rhodecode_db_user.full_contact
1088 author = self._rhodecode_db_user.full_contact
1088 message = self.request.POST.get('message') or c.default_message
1089 message = self.request.POST.get('message') or c.default_message
1089 try:
1090 try:
1090 nodes = {
1091 nodes = {
1091 node_path: {
1092 node_path: {
1092 'content': ''
1093 'content': ''
1093 }
1094 }
1094 }
1095 }
1095 ScmModel().delete_nodes(
1096 ScmModel().delete_nodes(
1096 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1097 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1097 message=message,
1098 message=message,
1098 nodes=nodes,
1099 nodes=nodes,
1099 parent_commit=c.commit,
1100 parent_commit=c.commit,
1100 author=author,
1101 author=author,
1101 )
1102 )
1102
1103
1103 h.flash(
1104 h.flash(
1104 _('Successfully deleted file `{}`').format(
1105 _('Successfully deleted file `{}`').format(
1105 h.escape(f_path)), category='success')
1106 h.escape(f_path)), category='success')
1106 except Exception:
1107 except Exception:
1107 log.exception('Error during commit operation')
1108 log.exception('Error during commit operation')
1108 h.flash(_('Error occurred during commit'), category='error')
1109 h.flash(_('Error occurred during commit'), category='error')
1109 raise HTTPFound(
1110 raise HTTPFound(
1110 h.route_path('repo_commit', repo_name=self.db_repo_name,
1111 h.route_path('repo_commit', repo_name=self.db_repo_name,
1111 commit_id='tip'))
1112 commit_id='tip'))
1112
1113
1113 @LoginRequired()
1114 @LoginRequired()
1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1115 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1115 @view_config(
1116 @view_config(
1116 route_name='repo_files_edit_file', request_method='GET',
1117 route_name='repo_files_edit_file', request_method='GET',
1117 renderer='rhodecode:templates/files/files_edit.mako')
1118 renderer='rhodecode:templates/files/files_edit.mako')
1118 def repo_files_edit_file(self):
1119 def repo_files_edit_file(self):
1119 _ = self.request.translate
1120 _ = self.request.translate
1120 c = self.load_default_context()
1121 c = self.load_default_context()
1121 commit_id, f_path = self._get_commit_and_path()
1122 commit_id, f_path = self._get_commit_and_path()
1122
1123
1123 self._ensure_not_locked()
1124 self._ensure_not_locked()
1124 _branch_name, _sha_commit_id, is_head = \
1125 _branch_name, _sha_commit_id, is_head = \
1125 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1126 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1126
1127
1127 if not is_head:
1128 if not is_head:
1128 h.flash(_('You can only edit files with commit '
1129 h.flash(_('You can only edit files with commit '
1129 'being a valid branch head.'), category='warning')
1130 'being a valid branch head.'), category='warning')
1130 raise HTTPFound(
1131 raise HTTPFound(
1131 h.route_path('repo_files',
1132 h.route_path('repo_files',
1132 repo_name=self.db_repo_name, commit_id='tip',
1133 repo_name=self.db_repo_name, commit_id='tip',
1133 f_path=f_path))
1134 f_path=f_path))
1134 self.check_branch_permission(_branch_name)
1135 self.check_branch_permission(_branch_name)
1135
1136
1136 c.commit = self._get_commit_or_redirect(commit_id)
1137 c.commit = self._get_commit_or_redirect(commit_id)
1137 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1138 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1138
1139
1139 if c.file.is_binary:
1140 if c.file.is_binary:
1140 files_url = h.route_path(
1141 files_url = h.route_path(
1141 'repo_files',
1142 'repo_files',
1142 repo_name=self.db_repo_name,
1143 repo_name=self.db_repo_name,
1143 commit_id=c.commit.raw_id, f_path=f_path)
1144 commit_id=c.commit.raw_id, f_path=f_path)
1144 raise HTTPFound(files_url)
1145 raise HTTPFound(files_url)
1145
1146
1146 c.default_message = _(
1147 c.default_message = _(
1147 'Edited file {} via RhodeCode Enterprise').format(f_path)
1148 'Edited file {} via RhodeCode Enterprise').format(f_path)
1148 c.f_path = f_path
1149 c.f_path = f_path
1149
1150
1150 return self._get_template_context(c)
1151 return self._get_template_context(c)
1151
1152
1152 @LoginRequired()
1153 @LoginRequired()
1153 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1154 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1154 @CSRFRequired()
1155 @CSRFRequired()
1155 @view_config(
1156 @view_config(
1156 route_name='repo_files_update_file', request_method='POST',
1157 route_name='repo_files_update_file', request_method='POST',
1157 renderer=None)
1158 renderer=None)
1158 def repo_files_update_file(self):
1159 def repo_files_update_file(self):
1159 _ = self.request.translate
1160 _ = self.request.translate
1160 c = self.load_default_context()
1161 c = self.load_default_context()
1161 commit_id, f_path = self._get_commit_and_path()
1162 commit_id, f_path = self._get_commit_and_path()
1162
1163
1163 self._ensure_not_locked()
1164 self._ensure_not_locked()
1164 _branch_name, _sha_commit_id, is_head = \
1165 _branch_name, _sha_commit_id, is_head = \
1165 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1166 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1166
1167
1167 if not is_head:
1168 if not is_head:
1168 h.flash(_('You can only edit files with commit '
1169 h.flash(_('You can only edit files with commit '
1169 'being a valid branch head.'), category='warning')
1170 'being a valid branch head.'), category='warning')
1170 raise HTTPFound(
1171 raise HTTPFound(
1171 h.route_path('repo_files',
1172 h.route_path('repo_files',
1172 repo_name=self.db_repo_name, commit_id='tip',
1173 repo_name=self.db_repo_name, commit_id='tip',
1173 f_path=f_path))
1174 f_path=f_path))
1174
1175
1175 self.check_branch_permission(_branch_name)
1176 self.check_branch_permission(_branch_name)
1176
1177
1177 c.commit = self._get_commit_or_redirect(commit_id)
1178 c.commit = self._get_commit_or_redirect(commit_id)
1178 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1179 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1179
1180
1180 if c.file.is_binary:
1181 if c.file.is_binary:
1181 raise HTTPFound(
1182 raise HTTPFound(
1182 h.route_path('repo_files',
1183 h.route_path('repo_files',
1183 repo_name=self.db_repo_name,
1184 repo_name=self.db_repo_name,
1184 commit_id=c.commit.raw_id,
1185 commit_id=c.commit.raw_id,
1185 f_path=f_path))
1186 f_path=f_path))
1186
1187
1187 c.default_message = _(
1188 c.default_message = _(
1188 'Edited file {} via RhodeCode Enterprise').format(f_path)
1189 'Edited file {} via RhodeCode Enterprise').format(f_path)
1189 c.f_path = f_path
1190 c.f_path = f_path
1190 old_content = c.file.content
1191 old_content = c.file.content
1191 sl = old_content.splitlines(1)
1192 sl = old_content.splitlines(1)
1192 first_line = sl[0] if sl else ''
1193 first_line = sl[0] if sl else ''
1193
1194
1194 r_post = self.request.POST
1195 r_post = self.request.POST
1195 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1196 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1196 line_ending_mode = detect_mode(first_line, 0)
1197 line_ending_mode = detect_mode(first_line, 0)
1197 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1198 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1198
1199
1199 message = r_post.get('message') or c.default_message
1200 message = r_post.get('message') or c.default_message
1200 org_f_path = c.file.unicode_path
1201 org_f_path = c.file.unicode_path
1201 filename = r_post['filename']
1202 filename = r_post['filename']
1202 org_filename = c.file.name
1203 org_filename = c.file.name
1203
1204
1204 if content == old_content and filename == org_filename:
1205 if content == old_content and filename == org_filename:
1205 h.flash(_('No changes'), category='warning')
1206 h.flash(_('No changes'), category='warning')
1206 raise HTTPFound(
1207 raise HTTPFound(
1207 h.route_path('repo_commit', repo_name=self.db_repo_name,
1208 h.route_path('repo_commit', repo_name=self.db_repo_name,
1208 commit_id='tip'))
1209 commit_id='tip'))
1209 try:
1210 try:
1210 mapping = {
1211 mapping = {
1211 org_f_path: {
1212 org_f_path: {
1212 'org_filename': org_f_path,
1213 'org_filename': org_f_path,
1213 'filename': os.path.join(c.file.dir_path, filename),
1214 'filename': os.path.join(c.file.dir_path, filename),
1214 'content': content,
1215 'content': content,
1215 'lexer': '',
1216 'lexer': '',
1216 'op': 'mod',
1217 'op': 'mod',
1217 'mode': c.file.mode
1218 'mode': c.file.mode
1218 }
1219 }
1219 }
1220 }
1220
1221
1221 ScmModel().update_nodes(
1222 ScmModel().update_nodes(
1222 user=self._rhodecode_db_user.user_id,
1223 user=self._rhodecode_db_user.user_id,
1223 repo=self.db_repo,
1224 repo=self.db_repo,
1224 message=message,
1225 message=message,
1225 nodes=mapping,
1226 nodes=mapping,
1226 parent_commit=c.commit,
1227 parent_commit=c.commit,
1227 )
1228 )
1228
1229
1229 h.flash(
1230 h.flash(
1230 _('Successfully committed changes to file `{}`').format(
1231 _('Successfully committed changes to file `{}`').format(
1231 h.escape(f_path)), category='success')
1232 h.escape(f_path)), category='success')
1232 except Exception:
1233 except Exception:
1233 log.exception('Error occurred during commit')
1234 log.exception('Error occurred during commit')
1234 h.flash(_('Error occurred during commit'), category='error')
1235 h.flash(_('Error occurred during commit'), category='error')
1235 raise HTTPFound(
1236 raise HTTPFound(
1236 h.route_path('repo_commit', repo_name=self.db_repo_name,
1237 h.route_path('repo_commit', repo_name=self.db_repo_name,
1237 commit_id='tip'))
1238 commit_id='tip'))
1238
1239
1239 @LoginRequired()
1240 @LoginRequired()
1240 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1241 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1241 @view_config(
1242 @view_config(
1242 route_name='repo_files_add_file', request_method='GET',
1243 route_name='repo_files_add_file', request_method='GET',
1243 renderer='rhodecode:templates/files/files_add.mako')
1244 renderer='rhodecode:templates/files/files_add.mako')
1244 def repo_files_add_file(self):
1245 def repo_files_add_file(self):
1245 _ = self.request.translate
1246 _ = self.request.translate
1246 c = self.load_default_context()
1247 c = self.load_default_context()
1247 commit_id, f_path = self._get_commit_and_path()
1248 commit_id, f_path = self._get_commit_and_path()
1248
1249
1249 self._ensure_not_locked()
1250 self._ensure_not_locked()
1250
1251
1251 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1252 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1252 if c.commit is None:
1253 if c.commit is None:
1253 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1254 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1254 c.default_message = (_('Added file via RhodeCode Enterprise'))
1255 c.default_message = (_('Added file via RhodeCode Enterprise'))
1255 c.f_path = f_path.lstrip('/') # ensure not relative path
1256 c.f_path = f_path.lstrip('/') # ensure not relative path
1256
1257
1257 if self.rhodecode_vcs_repo.is_empty:
1258 if self.rhodecode_vcs_repo.is_empty:
1258 # for empty repository we cannot check for current branch, we rely on
1259 # for empty repository we cannot check for current branch, we rely on
1259 # c.commit.branch instead
1260 # c.commit.branch instead
1260 _branch_name = c.commit.branch
1261 _branch_name = c.commit.branch
1261 is_head = True
1262 is_head = True
1262 else:
1263 else:
1263 _branch_name, _sha_commit_id, is_head = \
1264 _branch_name, _sha_commit_id, is_head = \
1264 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1265 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1265
1266
1266 if not is_head:
1267 if not is_head:
1267 h.flash(_('You can only add files with commit '
1268 h.flash(_('You can only add files with commit '
1268 'being a valid branch head.'), category='warning')
1269 'being a valid branch head.'), category='warning')
1269 raise HTTPFound(
1270 raise HTTPFound(
1270 h.route_path('repo_files',
1271 h.route_path('repo_files',
1271 repo_name=self.db_repo_name, commit_id='tip',
1272 repo_name=self.db_repo_name, commit_id='tip',
1272 f_path=f_path))
1273 f_path=f_path))
1273
1274
1274 self.check_branch_permission(_branch_name)
1275 self.check_branch_permission(_branch_name)
1275
1276
1276 return self._get_template_context(c)
1277 return self._get_template_context(c)
1277
1278
1278 @LoginRequired()
1279 @LoginRequired()
1279 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1280 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1280 @CSRFRequired()
1281 @CSRFRequired()
1281 @view_config(
1282 @view_config(
1282 route_name='repo_files_create_file', request_method='POST',
1283 route_name='repo_files_create_file', request_method='POST',
1283 renderer=None)
1284 renderer=None)
1284 def repo_files_create_file(self):
1285 def repo_files_create_file(self):
1285 _ = self.request.translate
1286 _ = self.request.translate
1286 c = self.load_default_context()
1287 c = self.load_default_context()
1287 commit_id, f_path = self._get_commit_and_path()
1288 commit_id, f_path = self._get_commit_and_path()
1288
1289
1289 self._ensure_not_locked()
1290 self._ensure_not_locked()
1290
1291
1291 r_post = self.request.POST
1292 r_post = self.request.POST
1292
1293
1293 c.commit = self._get_commit_or_redirect(
1294 c.commit = self._get_commit_or_redirect(
1294 commit_id, redirect_after=False)
1295 commit_id, redirect_after=False)
1295 if c.commit is None:
1296 if c.commit is None:
1296 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1297 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1297
1298
1298 if self.rhodecode_vcs_repo.is_empty:
1299 if self.rhodecode_vcs_repo.is_empty:
1299 # for empty repository we cannot check for current branch, we rely on
1300 # for empty repository we cannot check for current branch, we rely on
1300 # c.commit.branch instead
1301 # c.commit.branch instead
1301 _branch_name = c.commit.branch
1302 _branch_name = c.commit.branch
1302 is_head = True
1303 is_head = True
1303 else:
1304 else:
1304 _branch_name, _sha_commit_id, is_head = \
1305 _branch_name, _sha_commit_id, is_head = \
1305 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1306 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1306
1307
1307 if not is_head:
1308 if not is_head:
1308 h.flash(_('You can only add files with commit '
1309 h.flash(_('You can only add files with commit '
1309 'being a valid branch head.'), category='warning')
1310 'being a valid branch head.'), category='warning')
1310 raise HTTPFound(
1311 raise HTTPFound(
1311 h.route_path('repo_files',
1312 h.route_path('repo_files',
1312 repo_name=self.db_repo_name, commit_id='tip',
1313 repo_name=self.db_repo_name, commit_id='tip',
1313 f_path=f_path))
1314 f_path=f_path))
1314
1315
1315 self.check_branch_permission(_branch_name)
1316 self.check_branch_permission(_branch_name)
1316
1317
1317 c.default_message = (_('Added file via RhodeCode Enterprise'))
1318 c.default_message = (_('Added file via RhodeCode Enterprise'))
1318 c.f_path = f_path
1319 c.f_path = f_path
1319 unix_mode = 0
1320 unix_mode = 0
1320 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1321 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1321
1322
1322 message = r_post.get('message') or c.default_message
1323 message = r_post.get('message') or c.default_message
1323 filename = r_post.get('filename')
1324 filename = r_post.get('filename')
1324 location = r_post.get('location', '') # dir location
1325 location = r_post.get('location', '') # dir location
1325 file_obj = r_post.get('upload_file', None)
1326 file_obj = r_post.get('upload_file', None)
1326
1327
1327 if file_obj is not None and hasattr(file_obj, 'filename'):
1328 if file_obj is not None and hasattr(file_obj, 'filename'):
1328 filename = r_post.get('filename_upload')
1329 filename = r_post.get('filename_upload')
1329 content = file_obj.file
1330 content = file_obj.file
1330
1331
1331 if hasattr(content, 'file'):
1332 if hasattr(content, 'file'):
1332 # non posix systems store real file under file attr
1333 # non posix systems store real file under file attr
1333 content = content.file
1334 content = content.file
1334
1335
1335 if self.rhodecode_vcs_repo.is_empty:
1336 if self.rhodecode_vcs_repo.is_empty:
1336 default_redirect_url = h.route_path(
1337 default_redirect_url = h.route_path(
1337 'repo_summary', repo_name=self.db_repo_name)
1338 'repo_summary', repo_name=self.db_repo_name)
1338 else:
1339 else:
1339 default_redirect_url = h.route_path(
1340 default_redirect_url = h.route_path(
1340 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1341 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1341
1342
1342 # If there's no commit, redirect to repo summary
1343 # If there's no commit, redirect to repo summary
1343 if type(c.commit) is EmptyCommit:
1344 if type(c.commit) is EmptyCommit:
1344 redirect_url = h.route_path(
1345 redirect_url = h.route_path(
1345 'repo_summary', repo_name=self.db_repo_name)
1346 'repo_summary', repo_name=self.db_repo_name)
1346 else:
1347 else:
1347 redirect_url = default_redirect_url
1348 redirect_url = default_redirect_url
1348
1349
1349 if not filename:
1350 if not filename:
1350 h.flash(_('No filename'), category='warning')
1351 h.flash(_('No filename'), category='warning')
1351 raise HTTPFound(redirect_url)
1352 raise HTTPFound(redirect_url)
1352
1353
1353 # extract the location from filename,
1354 # extract the location from filename,
1354 # allows using foo/bar.txt syntax to create subdirectories
1355 # allows using foo/bar.txt syntax to create subdirectories
1355 subdir_loc = filename.rsplit('/', 1)
1356 subdir_loc = filename.rsplit('/', 1)
1356 if len(subdir_loc) == 2:
1357 if len(subdir_loc) == 2:
1357 location = os.path.join(location, subdir_loc[0])
1358 location = os.path.join(location, subdir_loc[0])
1358
1359
1359 # strip all crap out of file, just leave the basename
1360 # strip all crap out of file, just leave the basename
1360 filename = os.path.basename(filename)
1361 filename = os.path.basename(filename)
1361 node_path = os.path.join(location, filename)
1362 node_path = os.path.join(location, filename)
1362 author = self._rhodecode_db_user.full_contact
1363 author = self._rhodecode_db_user.full_contact
1363
1364
1364 try:
1365 try:
1365 nodes = {
1366 nodes = {
1366 node_path: {
1367 node_path: {
1367 'content': content
1368 'content': content
1368 }
1369 }
1369 }
1370 }
1370 ScmModel().create_nodes(
1371 ScmModel().create_nodes(
1371 user=self._rhodecode_db_user.user_id,
1372 user=self._rhodecode_db_user.user_id,
1372 repo=self.db_repo,
1373 repo=self.db_repo,
1373 message=message,
1374 message=message,
1374 nodes=nodes,
1375 nodes=nodes,
1375 parent_commit=c.commit,
1376 parent_commit=c.commit,
1376 author=author,
1377 author=author,
1377 )
1378 )
1378
1379
1379 h.flash(
1380 h.flash(
1380 _('Successfully committed new file `{}`').format(
1381 _('Successfully committed new file `{}`').format(
1381 h.escape(node_path)), category='success')
1382 h.escape(node_path)), category='success')
1382 except NonRelativePathError:
1383 except NonRelativePathError:
1383 log.exception('Non Relative path found')
1384 log.exception('Non Relative path found')
1384 h.flash(_(
1385 h.flash(_(
1385 'The location specified must be a relative path and must not '
1386 'The location specified must be a relative path and must not '
1386 'contain .. in the path'), category='warning')
1387 'contain .. in the path'), category='warning')
1387 raise HTTPFound(default_redirect_url)
1388 raise HTTPFound(default_redirect_url)
1388 except (NodeError, NodeAlreadyExistsError) as e:
1389 except (NodeError, NodeAlreadyExistsError) as e:
1389 h.flash(_(h.escape(e)), category='error')
1390 h.flash(_(h.escape(e)), category='error')
1390 except Exception:
1391 except Exception:
1391 log.exception('Error occurred during commit')
1392 log.exception('Error occurred during commit')
1392 h.flash(_('Error occurred during commit'), category='error')
1393 h.flash(_('Error occurred during commit'), category='error')
1393
1394
1394 raise HTTPFound(default_redirect_url)
1395 raise HTTPFound(default_redirect_url)
@@ -1,1471 +1,1464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, source=source, opened_by=opened_by,
79 repo_name, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, source=source, statuses=statuses,
83 repo_name, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, source=source, opened_by=opened_by,
87 repo_name, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, source=source, opened_by=opened_by,
96 repo_name, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, source=source, statuses=statuses,
100 repo_name, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.target_repo.repo_name),
112 'name_raw': pr.pull_request_id,
112 'name_raw': pr.pull_request_id,
113 'status': _render('pullrequest_status',
113 'status': _render('pullrequest_status',
114 pr.calculated_review_status()),
114 pr.calculated_review_status()),
115 'title': _render(
115 'title': _render(
116 'pullrequest_title', pr.title, pr.description),
116 'pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'author': _render('pullrequest_author',
124 'author': _render('pullrequest_author',
125 pr.author.full_contact, ),
125 pr.author.full_contact, ),
126 'author_raw': pr.author.full_name,
126 'author_raw': pr.author.full_name,
127 'comments': _render('pullrequest_comments', len(comments)),
127 'comments': _render('pullrequest_comments', len(comments)),
128 'comments_raw': len(comments),
128 'comments_raw': len(comments),
129 'closed': pr.is_closed(),
129 'closed': pr.is_closed(),
130 })
130 })
131
131
132 data = ({
132 data = ({
133 'draw': draw,
133 'draw': draw,
134 'data': data,
134 'data': data,
135 'recordsTotal': pull_requests_total_count,
135 'recordsTotal': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
137 })
137 })
138 return data
138 return data
139
139
140 def get_recache_flag(self):
141 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
142 flag_val = self.request.GET.get(flag_name)
143 if str2bool(flag_val):
144 return True
145 return False
146
147 @LoginRequired()
140 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
141 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
142 'repository.read', 'repository.write', 'repository.admin')
150 @view_config(
143 @view_config(
151 route_name='pullrequest_show_all', request_method='GET',
144 route_name='pullrequest_show_all', request_method='GET',
152 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
153 def pull_request_list(self):
146 def pull_request_list(self):
154 c = self.load_default_context()
147 c = self.load_default_context()
155
148
156 req_get = self.request.GET
149 req_get = self.request.GET
157 c.source = str2bool(req_get.get('source'))
150 c.source = str2bool(req_get.get('source'))
158 c.closed = str2bool(req_get.get('closed'))
151 c.closed = str2bool(req_get.get('closed'))
159 c.my = str2bool(req_get.get('my'))
152 c.my = str2bool(req_get.get('my'))
160 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
161 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
162
155
163 c.active = 'open'
156 c.active = 'open'
164 if c.my:
157 if c.my:
165 c.active = 'my'
158 c.active = 'my'
166 if c.closed:
159 if c.closed:
167 c.active = 'closed'
160 c.active = 'closed'
168 if c.awaiting_review and not c.source:
161 if c.awaiting_review and not c.source:
169 c.active = 'awaiting'
162 c.active = 'awaiting'
170 if c.source and not c.awaiting_review:
163 if c.source and not c.awaiting_review:
171 c.active = 'source'
164 c.active = 'source'
172 if c.awaiting_my_review:
165 if c.awaiting_my_review:
173 c.active = 'awaiting_my'
166 c.active = 'awaiting_my'
174
167
175 return self._get_template_context(c)
168 return self._get_template_context(c)
176
169
177 @LoginRequired()
170 @LoginRequired()
178 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
179 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
180 @view_config(
173 @view_config(
181 route_name='pullrequest_show_all_data', request_method='GET',
174 route_name='pullrequest_show_all_data', request_method='GET',
182 renderer='json_ext', xhr=True)
175 renderer='json_ext', xhr=True)
183 def pull_request_list_data(self):
176 def pull_request_list_data(self):
184 self.load_default_context()
177 self.load_default_context()
185
178
186 # additional filters
179 # additional filters
187 req_get = self.request.GET
180 req_get = self.request.GET
188 source = str2bool(req_get.get('source'))
181 source = str2bool(req_get.get('source'))
189 closed = str2bool(req_get.get('closed'))
182 closed = str2bool(req_get.get('closed'))
190 my = str2bool(req_get.get('my'))
183 my = str2bool(req_get.get('my'))
191 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193
186
194 filter_type = 'awaiting_review' if awaiting_review \
187 filter_type = 'awaiting_review' if awaiting_review \
195 else 'awaiting_my_review' if awaiting_my_review \
188 else 'awaiting_my_review' if awaiting_my_review \
196 else None
189 else None
197
190
198 opened_by = None
191 opened_by = None
199 if my:
192 if my:
200 opened_by = [self._rhodecode_user.user_id]
193 opened_by = [self._rhodecode_user.user_id]
201
194
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if closed:
196 if closed:
204 statuses = [PullRequest.STATUS_CLOSED]
197 statuses = [PullRequest.STATUS_CLOSED]
205
198
206 data = self._get_pull_requests_list(
199 data = self._get_pull_requests_list(
207 repo_name=self.db_repo_name, source=source,
200 repo_name=self.db_repo_name, source=source,
208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209
202
210 return data
203 return data
211
204
212 def _is_diff_cache_enabled(self, target_repo):
205 def _is_diff_cache_enabled(self, target_repo):
213 caching_enabled = self._get_general_setting(
206 caching_enabled = self._get_general_setting(
214 target_repo, 'rhodecode_diff_cache')
207 target_repo, 'rhodecode_diff_cache')
215 log.debug('Diff caching enabled: %s', caching_enabled)
208 log.debug('Diff caching enabled: %s', caching_enabled)
216 return caching_enabled
209 return caching_enabled
217
210
218 def _get_diffset(self, source_repo_name, source_repo,
211 def _get_diffset(self, source_repo_name, source_repo,
219 source_ref_id, target_ref_id,
212 source_ref_id, target_ref_id,
220 target_commit, source_commit, diff_limit, file_limit,
213 target_commit, source_commit, diff_limit, file_limit,
221 fulldiff, hide_whitespace_changes, diff_context):
214 fulldiff, hide_whitespace_changes, diff_context):
222
215
223 vcs_diff = PullRequestModel().get_diff(
216 vcs_diff = PullRequestModel().get_diff(
224 source_repo, source_ref_id, target_ref_id,
217 source_repo, source_ref_id, target_ref_id,
225 hide_whitespace_changes, diff_context)
218 hide_whitespace_changes, diff_context)
226
219
227 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
222 file_limit=file_limit, show_full_diff=fulldiff)
230
223
231 _parsed = diff_processor.prepare()
224 _parsed = diff_processor.prepare()
232
225
233 diffset = codeblocks.DiffSet(
226 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
227 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
228 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
231 )
239 diffset = self.path_filter.render_patchset_filtered(
232 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
234
242 return diffset
235 return diffset
243
236
244 def _get_range_diffset(self, source_scm, source_repo,
237 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
238 commit1, commit2, diff_limit, file_limit,
246 fulldiff, hide_whitespace_changes, diff_context):
239 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
240 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
241 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
242 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
243 context=diff_context)
251
244
252 diff_processor = diffs.DiffProcessor(
245 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
246 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
247 file_limit=file_limit, show_full_diff=fulldiff)
255
248
256 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
257
250
258 diffset = codeblocks.DiffSet(
251 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
252 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
253 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
254 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
255
263 diffset = self.path_filter.render_patchset_filtered(
256 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
258
266 return diffset
259 return diffset
267
260
268 @LoginRequired()
261 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
264 @view_config(
272 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 def pull_request_show(self):
267 def pull_request_show(self):
275 _ = self.request.translate
268 _ = self.request.translate
276 c = self.load_default_context()
269 c = self.load_default_context()
277
270
278 pull_request = PullRequest.get_or_404(
271 pull_request = PullRequest.get_or_404(
279 self.request.matchdict['pull_request_id'])
272 self.request.matchdict['pull_request_id'])
280 pull_request_id = pull_request.pull_request_id
273 pull_request_id = pull_request.pull_request_id
281
274
282 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
283 log.debug('show: forbidden because pull request is in state %s',
276 log.debug('show: forbidden because pull request is in state %s',
284 pull_request.pull_request_state)
277 pull_request.pull_request_state)
285 msg = _(u'Cannot show pull requests in state other than `{}`. '
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
286 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
287 pull_request.pull_request_state)
280 pull_request.pull_request_state)
288 h.flash(msg, category='error')
281 h.flash(msg, category='error')
289 raise HTTPFound(h.route_path('pullrequest_show_all',
282 raise HTTPFound(h.route_path('pullrequest_show_all',
290 repo_name=self.db_repo_name))
283 repo_name=self.db_repo_name))
291
284
292 version = self.request.GET.get('version')
285 version = self.request.GET.get('version')
293 from_version = self.request.GET.get('from_version') or version
286 from_version = self.request.GET.get('from_version') or version
294 merge_checks = self.request.GET.get('merge_checks')
287 merge_checks = self.request.GET.get('merge_checks')
295 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
296
289
297 # fetch global flags of ignore ws or context lines
290 # fetch global flags of ignore ws or context lines
298 diff_context = diffs.get_diff_context(self.request)
291 diff_context = diffs.get_diff_context(self.request)
299 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
300
293
301 force_refresh = str2bool(self.request.GET.get('force_refresh'))
294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
302
295
303 (pull_request_latest,
296 (pull_request_latest,
304 pull_request_at_ver,
297 pull_request_at_ver,
305 pull_request_display_obj,
298 pull_request_display_obj,
306 at_version) = PullRequestModel().get_pr_version(
299 at_version) = PullRequestModel().get_pr_version(
307 pull_request_id, version=version)
300 pull_request_id, version=version)
308 pr_closed = pull_request_latest.is_closed()
301 pr_closed = pull_request_latest.is_closed()
309
302
310 if pr_closed and (version or from_version):
303 if pr_closed and (version or from_version):
311 # not allow to browse versions
304 # not allow to browse versions
312 raise HTTPFound(h.route_path(
305 raise HTTPFound(h.route_path(
313 'pullrequest_show', repo_name=self.db_repo_name,
306 'pullrequest_show', repo_name=self.db_repo_name,
314 pull_request_id=pull_request_id))
307 pull_request_id=pull_request_id))
315
308
316 versions = pull_request_display_obj.versions()
309 versions = pull_request_display_obj.versions()
317 # used to store per-commit range diffs
310 # used to store per-commit range diffs
318 c.changes = collections.OrderedDict()
311 c.changes = collections.OrderedDict()
319 c.range_diff_on = self.request.GET.get('range-diff') == "1"
312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
320
313
321 c.at_version = at_version
314 c.at_version = at_version
322 c.at_version_num = (at_version
315 c.at_version_num = (at_version
323 if at_version and at_version != 'latest'
316 if at_version and at_version != 'latest'
324 else None)
317 else None)
325 c.at_version_pos = ChangesetComment.get_index_from_version(
318 c.at_version_pos = ChangesetComment.get_index_from_version(
326 c.at_version_num, versions)
319 c.at_version_num, versions)
327
320
328 (prev_pull_request_latest,
321 (prev_pull_request_latest,
329 prev_pull_request_at_ver,
322 prev_pull_request_at_ver,
330 prev_pull_request_display_obj,
323 prev_pull_request_display_obj,
331 prev_at_version) = PullRequestModel().get_pr_version(
324 prev_at_version) = PullRequestModel().get_pr_version(
332 pull_request_id, version=from_version)
325 pull_request_id, version=from_version)
333
326
334 c.from_version = prev_at_version
327 c.from_version = prev_at_version
335 c.from_version_num = (prev_at_version
328 c.from_version_num = (prev_at_version
336 if prev_at_version and prev_at_version != 'latest'
329 if prev_at_version and prev_at_version != 'latest'
337 else None)
330 else None)
338 c.from_version_pos = ChangesetComment.get_index_from_version(
331 c.from_version_pos = ChangesetComment.get_index_from_version(
339 c.from_version_num, versions)
332 c.from_version_num, versions)
340
333
341 # define if we're in COMPARE mode or VIEW at version mode
334 # define if we're in COMPARE mode or VIEW at version mode
342 compare = at_version != prev_at_version
335 compare = at_version != prev_at_version
343
336
344 # pull_requests repo_name we opened it against
337 # pull_requests repo_name we opened it against
345 # ie. target_repo must match
338 # ie. target_repo must match
346 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
347 raise HTTPNotFound()
340 raise HTTPNotFound()
348
341
349 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
350 pull_request_at_ver)
343 pull_request_at_ver)
351
344
352 c.pull_request = pull_request_display_obj
345 c.pull_request = pull_request_display_obj
353 c.renderer = pull_request_at_ver.description_renderer or c.renderer
346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
354 c.pull_request_latest = pull_request_latest
347 c.pull_request_latest = pull_request_latest
355
348
356 if compare or (at_version and not at_version == 'latest'):
349 if compare or (at_version and not at_version == 'latest'):
357 c.allowed_to_change_status = False
350 c.allowed_to_change_status = False
358 c.allowed_to_update = False
351 c.allowed_to_update = False
359 c.allowed_to_merge = False
352 c.allowed_to_merge = False
360 c.allowed_to_delete = False
353 c.allowed_to_delete = False
361 c.allowed_to_comment = False
354 c.allowed_to_comment = False
362 c.allowed_to_close = False
355 c.allowed_to_close = False
363 else:
356 else:
364 can_change_status = PullRequestModel().check_user_change_status(
357 can_change_status = PullRequestModel().check_user_change_status(
365 pull_request_at_ver, self._rhodecode_user)
358 pull_request_at_ver, self._rhodecode_user)
366 c.allowed_to_change_status = can_change_status and not pr_closed
359 c.allowed_to_change_status = can_change_status and not pr_closed
367
360
368 c.allowed_to_update = PullRequestModel().check_user_update(
361 c.allowed_to_update = PullRequestModel().check_user_update(
369 pull_request_latest, self._rhodecode_user) and not pr_closed
362 pull_request_latest, self._rhodecode_user) and not pr_closed
370 c.allowed_to_merge = PullRequestModel().check_user_merge(
363 c.allowed_to_merge = PullRequestModel().check_user_merge(
371 pull_request_latest, self._rhodecode_user) and not pr_closed
364 pull_request_latest, self._rhodecode_user) and not pr_closed
372 c.allowed_to_delete = PullRequestModel().check_user_delete(
365 c.allowed_to_delete = PullRequestModel().check_user_delete(
373 pull_request_latest, self._rhodecode_user) and not pr_closed
366 pull_request_latest, self._rhodecode_user) and not pr_closed
374 c.allowed_to_comment = not pr_closed
367 c.allowed_to_comment = not pr_closed
375 c.allowed_to_close = c.allowed_to_merge and not pr_closed
368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
376
369
377 c.forbid_adding_reviewers = False
370 c.forbid_adding_reviewers = False
378 c.forbid_author_to_review = False
371 c.forbid_author_to_review = False
379 c.forbid_commit_author_to_review = False
372 c.forbid_commit_author_to_review = False
380
373
381 if pull_request_latest.reviewer_data and \
374 if pull_request_latest.reviewer_data and \
382 'rules' in pull_request_latest.reviewer_data:
375 'rules' in pull_request_latest.reviewer_data:
383 rules = pull_request_latest.reviewer_data['rules'] or {}
376 rules = pull_request_latest.reviewer_data['rules'] or {}
384 try:
377 try:
385 c.forbid_adding_reviewers = rules.get(
378 c.forbid_adding_reviewers = rules.get(
386 'forbid_adding_reviewers')
379 'forbid_adding_reviewers')
387 c.forbid_author_to_review = rules.get(
380 c.forbid_author_to_review = rules.get(
388 'forbid_author_to_review')
381 'forbid_author_to_review')
389 c.forbid_commit_author_to_review = rules.get(
382 c.forbid_commit_author_to_review = rules.get(
390 'forbid_commit_author_to_review')
383 'forbid_commit_author_to_review')
391 except Exception:
384 except Exception:
392 pass
385 pass
393
386
394 # check merge capabilities
387 # check merge capabilities
395 _merge_check = MergeCheck.validate(
388 _merge_check = MergeCheck.validate(
396 pull_request_latest, auth_user=self._rhodecode_user,
389 pull_request_latest, auth_user=self._rhodecode_user,
397 translator=self.request.translate,
390 translator=self.request.translate,
398 force_shadow_repo_refresh=force_refresh)
391 force_shadow_repo_refresh=force_refresh)
399 c.pr_merge_errors = _merge_check.error_details
392 c.pr_merge_errors = _merge_check.error_details
400 c.pr_merge_possible = not _merge_check.failed
393 c.pr_merge_possible = not _merge_check.failed
401 c.pr_merge_message = _merge_check.merge_msg
394 c.pr_merge_message = _merge_check.merge_msg
402
395
403 c.pr_merge_info = MergeCheck.get_merge_conditions(
396 c.pr_merge_info = MergeCheck.get_merge_conditions(
404 pull_request_latest, translator=self.request.translate)
397 pull_request_latest, translator=self.request.translate)
405
398
406 c.pull_request_review_status = _merge_check.review_status
399 c.pull_request_review_status = _merge_check.review_status
407 if merge_checks:
400 if merge_checks:
408 self.request.override_renderer = \
401 self.request.override_renderer = \
409 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
410 return self._get_template_context(c)
403 return self._get_template_context(c)
411
404
412 comments_model = CommentsModel()
405 comments_model = CommentsModel()
413
406
414 # reviewers and statuses
407 # reviewers and statuses
415 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
416 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
417
410
418 # GENERAL COMMENTS with versions #
411 # GENERAL COMMENTS with versions #
419 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
420 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
421 general_comments = q
414 general_comments = q
422
415
423 # pick comments we want to render at current version
416 # pick comments we want to render at current version
424 c.comment_versions = comments_model.aggregate_comments(
417 c.comment_versions = comments_model.aggregate_comments(
425 general_comments, versions, c.at_version_num)
418 general_comments, versions, c.at_version_num)
426 c.comments = c.comment_versions[c.at_version_num]['until']
419 c.comments = c.comment_versions[c.at_version_num]['until']
427
420
428 # INLINE COMMENTS with versions #
421 # INLINE COMMENTS with versions #
429 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
430 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
431 inline_comments = q
424 inline_comments = q
432
425
433 c.inline_versions = comments_model.aggregate_comments(
426 c.inline_versions = comments_model.aggregate_comments(
434 inline_comments, versions, c.at_version_num, inline=True)
427 inline_comments, versions, c.at_version_num, inline=True)
435
428
436 # inject latest version
429 # inject latest version
437 latest_ver = PullRequest.get_pr_display_object(
430 latest_ver = PullRequest.get_pr_display_object(
438 pull_request_latest, pull_request_latest)
431 pull_request_latest, pull_request_latest)
439
432
440 c.versions = versions + [latest_ver]
433 c.versions = versions + [latest_ver]
441
434
442 # if we use version, then do not show later comments
435 # if we use version, then do not show later comments
443 # than current version
436 # than current version
444 display_inline_comments = collections.defaultdict(
437 display_inline_comments = collections.defaultdict(
445 lambda: collections.defaultdict(list))
438 lambda: collections.defaultdict(list))
446 for co in inline_comments:
439 for co in inline_comments:
447 if c.at_version_num:
440 if c.at_version_num:
448 # pick comments that are at least UPTO given version, so we
441 # pick comments that are at least UPTO given version, so we
449 # don't render comments for higher version
442 # don't render comments for higher version
450 should_render = co.pull_request_version_id and \
443 should_render = co.pull_request_version_id and \
451 co.pull_request_version_id <= c.at_version_num
444 co.pull_request_version_id <= c.at_version_num
452 else:
445 else:
453 # showing all, for 'latest'
446 # showing all, for 'latest'
454 should_render = True
447 should_render = True
455
448
456 if should_render:
449 if should_render:
457 display_inline_comments[co.f_path][co.line_no].append(co)
450 display_inline_comments[co.f_path][co.line_no].append(co)
458
451
459 # load diff data into template context, if we use compare mode then
452 # load diff data into template context, if we use compare mode then
460 # diff is calculated based on changes between versions of PR
453 # diff is calculated based on changes between versions of PR
461
454
462 source_repo = pull_request_at_ver.source_repo
455 source_repo = pull_request_at_ver.source_repo
463 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
456 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
464
457
465 target_repo = pull_request_at_ver.target_repo
458 target_repo = pull_request_at_ver.target_repo
466 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
459 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
467
460
468 if compare:
461 if compare:
469 # in compare switch the diff base to latest commit from prev version
462 # in compare switch the diff base to latest commit from prev version
470 target_ref_id = prev_pull_request_display_obj.revisions[0]
463 target_ref_id = prev_pull_request_display_obj.revisions[0]
471
464
472 # despite opening commits for bookmarks/branches/tags, we always
465 # despite opening commits for bookmarks/branches/tags, we always
473 # convert this to rev to prevent changes after bookmark or branch change
466 # convert this to rev to prevent changes after bookmark or branch change
474 c.source_ref_type = 'rev'
467 c.source_ref_type = 'rev'
475 c.source_ref = source_ref_id
468 c.source_ref = source_ref_id
476
469
477 c.target_ref_type = 'rev'
470 c.target_ref_type = 'rev'
478 c.target_ref = target_ref_id
471 c.target_ref = target_ref_id
479
472
480 c.source_repo = source_repo
473 c.source_repo = source_repo
481 c.target_repo = target_repo
474 c.target_repo = target_repo
482
475
483 c.commit_ranges = []
476 c.commit_ranges = []
484 source_commit = EmptyCommit()
477 source_commit = EmptyCommit()
485 target_commit = EmptyCommit()
478 target_commit = EmptyCommit()
486 c.missing_requirements = False
479 c.missing_requirements = False
487
480
488 source_scm = source_repo.scm_instance()
481 source_scm = source_repo.scm_instance()
489 target_scm = target_repo.scm_instance()
482 target_scm = target_repo.scm_instance()
490
483
491 shadow_scm = None
484 shadow_scm = None
492 try:
485 try:
493 shadow_scm = pull_request_latest.get_shadow_repo()
486 shadow_scm = pull_request_latest.get_shadow_repo()
494 except Exception:
487 except Exception:
495 log.debug('Failed to get shadow repo', exc_info=True)
488 log.debug('Failed to get shadow repo', exc_info=True)
496 # try first the existing source_repo, and then shadow
489 # try first the existing source_repo, and then shadow
497 # repo if we can obtain one
490 # repo if we can obtain one
498 commits_source_repo = source_scm or shadow_scm
491 commits_source_repo = source_scm or shadow_scm
499
492
500 c.commits_source_repo = commits_source_repo
493 c.commits_source_repo = commits_source_repo
501 c.ancestor = None # set it to None, to hide it from PR view
494 c.ancestor = None # set it to None, to hide it from PR view
502
495
503 # empty version means latest, so we keep this to prevent
496 # empty version means latest, so we keep this to prevent
504 # double caching
497 # double caching
505 version_normalized = version or 'latest'
498 version_normalized = version or 'latest'
506 from_version_normalized = from_version or 'latest'
499 from_version_normalized = from_version or 'latest'
507
500
508 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
501 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
509 cache_file_path = diff_cache_exist(
502 cache_file_path = diff_cache_exist(
510 cache_path, 'pull_request', pull_request_id, version_normalized,
503 cache_path, 'pull_request', pull_request_id, version_normalized,
511 from_version_normalized, source_ref_id, target_ref_id,
504 from_version_normalized, source_ref_id, target_ref_id,
512 hide_whitespace_changes, diff_context, c.fulldiff)
505 hide_whitespace_changes, diff_context, c.fulldiff)
513
506
514 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
507 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
515 force_recache = self.get_recache_flag()
508 force_recache = self.get_recache_flag()
516
509
517 cached_diff = None
510 cached_diff = None
518 if caching_enabled:
511 if caching_enabled:
519 cached_diff = load_cached_diff(cache_file_path)
512 cached_diff = load_cached_diff(cache_file_path)
520
513
521 has_proper_commit_cache = (
514 has_proper_commit_cache = (
522 cached_diff and cached_diff.get('commits')
515 cached_diff and cached_diff.get('commits')
523 and len(cached_diff.get('commits', [])) == 5
516 and len(cached_diff.get('commits', [])) == 5
524 and cached_diff.get('commits')[0]
517 and cached_diff.get('commits')[0]
525 and cached_diff.get('commits')[3])
518 and cached_diff.get('commits')[3])
526
519
527 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
520 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
528 diff_commit_cache = \
521 diff_commit_cache = \
529 (ancestor_commit, commit_cache, missing_requirements,
522 (ancestor_commit, commit_cache, missing_requirements,
530 source_commit, target_commit) = cached_diff['commits']
523 source_commit, target_commit) = cached_diff['commits']
531 else:
524 else:
532 diff_commit_cache = \
525 diff_commit_cache = \
533 (ancestor_commit, commit_cache, missing_requirements,
526 (ancestor_commit, commit_cache, missing_requirements,
534 source_commit, target_commit) = self.get_commits(
527 source_commit, target_commit) = self.get_commits(
535 commits_source_repo,
528 commits_source_repo,
536 pull_request_at_ver,
529 pull_request_at_ver,
537 source_commit,
530 source_commit,
538 source_ref_id,
531 source_ref_id,
539 source_scm,
532 source_scm,
540 target_commit,
533 target_commit,
541 target_ref_id,
534 target_ref_id,
542 target_scm)
535 target_scm)
543
536
544 # register our commit range
537 # register our commit range
545 for comm in commit_cache.values():
538 for comm in commit_cache.values():
546 c.commit_ranges.append(comm)
539 c.commit_ranges.append(comm)
547
540
548 c.missing_requirements = missing_requirements
541 c.missing_requirements = missing_requirements
549 c.ancestor_commit = ancestor_commit
542 c.ancestor_commit = ancestor_commit
550 c.statuses = source_repo.statuses(
543 c.statuses = source_repo.statuses(
551 [x.raw_id for x in c.commit_ranges])
544 [x.raw_id for x in c.commit_ranges])
552
545
553 # auto collapse if we have more than limit
546 # auto collapse if we have more than limit
554 collapse_limit = diffs.DiffProcessor._collapse_commits_over
547 collapse_limit = diffs.DiffProcessor._collapse_commits_over
555 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
548 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
556 c.compare_mode = compare
549 c.compare_mode = compare
557
550
558 # diff_limit is the old behavior, will cut off the whole diff
551 # diff_limit is the old behavior, will cut off the whole diff
559 # if the limit is applied otherwise will just hide the
552 # if the limit is applied otherwise will just hide the
560 # big files from the front-end
553 # big files from the front-end
561 diff_limit = c.visual.cut_off_limit_diff
554 diff_limit = c.visual.cut_off_limit_diff
562 file_limit = c.visual.cut_off_limit_file
555 file_limit = c.visual.cut_off_limit_file
563
556
564 c.missing_commits = False
557 c.missing_commits = False
565 if (c.missing_requirements
558 if (c.missing_requirements
566 or isinstance(source_commit, EmptyCommit)
559 or isinstance(source_commit, EmptyCommit)
567 or source_commit == target_commit):
560 or source_commit == target_commit):
568
561
569 c.missing_commits = True
562 c.missing_commits = True
570 else:
563 else:
571 c.inline_comments = display_inline_comments
564 c.inline_comments = display_inline_comments
572
565
573 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
566 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
574 if not force_recache and has_proper_diff_cache:
567 if not force_recache and has_proper_diff_cache:
575 c.diffset = cached_diff['diff']
568 c.diffset = cached_diff['diff']
576 (ancestor_commit, commit_cache, missing_requirements,
569 (ancestor_commit, commit_cache, missing_requirements,
577 source_commit, target_commit) = cached_diff['commits']
570 source_commit, target_commit) = cached_diff['commits']
578 else:
571 else:
579 c.diffset = self._get_diffset(
572 c.diffset = self._get_diffset(
580 c.source_repo.repo_name, commits_source_repo,
573 c.source_repo.repo_name, commits_source_repo,
581 source_ref_id, target_ref_id,
574 source_ref_id, target_ref_id,
582 target_commit, source_commit,
575 target_commit, source_commit,
583 diff_limit, file_limit, c.fulldiff,
576 diff_limit, file_limit, c.fulldiff,
584 hide_whitespace_changes, diff_context)
577 hide_whitespace_changes, diff_context)
585
578
586 # save cached diff
579 # save cached diff
587 if caching_enabled:
580 if caching_enabled:
588 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
581 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
589
582
590 c.limited_diff = c.diffset.limited_diff
583 c.limited_diff = c.diffset.limited_diff
591
584
592 # calculate removed files that are bound to comments
585 # calculate removed files that are bound to comments
593 comment_deleted_files = [
586 comment_deleted_files = [
594 fname for fname in display_inline_comments
587 fname for fname in display_inline_comments
595 if fname not in c.diffset.file_stats]
588 if fname not in c.diffset.file_stats]
596
589
597 c.deleted_files_comments = collections.defaultdict(dict)
590 c.deleted_files_comments = collections.defaultdict(dict)
598 for fname, per_line_comments in display_inline_comments.items():
591 for fname, per_line_comments in display_inline_comments.items():
599 if fname in comment_deleted_files:
592 if fname in comment_deleted_files:
600 c.deleted_files_comments[fname]['stats'] = 0
593 c.deleted_files_comments[fname]['stats'] = 0
601 c.deleted_files_comments[fname]['comments'] = list()
594 c.deleted_files_comments[fname]['comments'] = list()
602 for lno, comments in per_line_comments.items():
595 for lno, comments in per_line_comments.items():
603 c.deleted_files_comments[fname]['comments'].extend(comments)
596 c.deleted_files_comments[fname]['comments'].extend(comments)
604
597
605 # maybe calculate the range diff
598 # maybe calculate the range diff
606 if c.range_diff_on:
599 if c.range_diff_on:
607 # TODO(marcink): set whitespace/context
600 # TODO(marcink): set whitespace/context
608 context_lcl = 3
601 context_lcl = 3
609 ign_whitespace_lcl = False
602 ign_whitespace_lcl = False
610
603
611 for commit in c.commit_ranges:
604 for commit in c.commit_ranges:
612 commit2 = commit
605 commit2 = commit
613 commit1 = commit.first_parent
606 commit1 = commit.first_parent
614
607
615 range_diff_cache_file_path = diff_cache_exist(
608 range_diff_cache_file_path = diff_cache_exist(
616 cache_path, 'diff', commit.raw_id,
609 cache_path, 'diff', commit.raw_id,
617 ign_whitespace_lcl, context_lcl, c.fulldiff)
610 ign_whitespace_lcl, context_lcl, c.fulldiff)
618
611
619 cached_diff = None
612 cached_diff = None
620 if caching_enabled:
613 if caching_enabled:
621 cached_diff = load_cached_diff(range_diff_cache_file_path)
614 cached_diff = load_cached_diff(range_diff_cache_file_path)
622
615
623 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
616 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
624 if not force_recache and has_proper_diff_cache:
617 if not force_recache and has_proper_diff_cache:
625 diffset = cached_diff['diff']
618 diffset = cached_diff['diff']
626 else:
619 else:
627 diffset = self._get_range_diffset(
620 diffset = self._get_range_diffset(
628 source_scm, source_repo,
621 source_scm, source_repo,
629 commit1, commit2, diff_limit, file_limit,
622 commit1, commit2, diff_limit, file_limit,
630 c.fulldiff, ign_whitespace_lcl, context_lcl
623 c.fulldiff, ign_whitespace_lcl, context_lcl
631 )
624 )
632
625
633 # save cached diff
626 # save cached diff
634 if caching_enabled:
627 if caching_enabled:
635 cache_diff(range_diff_cache_file_path, diffset, None)
628 cache_diff(range_diff_cache_file_path, diffset, None)
636
629
637 c.changes[commit.raw_id] = diffset
630 c.changes[commit.raw_id] = diffset
638
631
639 # this is a hack to properly display links, when creating PR, the
632 # this is a hack to properly display links, when creating PR, the
640 # compare view and others uses different notation, and
633 # compare view and others uses different notation, and
641 # compare_commits.mako renders links based on the target_repo.
634 # compare_commits.mako renders links based on the target_repo.
642 # We need to swap that here to generate it properly on the html side
635 # We need to swap that here to generate it properly on the html side
643 c.target_repo = c.source_repo
636 c.target_repo = c.source_repo
644
637
645 c.commit_statuses = ChangesetStatus.STATUSES
638 c.commit_statuses = ChangesetStatus.STATUSES
646
639
647 c.show_version_changes = not pr_closed
640 c.show_version_changes = not pr_closed
648 if c.show_version_changes:
641 if c.show_version_changes:
649 cur_obj = pull_request_at_ver
642 cur_obj = pull_request_at_ver
650 prev_obj = prev_pull_request_at_ver
643 prev_obj = prev_pull_request_at_ver
651
644
652 old_commit_ids = prev_obj.revisions
645 old_commit_ids = prev_obj.revisions
653 new_commit_ids = cur_obj.revisions
646 new_commit_ids = cur_obj.revisions
654 commit_changes = PullRequestModel()._calculate_commit_id_changes(
647 commit_changes = PullRequestModel()._calculate_commit_id_changes(
655 old_commit_ids, new_commit_ids)
648 old_commit_ids, new_commit_ids)
656 c.commit_changes_summary = commit_changes
649 c.commit_changes_summary = commit_changes
657
650
658 # calculate the diff for commits between versions
651 # calculate the diff for commits between versions
659 c.commit_changes = []
652 c.commit_changes = []
660 mark = lambda cs, fw: list(
653 mark = lambda cs, fw: list(
661 h.itertools.izip_longest([], cs, fillvalue=fw))
654 h.itertools.izip_longest([], cs, fillvalue=fw))
662 for c_type, raw_id in mark(commit_changes.added, 'a') \
655 for c_type, raw_id in mark(commit_changes.added, 'a') \
663 + mark(commit_changes.removed, 'r') \
656 + mark(commit_changes.removed, 'r') \
664 + mark(commit_changes.common, 'c'):
657 + mark(commit_changes.common, 'c'):
665
658
666 if raw_id in commit_cache:
659 if raw_id in commit_cache:
667 commit = commit_cache[raw_id]
660 commit = commit_cache[raw_id]
668 else:
661 else:
669 try:
662 try:
670 commit = commits_source_repo.get_commit(raw_id)
663 commit = commits_source_repo.get_commit(raw_id)
671 except CommitDoesNotExistError:
664 except CommitDoesNotExistError:
672 # in case we fail extracting still use "dummy" commit
665 # in case we fail extracting still use "dummy" commit
673 # for display in commit diff
666 # for display in commit diff
674 commit = h.AttributeDict(
667 commit = h.AttributeDict(
675 {'raw_id': raw_id,
668 {'raw_id': raw_id,
676 'message': 'EMPTY or MISSING COMMIT'})
669 'message': 'EMPTY or MISSING COMMIT'})
677 c.commit_changes.append([c_type, commit])
670 c.commit_changes.append([c_type, commit])
678
671
679 # current user review statuses for each version
672 # current user review statuses for each version
680 c.review_versions = {}
673 c.review_versions = {}
681 if self._rhodecode_user.user_id in allowed_reviewers:
674 if self._rhodecode_user.user_id in allowed_reviewers:
682 for co in general_comments:
675 for co in general_comments:
683 if co.author.user_id == self._rhodecode_user.user_id:
676 if co.author.user_id == self._rhodecode_user.user_id:
684 status = co.status_change
677 status = co.status_change
685 if status:
678 if status:
686 _ver_pr = status[0].comment.pull_request_version_id
679 _ver_pr = status[0].comment.pull_request_version_id
687 c.review_versions[_ver_pr] = status[0]
680 c.review_versions[_ver_pr] = status[0]
688
681
689 return self._get_template_context(c)
682 return self._get_template_context(c)
690
683
691 def get_commits(
684 def get_commits(
692 self, commits_source_repo, pull_request_at_ver, source_commit,
685 self, commits_source_repo, pull_request_at_ver, source_commit,
693 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
686 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
694 commit_cache = collections.OrderedDict()
687 commit_cache = collections.OrderedDict()
695 missing_requirements = False
688 missing_requirements = False
696 try:
689 try:
697 pre_load = ["author", "branch", "date", "message", "parents"]
690 pre_load = ["author", "branch", "date", "message", "parents"]
698 show_revs = pull_request_at_ver.revisions
691 show_revs = pull_request_at_ver.revisions
699 for rev in show_revs:
692 for rev in show_revs:
700 comm = commits_source_repo.get_commit(
693 comm = commits_source_repo.get_commit(
701 commit_id=rev, pre_load=pre_load)
694 commit_id=rev, pre_load=pre_load)
702 commit_cache[comm.raw_id] = comm
695 commit_cache[comm.raw_id] = comm
703
696
704 # Order here matters, we first need to get target, and then
697 # Order here matters, we first need to get target, and then
705 # the source
698 # the source
706 target_commit = commits_source_repo.get_commit(
699 target_commit = commits_source_repo.get_commit(
707 commit_id=safe_str(target_ref_id))
700 commit_id=safe_str(target_ref_id))
708
701
709 source_commit = commits_source_repo.get_commit(
702 source_commit = commits_source_repo.get_commit(
710 commit_id=safe_str(source_ref_id))
703 commit_id=safe_str(source_ref_id))
711 except CommitDoesNotExistError:
704 except CommitDoesNotExistError:
712 log.warning(
705 log.warning(
713 'Failed to get commit from `{}` repo'.format(
706 'Failed to get commit from `{}` repo'.format(
714 commits_source_repo), exc_info=True)
707 commits_source_repo), exc_info=True)
715 except RepositoryRequirementError:
708 except RepositoryRequirementError:
716 log.warning(
709 log.warning(
717 'Failed to get all required data from repo', exc_info=True)
710 'Failed to get all required data from repo', exc_info=True)
718 missing_requirements = True
711 missing_requirements = True
719 ancestor_commit = None
712 ancestor_commit = None
720 try:
713 try:
721 ancestor_id = source_scm.get_common_ancestor(
714 ancestor_id = source_scm.get_common_ancestor(
722 source_commit.raw_id, target_commit.raw_id, target_scm)
715 source_commit.raw_id, target_commit.raw_id, target_scm)
723 ancestor_commit = source_scm.get_commit(ancestor_id)
716 ancestor_commit = source_scm.get_commit(ancestor_id)
724 except Exception:
717 except Exception:
725 ancestor_commit = None
718 ancestor_commit = None
726 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
719 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
727
720
728 def assure_not_empty_repo(self):
721 def assure_not_empty_repo(self):
729 _ = self.request.translate
722 _ = self.request.translate
730
723
731 try:
724 try:
732 self.db_repo.scm_instance().get_commit()
725 self.db_repo.scm_instance().get_commit()
733 except EmptyRepositoryError:
726 except EmptyRepositoryError:
734 h.flash(h.literal(_('There are no commits yet')),
727 h.flash(h.literal(_('There are no commits yet')),
735 category='warning')
728 category='warning')
736 raise HTTPFound(
729 raise HTTPFound(
737 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
730 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
738
731
739 @LoginRequired()
732 @LoginRequired()
740 @NotAnonymous()
733 @NotAnonymous()
741 @HasRepoPermissionAnyDecorator(
734 @HasRepoPermissionAnyDecorator(
742 'repository.read', 'repository.write', 'repository.admin')
735 'repository.read', 'repository.write', 'repository.admin')
743 @view_config(
736 @view_config(
744 route_name='pullrequest_new', request_method='GET',
737 route_name='pullrequest_new', request_method='GET',
745 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
738 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
746 def pull_request_new(self):
739 def pull_request_new(self):
747 _ = self.request.translate
740 _ = self.request.translate
748 c = self.load_default_context()
741 c = self.load_default_context()
749
742
750 self.assure_not_empty_repo()
743 self.assure_not_empty_repo()
751 source_repo = self.db_repo
744 source_repo = self.db_repo
752
745
753 commit_id = self.request.GET.get('commit')
746 commit_id = self.request.GET.get('commit')
754 branch_ref = self.request.GET.get('branch')
747 branch_ref = self.request.GET.get('branch')
755 bookmark_ref = self.request.GET.get('bookmark')
748 bookmark_ref = self.request.GET.get('bookmark')
756
749
757 try:
750 try:
758 source_repo_data = PullRequestModel().generate_repo_data(
751 source_repo_data = PullRequestModel().generate_repo_data(
759 source_repo, commit_id=commit_id,
752 source_repo, commit_id=commit_id,
760 branch=branch_ref, bookmark=bookmark_ref,
753 branch=branch_ref, bookmark=bookmark_ref,
761 translator=self.request.translate)
754 translator=self.request.translate)
762 except CommitDoesNotExistError as e:
755 except CommitDoesNotExistError as e:
763 log.exception(e)
756 log.exception(e)
764 h.flash(_('Commit does not exist'), 'error')
757 h.flash(_('Commit does not exist'), 'error')
765 raise HTTPFound(
758 raise HTTPFound(
766 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
759 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
767
760
768 default_target_repo = source_repo
761 default_target_repo = source_repo
769
762
770 if source_repo.parent and c.has_origin_repo_read_perm:
763 if source_repo.parent and c.has_origin_repo_read_perm:
771 parent_vcs_obj = source_repo.parent.scm_instance()
764 parent_vcs_obj = source_repo.parent.scm_instance()
772 if parent_vcs_obj and not parent_vcs_obj.is_empty():
765 if parent_vcs_obj and not parent_vcs_obj.is_empty():
773 # change default if we have a parent repo
766 # change default if we have a parent repo
774 default_target_repo = source_repo.parent
767 default_target_repo = source_repo.parent
775
768
776 target_repo_data = PullRequestModel().generate_repo_data(
769 target_repo_data = PullRequestModel().generate_repo_data(
777 default_target_repo, translator=self.request.translate)
770 default_target_repo, translator=self.request.translate)
778
771
779 selected_source_ref = source_repo_data['refs']['selected_ref']
772 selected_source_ref = source_repo_data['refs']['selected_ref']
780 title_source_ref = ''
773 title_source_ref = ''
781 if selected_source_ref:
774 if selected_source_ref:
782 title_source_ref = selected_source_ref.split(':', 2)[1]
775 title_source_ref = selected_source_ref.split(':', 2)[1]
783 c.default_title = PullRequestModel().generate_pullrequest_title(
776 c.default_title = PullRequestModel().generate_pullrequest_title(
784 source=source_repo.repo_name,
777 source=source_repo.repo_name,
785 source_ref=title_source_ref,
778 source_ref=title_source_ref,
786 target=default_target_repo.repo_name
779 target=default_target_repo.repo_name
787 )
780 )
788
781
789 c.default_repo_data = {
782 c.default_repo_data = {
790 'source_repo_name': source_repo.repo_name,
783 'source_repo_name': source_repo.repo_name,
791 'source_refs_json': json.dumps(source_repo_data),
784 'source_refs_json': json.dumps(source_repo_data),
792 'target_repo_name': default_target_repo.repo_name,
785 'target_repo_name': default_target_repo.repo_name,
793 'target_refs_json': json.dumps(target_repo_data),
786 'target_refs_json': json.dumps(target_repo_data),
794 }
787 }
795 c.default_source_ref = selected_source_ref
788 c.default_source_ref = selected_source_ref
796
789
797 return self._get_template_context(c)
790 return self._get_template_context(c)
798
791
799 @LoginRequired()
792 @LoginRequired()
800 @NotAnonymous()
793 @NotAnonymous()
801 @HasRepoPermissionAnyDecorator(
794 @HasRepoPermissionAnyDecorator(
802 'repository.read', 'repository.write', 'repository.admin')
795 'repository.read', 'repository.write', 'repository.admin')
803 @view_config(
796 @view_config(
804 route_name='pullrequest_repo_refs', request_method='GET',
797 route_name='pullrequest_repo_refs', request_method='GET',
805 renderer='json_ext', xhr=True)
798 renderer='json_ext', xhr=True)
806 def pull_request_repo_refs(self):
799 def pull_request_repo_refs(self):
807 self.load_default_context()
800 self.load_default_context()
808 target_repo_name = self.request.matchdict['target_repo_name']
801 target_repo_name = self.request.matchdict['target_repo_name']
809 repo = Repository.get_by_repo_name(target_repo_name)
802 repo = Repository.get_by_repo_name(target_repo_name)
810 if not repo:
803 if not repo:
811 raise HTTPNotFound()
804 raise HTTPNotFound()
812
805
813 target_perm = HasRepoPermissionAny(
806 target_perm = HasRepoPermissionAny(
814 'repository.read', 'repository.write', 'repository.admin')(
807 'repository.read', 'repository.write', 'repository.admin')(
815 target_repo_name)
808 target_repo_name)
816 if not target_perm:
809 if not target_perm:
817 raise HTTPNotFound()
810 raise HTTPNotFound()
818
811
819 return PullRequestModel().generate_repo_data(
812 return PullRequestModel().generate_repo_data(
820 repo, translator=self.request.translate)
813 repo, translator=self.request.translate)
821
814
822 @LoginRequired()
815 @LoginRequired()
823 @NotAnonymous()
816 @NotAnonymous()
824 @HasRepoPermissionAnyDecorator(
817 @HasRepoPermissionAnyDecorator(
825 'repository.read', 'repository.write', 'repository.admin')
818 'repository.read', 'repository.write', 'repository.admin')
826 @view_config(
819 @view_config(
827 route_name='pullrequest_repo_targets', request_method='GET',
820 route_name='pullrequest_repo_targets', request_method='GET',
828 renderer='json_ext', xhr=True)
821 renderer='json_ext', xhr=True)
829 def pullrequest_repo_targets(self):
822 def pullrequest_repo_targets(self):
830 _ = self.request.translate
823 _ = self.request.translate
831 filter_query = self.request.GET.get('query')
824 filter_query = self.request.GET.get('query')
832
825
833 # get the parents
826 # get the parents
834 parent_target_repos = []
827 parent_target_repos = []
835 if self.db_repo.parent:
828 if self.db_repo.parent:
836 parents_query = Repository.query() \
829 parents_query = Repository.query() \
837 .order_by(func.length(Repository.repo_name)) \
830 .order_by(func.length(Repository.repo_name)) \
838 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
831 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
839
832
840 if filter_query:
833 if filter_query:
841 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
834 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
842 parents_query = parents_query.filter(
835 parents_query = parents_query.filter(
843 Repository.repo_name.ilike(ilike_expression))
836 Repository.repo_name.ilike(ilike_expression))
844 parents = parents_query.limit(20).all()
837 parents = parents_query.limit(20).all()
845
838
846 for parent in parents:
839 for parent in parents:
847 parent_vcs_obj = parent.scm_instance()
840 parent_vcs_obj = parent.scm_instance()
848 if parent_vcs_obj and not parent_vcs_obj.is_empty():
841 if parent_vcs_obj and not parent_vcs_obj.is_empty():
849 parent_target_repos.append(parent)
842 parent_target_repos.append(parent)
850
843
851 # get other forks, and repo itself
844 # get other forks, and repo itself
852 query = Repository.query() \
845 query = Repository.query() \
853 .order_by(func.length(Repository.repo_name)) \
846 .order_by(func.length(Repository.repo_name)) \
854 .filter(
847 .filter(
855 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
848 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
856 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
849 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
857 ) \
850 ) \
858 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
851 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
859
852
860 if filter_query:
853 if filter_query:
861 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
854 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
862 query = query.filter(Repository.repo_name.ilike(ilike_expression))
855 query = query.filter(Repository.repo_name.ilike(ilike_expression))
863
856
864 limit = max(20 - len(parent_target_repos), 5) # not less then 5
857 limit = max(20 - len(parent_target_repos), 5) # not less then 5
865 target_repos = query.limit(limit).all()
858 target_repos = query.limit(limit).all()
866
859
867 all_target_repos = target_repos + parent_target_repos
860 all_target_repos = target_repos + parent_target_repos
868
861
869 repos = []
862 repos = []
870 # This checks permissions to the repositories
863 # This checks permissions to the repositories
871 for obj in ScmModel().get_repos(all_target_repos):
864 for obj in ScmModel().get_repos(all_target_repos):
872 repos.append({
865 repos.append({
873 'id': obj['name'],
866 'id': obj['name'],
874 'text': obj['name'],
867 'text': obj['name'],
875 'type': 'repo',
868 'type': 'repo',
876 'repo_id': obj['dbrepo']['repo_id'],
869 'repo_id': obj['dbrepo']['repo_id'],
877 'repo_type': obj['dbrepo']['repo_type'],
870 'repo_type': obj['dbrepo']['repo_type'],
878 'private': obj['dbrepo']['private'],
871 'private': obj['dbrepo']['private'],
879
872
880 })
873 })
881
874
882 data = {
875 data = {
883 'more': False,
876 'more': False,
884 'results': [{
877 'results': [{
885 'text': _('Repositories'),
878 'text': _('Repositories'),
886 'children': repos
879 'children': repos
887 }] if repos else []
880 }] if repos else []
888 }
881 }
889 return data
882 return data
890
883
891 @LoginRequired()
884 @LoginRequired()
892 @NotAnonymous()
885 @NotAnonymous()
893 @HasRepoPermissionAnyDecorator(
886 @HasRepoPermissionAnyDecorator(
894 'repository.read', 'repository.write', 'repository.admin')
887 'repository.read', 'repository.write', 'repository.admin')
895 @CSRFRequired()
888 @CSRFRequired()
896 @view_config(
889 @view_config(
897 route_name='pullrequest_create', request_method='POST',
890 route_name='pullrequest_create', request_method='POST',
898 renderer=None)
891 renderer=None)
899 def pull_request_create(self):
892 def pull_request_create(self):
900 _ = self.request.translate
893 _ = self.request.translate
901 self.assure_not_empty_repo()
894 self.assure_not_empty_repo()
902 self.load_default_context()
895 self.load_default_context()
903
896
904 controls = peppercorn.parse(self.request.POST.items())
897 controls = peppercorn.parse(self.request.POST.items())
905
898
906 try:
899 try:
907 form = PullRequestForm(
900 form = PullRequestForm(
908 self.request.translate, self.db_repo.repo_id)()
901 self.request.translate, self.db_repo.repo_id)()
909 _form = form.to_python(controls)
902 _form = form.to_python(controls)
910 except formencode.Invalid as errors:
903 except formencode.Invalid as errors:
911 if errors.error_dict.get('revisions'):
904 if errors.error_dict.get('revisions'):
912 msg = 'Revisions: %s' % errors.error_dict['revisions']
905 msg = 'Revisions: %s' % errors.error_dict['revisions']
913 elif errors.error_dict.get('pullrequest_title'):
906 elif errors.error_dict.get('pullrequest_title'):
914 msg = errors.error_dict.get('pullrequest_title')
907 msg = errors.error_dict.get('pullrequest_title')
915 else:
908 else:
916 msg = _('Error creating pull request: {}').format(errors)
909 msg = _('Error creating pull request: {}').format(errors)
917 log.exception(msg)
910 log.exception(msg)
918 h.flash(msg, 'error')
911 h.flash(msg, 'error')
919
912
920 # would rather just go back to form ...
913 # would rather just go back to form ...
921 raise HTTPFound(
914 raise HTTPFound(
922 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
915 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
923
916
924 source_repo = _form['source_repo']
917 source_repo = _form['source_repo']
925 source_ref = _form['source_ref']
918 source_ref = _form['source_ref']
926 target_repo = _form['target_repo']
919 target_repo = _form['target_repo']
927 target_ref = _form['target_ref']
920 target_ref = _form['target_ref']
928 commit_ids = _form['revisions'][::-1]
921 commit_ids = _form['revisions'][::-1]
929
922
930 # find the ancestor for this pr
923 # find the ancestor for this pr
931 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
924 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
932 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
925 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
933
926
934 if not (source_db_repo or target_db_repo):
927 if not (source_db_repo or target_db_repo):
935 h.flash(_('source_repo or target repo not found'), category='error')
928 h.flash(_('source_repo or target repo not found'), category='error')
936 raise HTTPFound(
929 raise HTTPFound(
937 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
938
931
939 # re-check permissions again here
932 # re-check permissions again here
940 # source_repo we must have read permissions
933 # source_repo we must have read permissions
941
934
942 source_perm = HasRepoPermissionAny(
935 source_perm = HasRepoPermissionAny(
943 'repository.read', 'repository.write', 'repository.admin')(
936 'repository.read', 'repository.write', 'repository.admin')(
944 source_db_repo.repo_name)
937 source_db_repo.repo_name)
945 if not source_perm:
938 if not source_perm:
946 msg = _('Not Enough permissions to source repo `{}`.'.format(
939 msg = _('Not Enough permissions to source repo `{}`.'.format(
947 source_db_repo.repo_name))
940 source_db_repo.repo_name))
948 h.flash(msg, category='error')
941 h.flash(msg, category='error')
949 # copy the args back to redirect
942 # copy the args back to redirect
950 org_query = self.request.GET.mixed()
943 org_query = self.request.GET.mixed()
951 raise HTTPFound(
944 raise HTTPFound(
952 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
945 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
953 _query=org_query))
946 _query=org_query))
954
947
955 # target repo we must have read permissions, and also later on
948 # target repo we must have read permissions, and also later on
956 # we want to check branch permissions here
949 # we want to check branch permissions here
957 target_perm = HasRepoPermissionAny(
950 target_perm = HasRepoPermissionAny(
958 'repository.read', 'repository.write', 'repository.admin')(
951 'repository.read', 'repository.write', 'repository.admin')(
959 target_db_repo.repo_name)
952 target_db_repo.repo_name)
960 if not target_perm:
953 if not target_perm:
961 msg = _('Not Enough permissions to target repo `{}`.'.format(
954 msg = _('Not Enough permissions to target repo `{}`.'.format(
962 target_db_repo.repo_name))
955 target_db_repo.repo_name))
963 h.flash(msg, category='error')
956 h.flash(msg, category='error')
964 # copy the args back to redirect
957 # copy the args back to redirect
965 org_query = self.request.GET.mixed()
958 org_query = self.request.GET.mixed()
966 raise HTTPFound(
959 raise HTTPFound(
967 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
960 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
968 _query=org_query))
961 _query=org_query))
969
962
970 source_scm = source_db_repo.scm_instance()
963 source_scm = source_db_repo.scm_instance()
971 target_scm = target_db_repo.scm_instance()
964 target_scm = target_db_repo.scm_instance()
972
965
973 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
966 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
974 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
967 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
975
968
976 ancestor = source_scm.get_common_ancestor(
969 ancestor = source_scm.get_common_ancestor(
977 source_commit.raw_id, target_commit.raw_id, target_scm)
970 source_commit.raw_id, target_commit.raw_id, target_scm)
978
971
979 # recalculate target ref based on ancestor
972 # recalculate target ref based on ancestor
980 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
973 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
981 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
974 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
982
975
983 get_default_reviewers_data, validate_default_reviewers = \
976 get_default_reviewers_data, validate_default_reviewers = \
984 PullRequestModel().get_reviewer_functions()
977 PullRequestModel().get_reviewer_functions()
985
978
986 # recalculate reviewers logic, to make sure we can validate this
979 # recalculate reviewers logic, to make sure we can validate this
987 reviewer_rules = get_default_reviewers_data(
980 reviewer_rules = get_default_reviewers_data(
988 self._rhodecode_db_user, source_db_repo,
981 self._rhodecode_db_user, source_db_repo,
989 source_commit, target_db_repo, target_commit)
982 source_commit, target_db_repo, target_commit)
990
983
991 given_reviewers = _form['review_members']
984 given_reviewers = _form['review_members']
992 reviewers = validate_default_reviewers(
985 reviewers = validate_default_reviewers(
993 given_reviewers, reviewer_rules)
986 given_reviewers, reviewer_rules)
994
987
995 pullrequest_title = _form['pullrequest_title']
988 pullrequest_title = _form['pullrequest_title']
996 title_source_ref = source_ref.split(':', 2)[1]
989 title_source_ref = source_ref.split(':', 2)[1]
997 if not pullrequest_title:
990 if not pullrequest_title:
998 pullrequest_title = PullRequestModel().generate_pullrequest_title(
991 pullrequest_title = PullRequestModel().generate_pullrequest_title(
999 source=source_repo,
992 source=source_repo,
1000 source_ref=title_source_ref,
993 source_ref=title_source_ref,
1001 target=target_repo
994 target=target_repo
1002 )
995 )
1003
996
1004 description = _form['pullrequest_desc']
997 description = _form['pullrequest_desc']
1005 description_renderer = _form['description_renderer']
998 description_renderer = _form['description_renderer']
1006
999
1007 try:
1000 try:
1008 pull_request = PullRequestModel().create(
1001 pull_request = PullRequestModel().create(
1009 created_by=self._rhodecode_user.user_id,
1002 created_by=self._rhodecode_user.user_id,
1010 source_repo=source_repo,
1003 source_repo=source_repo,
1011 source_ref=source_ref,
1004 source_ref=source_ref,
1012 target_repo=target_repo,
1005 target_repo=target_repo,
1013 target_ref=target_ref,
1006 target_ref=target_ref,
1014 revisions=commit_ids,
1007 revisions=commit_ids,
1015 reviewers=reviewers,
1008 reviewers=reviewers,
1016 title=pullrequest_title,
1009 title=pullrequest_title,
1017 description=description,
1010 description=description,
1018 description_renderer=description_renderer,
1011 description_renderer=description_renderer,
1019 reviewer_data=reviewer_rules,
1012 reviewer_data=reviewer_rules,
1020 auth_user=self._rhodecode_user
1013 auth_user=self._rhodecode_user
1021 )
1014 )
1022 Session().commit()
1015 Session().commit()
1023
1016
1024 h.flash(_('Successfully opened new pull request'),
1017 h.flash(_('Successfully opened new pull request'),
1025 category='success')
1018 category='success')
1026 except Exception:
1019 except Exception:
1027 msg = _('Error occurred during creation of this pull request.')
1020 msg = _('Error occurred during creation of this pull request.')
1028 log.exception(msg)
1021 log.exception(msg)
1029 h.flash(msg, category='error')
1022 h.flash(msg, category='error')
1030
1023
1031 # copy the args back to redirect
1024 # copy the args back to redirect
1032 org_query = self.request.GET.mixed()
1025 org_query = self.request.GET.mixed()
1033 raise HTTPFound(
1026 raise HTTPFound(
1034 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1027 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1035 _query=org_query))
1028 _query=org_query))
1036
1029
1037 raise HTTPFound(
1030 raise HTTPFound(
1038 h.route_path('pullrequest_show', repo_name=target_repo,
1031 h.route_path('pullrequest_show', repo_name=target_repo,
1039 pull_request_id=pull_request.pull_request_id))
1032 pull_request_id=pull_request.pull_request_id))
1040
1033
1041 @LoginRequired()
1034 @LoginRequired()
1042 @NotAnonymous()
1035 @NotAnonymous()
1043 @HasRepoPermissionAnyDecorator(
1036 @HasRepoPermissionAnyDecorator(
1044 'repository.read', 'repository.write', 'repository.admin')
1037 'repository.read', 'repository.write', 'repository.admin')
1045 @CSRFRequired()
1038 @CSRFRequired()
1046 @view_config(
1039 @view_config(
1047 route_name='pullrequest_update', request_method='POST',
1040 route_name='pullrequest_update', request_method='POST',
1048 renderer='json_ext')
1041 renderer='json_ext')
1049 def pull_request_update(self):
1042 def pull_request_update(self):
1050 pull_request = PullRequest.get_or_404(
1043 pull_request = PullRequest.get_or_404(
1051 self.request.matchdict['pull_request_id'])
1044 self.request.matchdict['pull_request_id'])
1052 _ = self.request.translate
1045 _ = self.request.translate
1053
1046
1054 self.load_default_context()
1047 self.load_default_context()
1055
1048
1056 if pull_request.is_closed():
1049 if pull_request.is_closed():
1057 log.debug('update: forbidden because pull request is closed')
1050 log.debug('update: forbidden because pull request is closed')
1058 msg = _(u'Cannot update closed pull requests.')
1051 msg = _(u'Cannot update closed pull requests.')
1059 h.flash(msg, category='error')
1052 h.flash(msg, category='error')
1060 return True
1053 return True
1061
1054
1062 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1055 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1063 log.debug('update: forbidden because pull request is in state %s',
1056 log.debug('update: forbidden because pull request is in state %s',
1064 pull_request.pull_request_state)
1057 pull_request.pull_request_state)
1065 msg = _(u'Cannot update pull requests in state other than `{}`. '
1058 msg = _(u'Cannot update pull requests in state other than `{}`. '
1066 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1059 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1067 pull_request.pull_request_state)
1060 pull_request.pull_request_state)
1068 h.flash(msg, category='error')
1061 h.flash(msg, category='error')
1069 return True
1062 return True
1070
1063
1071 # only owner or admin can update it
1064 # only owner or admin can update it
1072 allowed_to_update = PullRequestModel().check_user_update(
1065 allowed_to_update = PullRequestModel().check_user_update(
1073 pull_request, self._rhodecode_user)
1066 pull_request, self._rhodecode_user)
1074 if allowed_to_update:
1067 if allowed_to_update:
1075 controls = peppercorn.parse(self.request.POST.items())
1068 controls = peppercorn.parse(self.request.POST.items())
1076
1069
1077 if 'review_members' in controls:
1070 if 'review_members' in controls:
1078 self._update_reviewers(
1071 self._update_reviewers(
1079 pull_request, controls['review_members'],
1072 pull_request, controls['review_members'],
1080 pull_request.reviewer_data)
1073 pull_request.reviewer_data)
1081 elif str2bool(self.request.POST.get('update_commits', 'false')):
1074 elif str2bool(self.request.POST.get('update_commits', 'false')):
1082 self._update_commits(pull_request)
1075 self._update_commits(pull_request)
1083 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1076 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1084 self._edit_pull_request(pull_request)
1077 self._edit_pull_request(pull_request)
1085 else:
1078 else:
1086 raise HTTPBadRequest()
1079 raise HTTPBadRequest()
1087 return True
1080 return True
1088 raise HTTPForbidden()
1081 raise HTTPForbidden()
1089
1082
1090 def _edit_pull_request(self, pull_request):
1083 def _edit_pull_request(self, pull_request):
1091 _ = self.request.translate
1084 _ = self.request.translate
1092
1085
1093 try:
1086 try:
1094 PullRequestModel().edit(
1087 PullRequestModel().edit(
1095 pull_request,
1088 pull_request,
1096 self.request.POST.get('title'),
1089 self.request.POST.get('title'),
1097 self.request.POST.get('description'),
1090 self.request.POST.get('description'),
1098 self.request.POST.get('description_renderer'),
1091 self.request.POST.get('description_renderer'),
1099 self._rhodecode_user)
1092 self._rhodecode_user)
1100 except ValueError:
1093 except ValueError:
1101 msg = _(u'Cannot update closed pull requests.')
1094 msg = _(u'Cannot update closed pull requests.')
1102 h.flash(msg, category='error')
1095 h.flash(msg, category='error')
1103 return
1096 return
1104 else:
1097 else:
1105 Session().commit()
1098 Session().commit()
1106
1099
1107 msg = _(u'Pull request title & description updated.')
1100 msg = _(u'Pull request title & description updated.')
1108 h.flash(msg, category='success')
1101 h.flash(msg, category='success')
1109 return
1102 return
1110
1103
1111 def _update_commits(self, pull_request):
1104 def _update_commits(self, pull_request):
1112 _ = self.request.translate
1105 _ = self.request.translate
1113
1106
1114 with pull_request.set_state(PullRequest.STATE_UPDATING):
1107 with pull_request.set_state(PullRequest.STATE_UPDATING):
1115 resp = PullRequestModel().update_commits(pull_request)
1108 resp = PullRequestModel().update_commits(pull_request)
1116
1109
1117 if resp.executed:
1110 if resp.executed:
1118
1111
1119 if resp.target_changed and resp.source_changed:
1112 if resp.target_changed and resp.source_changed:
1120 changed = 'target and source repositories'
1113 changed = 'target and source repositories'
1121 elif resp.target_changed and not resp.source_changed:
1114 elif resp.target_changed and not resp.source_changed:
1122 changed = 'target repository'
1115 changed = 'target repository'
1123 elif not resp.target_changed and resp.source_changed:
1116 elif not resp.target_changed and resp.source_changed:
1124 changed = 'source repository'
1117 changed = 'source repository'
1125 else:
1118 else:
1126 changed = 'nothing'
1119 changed = 'nothing'
1127
1120
1128 msg = _(u'Pull request updated to "{source_commit_id}" with '
1121 msg = _(u'Pull request updated to "{source_commit_id}" with '
1129 u'{count_added} added, {count_removed} removed commits. '
1122 u'{count_added} added, {count_removed} removed commits. '
1130 u'Source of changes: {change_source}')
1123 u'Source of changes: {change_source}')
1131 msg = msg.format(
1124 msg = msg.format(
1132 source_commit_id=pull_request.source_ref_parts.commit_id,
1125 source_commit_id=pull_request.source_ref_parts.commit_id,
1133 count_added=len(resp.changes.added),
1126 count_added=len(resp.changes.added),
1134 count_removed=len(resp.changes.removed),
1127 count_removed=len(resp.changes.removed),
1135 change_source=changed)
1128 change_source=changed)
1136 h.flash(msg, category='success')
1129 h.flash(msg, category='success')
1137
1130
1138 channel = '/repo${}$/pr/{}'.format(
1131 channel = '/repo${}$/pr/{}'.format(
1139 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1132 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1140 message = msg + (
1133 message = msg + (
1141 ' - <a onclick="window.location.reload()">'
1134 ' - <a onclick="window.location.reload()">'
1142 '<strong>{}</strong></a>'.format(_('Reload page')))
1135 '<strong>{}</strong></a>'.format(_('Reload page')))
1143 channelstream.post_message(
1136 channelstream.post_message(
1144 channel, message, self._rhodecode_user.username,
1137 channel, message, self._rhodecode_user.username,
1145 registry=self.request.registry)
1138 registry=self.request.registry)
1146 else:
1139 else:
1147 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1140 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1148 warning_reasons = [
1141 warning_reasons = [
1149 UpdateFailureReason.NO_CHANGE,
1142 UpdateFailureReason.NO_CHANGE,
1150 UpdateFailureReason.WRONG_REF_TYPE,
1143 UpdateFailureReason.WRONG_REF_TYPE,
1151 ]
1144 ]
1152 category = 'warning' if resp.reason in warning_reasons else 'error'
1145 category = 'warning' if resp.reason in warning_reasons else 'error'
1153 h.flash(msg, category=category)
1146 h.flash(msg, category=category)
1154
1147
1155 @LoginRequired()
1148 @LoginRequired()
1156 @NotAnonymous()
1149 @NotAnonymous()
1157 @HasRepoPermissionAnyDecorator(
1150 @HasRepoPermissionAnyDecorator(
1158 'repository.read', 'repository.write', 'repository.admin')
1151 'repository.read', 'repository.write', 'repository.admin')
1159 @CSRFRequired()
1152 @CSRFRequired()
1160 @view_config(
1153 @view_config(
1161 route_name='pullrequest_merge', request_method='POST',
1154 route_name='pullrequest_merge', request_method='POST',
1162 renderer='json_ext')
1155 renderer='json_ext')
1163 def pull_request_merge(self):
1156 def pull_request_merge(self):
1164 """
1157 """
1165 Merge will perform a server-side merge of the specified
1158 Merge will perform a server-side merge of the specified
1166 pull request, if the pull request is approved and mergeable.
1159 pull request, if the pull request is approved and mergeable.
1167 After successful merging, the pull request is automatically
1160 After successful merging, the pull request is automatically
1168 closed, with a relevant comment.
1161 closed, with a relevant comment.
1169 """
1162 """
1170 pull_request = PullRequest.get_or_404(
1163 pull_request = PullRequest.get_or_404(
1171 self.request.matchdict['pull_request_id'])
1164 self.request.matchdict['pull_request_id'])
1172 _ = self.request.translate
1165 _ = self.request.translate
1173
1166
1174 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1167 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1175 log.debug('show: forbidden because pull request is in state %s',
1168 log.debug('show: forbidden because pull request is in state %s',
1176 pull_request.pull_request_state)
1169 pull_request.pull_request_state)
1177 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1170 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1178 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1171 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1179 pull_request.pull_request_state)
1172 pull_request.pull_request_state)
1180 h.flash(msg, category='error')
1173 h.flash(msg, category='error')
1181 raise HTTPFound(
1174 raise HTTPFound(
1182 h.route_path('pullrequest_show',
1175 h.route_path('pullrequest_show',
1183 repo_name=pull_request.target_repo.repo_name,
1176 repo_name=pull_request.target_repo.repo_name,
1184 pull_request_id=pull_request.pull_request_id))
1177 pull_request_id=pull_request.pull_request_id))
1185
1178
1186 self.load_default_context()
1179 self.load_default_context()
1187
1180
1188 with pull_request.set_state(PullRequest.STATE_UPDATING):
1181 with pull_request.set_state(PullRequest.STATE_UPDATING):
1189 check = MergeCheck.validate(
1182 check = MergeCheck.validate(
1190 pull_request, auth_user=self._rhodecode_user,
1183 pull_request, auth_user=self._rhodecode_user,
1191 translator=self.request.translate)
1184 translator=self.request.translate)
1192 merge_possible = not check.failed
1185 merge_possible = not check.failed
1193
1186
1194 for err_type, error_msg in check.errors:
1187 for err_type, error_msg in check.errors:
1195 h.flash(error_msg, category=err_type)
1188 h.flash(error_msg, category=err_type)
1196
1189
1197 if merge_possible:
1190 if merge_possible:
1198 log.debug("Pre-conditions checked, trying to merge.")
1191 log.debug("Pre-conditions checked, trying to merge.")
1199 extras = vcs_operation_context(
1192 extras = vcs_operation_context(
1200 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1193 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1201 username=self._rhodecode_db_user.username, action='push',
1194 username=self._rhodecode_db_user.username, action='push',
1202 scm=pull_request.target_repo.repo_type)
1195 scm=pull_request.target_repo.repo_type)
1203 with pull_request.set_state(PullRequest.STATE_UPDATING):
1196 with pull_request.set_state(PullRequest.STATE_UPDATING):
1204 self._merge_pull_request(
1197 self._merge_pull_request(
1205 pull_request, self._rhodecode_db_user, extras)
1198 pull_request, self._rhodecode_db_user, extras)
1206 else:
1199 else:
1207 log.debug("Pre-conditions failed, NOT merging.")
1200 log.debug("Pre-conditions failed, NOT merging.")
1208
1201
1209 raise HTTPFound(
1202 raise HTTPFound(
1210 h.route_path('pullrequest_show',
1203 h.route_path('pullrequest_show',
1211 repo_name=pull_request.target_repo.repo_name,
1204 repo_name=pull_request.target_repo.repo_name,
1212 pull_request_id=pull_request.pull_request_id))
1205 pull_request_id=pull_request.pull_request_id))
1213
1206
1214 def _merge_pull_request(self, pull_request, user, extras):
1207 def _merge_pull_request(self, pull_request, user, extras):
1215 _ = self.request.translate
1208 _ = self.request.translate
1216 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1209 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1217
1210
1218 if merge_resp.executed:
1211 if merge_resp.executed:
1219 log.debug("The merge was successful, closing the pull request.")
1212 log.debug("The merge was successful, closing the pull request.")
1220 PullRequestModel().close_pull_request(
1213 PullRequestModel().close_pull_request(
1221 pull_request.pull_request_id, user)
1214 pull_request.pull_request_id, user)
1222 Session().commit()
1215 Session().commit()
1223 msg = _('Pull request was successfully merged and closed.')
1216 msg = _('Pull request was successfully merged and closed.')
1224 h.flash(msg, category='success')
1217 h.flash(msg, category='success')
1225 else:
1218 else:
1226 log.debug(
1219 log.debug(
1227 "The merge was not successful. Merge response: %s", merge_resp)
1220 "The merge was not successful. Merge response: %s", merge_resp)
1228 msg = merge_resp.merge_status_message
1221 msg = merge_resp.merge_status_message
1229 h.flash(msg, category='error')
1222 h.flash(msg, category='error')
1230
1223
1231 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1224 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1232 _ = self.request.translate
1225 _ = self.request.translate
1233
1226
1234 get_default_reviewers_data, validate_default_reviewers = \
1227 get_default_reviewers_data, validate_default_reviewers = \
1235 PullRequestModel().get_reviewer_functions()
1228 PullRequestModel().get_reviewer_functions()
1236
1229
1237 try:
1230 try:
1238 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1231 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1239 except ValueError as e:
1232 except ValueError as e:
1240 log.error('Reviewers Validation: {}'.format(e))
1233 log.error('Reviewers Validation: {}'.format(e))
1241 h.flash(e, category='error')
1234 h.flash(e, category='error')
1242 return
1235 return
1243
1236
1244 old_calculated_status = pull_request.calculated_review_status()
1237 old_calculated_status = pull_request.calculated_review_status()
1245 PullRequestModel().update_reviewers(
1238 PullRequestModel().update_reviewers(
1246 pull_request, reviewers, self._rhodecode_user)
1239 pull_request, reviewers, self._rhodecode_user)
1247 h.flash(_('Pull request reviewers updated.'), category='success')
1240 h.flash(_('Pull request reviewers updated.'), category='success')
1248 Session().commit()
1241 Session().commit()
1249
1242
1250 # trigger status changed if change in reviewers changes the status
1243 # trigger status changed if change in reviewers changes the status
1251 calculated_status = pull_request.calculated_review_status()
1244 calculated_status = pull_request.calculated_review_status()
1252 if old_calculated_status != calculated_status:
1245 if old_calculated_status != calculated_status:
1253 PullRequestModel().trigger_pull_request_hook(
1246 PullRequestModel().trigger_pull_request_hook(
1254 pull_request, self._rhodecode_user, 'review_status_change',
1247 pull_request, self._rhodecode_user, 'review_status_change',
1255 data={'status': calculated_status})
1248 data={'status': calculated_status})
1256
1249
1257 @LoginRequired()
1250 @LoginRequired()
1258 @NotAnonymous()
1251 @NotAnonymous()
1259 @HasRepoPermissionAnyDecorator(
1252 @HasRepoPermissionAnyDecorator(
1260 'repository.read', 'repository.write', 'repository.admin')
1253 'repository.read', 'repository.write', 'repository.admin')
1261 @CSRFRequired()
1254 @CSRFRequired()
1262 @view_config(
1255 @view_config(
1263 route_name='pullrequest_delete', request_method='POST',
1256 route_name='pullrequest_delete', request_method='POST',
1264 renderer='json_ext')
1257 renderer='json_ext')
1265 def pull_request_delete(self):
1258 def pull_request_delete(self):
1266 _ = self.request.translate
1259 _ = self.request.translate
1267
1260
1268 pull_request = PullRequest.get_or_404(
1261 pull_request = PullRequest.get_or_404(
1269 self.request.matchdict['pull_request_id'])
1262 self.request.matchdict['pull_request_id'])
1270 self.load_default_context()
1263 self.load_default_context()
1271
1264
1272 pr_closed = pull_request.is_closed()
1265 pr_closed = pull_request.is_closed()
1273 allowed_to_delete = PullRequestModel().check_user_delete(
1266 allowed_to_delete = PullRequestModel().check_user_delete(
1274 pull_request, self._rhodecode_user) and not pr_closed
1267 pull_request, self._rhodecode_user) and not pr_closed
1275
1268
1276 # only owner can delete it !
1269 # only owner can delete it !
1277 if allowed_to_delete:
1270 if allowed_to_delete:
1278 PullRequestModel().delete(pull_request, self._rhodecode_user)
1271 PullRequestModel().delete(pull_request, self._rhodecode_user)
1279 Session().commit()
1272 Session().commit()
1280 h.flash(_('Successfully deleted pull request'),
1273 h.flash(_('Successfully deleted pull request'),
1281 category='success')
1274 category='success')
1282 raise HTTPFound(h.route_path('pullrequest_show_all',
1275 raise HTTPFound(h.route_path('pullrequest_show_all',
1283 repo_name=self.db_repo_name))
1276 repo_name=self.db_repo_name))
1284
1277
1285 log.warning('user %s tried to delete pull request without access',
1278 log.warning('user %s tried to delete pull request without access',
1286 self._rhodecode_user)
1279 self._rhodecode_user)
1287 raise HTTPNotFound()
1280 raise HTTPNotFound()
1288
1281
1289 @LoginRequired()
1282 @LoginRequired()
1290 @NotAnonymous()
1283 @NotAnonymous()
1291 @HasRepoPermissionAnyDecorator(
1284 @HasRepoPermissionAnyDecorator(
1292 'repository.read', 'repository.write', 'repository.admin')
1285 'repository.read', 'repository.write', 'repository.admin')
1293 @CSRFRequired()
1286 @CSRFRequired()
1294 @view_config(
1287 @view_config(
1295 route_name='pullrequest_comment_create', request_method='POST',
1288 route_name='pullrequest_comment_create', request_method='POST',
1296 renderer='json_ext')
1289 renderer='json_ext')
1297 def pull_request_comment_create(self):
1290 def pull_request_comment_create(self):
1298 _ = self.request.translate
1291 _ = self.request.translate
1299
1292
1300 pull_request = PullRequest.get_or_404(
1293 pull_request = PullRequest.get_or_404(
1301 self.request.matchdict['pull_request_id'])
1294 self.request.matchdict['pull_request_id'])
1302 pull_request_id = pull_request.pull_request_id
1295 pull_request_id = pull_request.pull_request_id
1303
1296
1304 if pull_request.is_closed():
1297 if pull_request.is_closed():
1305 log.debug('comment: forbidden because pull request is closed')
1298 log.debug('comment: forbidden because pull request is closed')
1306 raise HTTPForbidden()
1299 raise HTTPForbidden()
1307
1300
1308 allowed_to_comment = PullRequestModel().check_user_comment(
1301 allowed_to_comment = PullRequestModel().check_user_comment(
1309 pull_request, self._rhodecode_user)
1302 pull_request, self._rhodecode_user)
1310 if not allowed_to_comment:
1303 if not allowed_to_comment:
1311 log.debug(
1304 log.debug(
1312 'comment: forbidden because pull request is from forbidden repo')
1305 'comment: forbidden because pull request is from forbidden repo')
1313 raise HTTPForbidden()
1306 raise HTTPForbidden()
1314
1307
1315 c = self.load_default_context()
1308 c = self.load_default_context()
1316
1309
1317 status = self.request.POST.get('changeset_status', None)
1310 status = self.request.POST.get('changeset_status', None)
1318 text = self.request.POST.get('text')
1311 text = self.request.POST.get('text')
1319 comment_type = self.request.POST.get('comment_type')
1312 comment_type = self.request.POST.get('comment_type')
1320 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1313 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1321 close_pull_request = self.request.POST.get('close_pull_request')
1314 close_pull_request = self.request.POST.get('close_pull_request')
1322
1315
1323 # the logic here should work like following, if we submit close
1316 # the logic here should work like following, if we submit close
1324 # pr comment, use `close_pull_request_with_comment` function
1317 # pr comment, use `close_pull_request_with_comment` function
1325 # else handle regular comment logic
1318 # else handle regular comment logic
1326
1319
1327 if close_pull_request:
1320 if close_pull_request:
1328 # only owner or admin or person with write permissions
1321 # only owner or admin or person with write permissions
1329 allowed_to_close = PullRequestModel().check_user_update(
1322 allowed_to_close = PullRequestModel().check_user_update(
1330 pull_request, self._rhodecode_user)
1323 pull_request, self._rhodecode_user)
1331 if not allowed_to_close:
1324 if not allowed_to_close:
1332 log.debug('comment: forbidden because not allowed to close '
1325 log.debug('comment: forbidden because not allowed to close '
1333 'pull request %s', pull_request_id)
1326 'pull request %s', pull_request_id)
1334 raise HTTPForbidden()
1327 raise HTTPForbidden()
1335
1328
1336 # This also triggers `review_status_change`
1329 # This also triggers `review_status_change`
1337 comment, status = PullRequestModel().close_pull_request_with_comment(
1330 comment, status = PullRequestModel().close_pull_request_with_comment(
1338 pull_request, self._rhodecode_user, self.db_repo, message=text,
1331 pull_request, self._rhodecode_user, self.db_repo, message=text,
1339 auth_user=self._rhodecode_user)
1332 auth_user=self._rhodecode_user)
1340 Session().flush()
1333 Session().flush()
1341
1334
1342 PullRequestModel().trigger_pull_request_hook(
1335 PullRequestModel().trigger_pull_request_hook(
1343 pull_request, self._rhodecode_user, 'comment',
1336 pull_request, self._rhodecode_user, 'comment',
1344 data={'comment': comment})
1337 data={'comment': comment})
1345
1338
1346 else:
1339 else:
1347 # regular comment case, could be inline, or one with status.
1340 # regular comment case, could be inline, or one with status.
1348 # for that one we check also permissions
1341 # for that one we check also permissions
1349
1342
1350 allowed_to_change_status = PullRequestModel().check_user_change_status(
1343 allowed_to_change_status = PullRequestModel().check_user_change_status(
1351 pull_request, self._rhodecode_user)
1344 pull_request, self._rhodecode_user)
1352
1345
1353 if status and allowed_to_change_status:
1346 if status and allowed_to_change_status:
1354 message = (_('Status change %(transition_icon)s %(status)s')
1347 message = (_('Status change %(transition_icon)s %(status)s')
1355 % {'transition_icon': '>',
1348 % {'transition_icon': '>',
1356 'status': ChangesetStatus.get_status_lbl(status)})
1349 'status': ChangesetStatus.get_status_lbl(status)})
1357 text = text or message
1350 text = text or message
1358
1351
1359 comment = CommentsModel().create(
1352 comment = CommentsModel().create(
1360 text=text,
1353 text=text,
1361 repo=self.db_repo.repo_id,
1354 repo=self.db_repo.repo_id,
1362 user=self._rhodecode_user.user_id,
1355 user=self._rhodecode_user.user_id,
1363 pull_request=pull_request,
1356 pull_request=pull_request,
1364 f_path=self.request.POST.get('f_path'),
1357 f_path=self.request.POST.get('f_path'),
1365 line_no=self.request.POST.get('line'),
1358 line_no=self.request.POST.get('line'),
1366 status_change=(ChangesetStatus.get_status_lbl(status)
1359 status_change=(ChangesetStatus.get_status_lbl(status)
1367 if status and allowed_to_change_status else None),
1360 if status and allowed_to_change_status else None),
1368 status_change_type=(status
1361 status_change_type=(status
1369 if status and allowed_to_change_status else None),
1362 if status and allowed_to_change_status else None),
1370 comment_type=comment_type,
1363 comment_type=comment_type,
1371 resolves_comment_id=resolves_comment_id,
1364 resolves_comment_id=resolves_comment_id,
1372 auth_user=self._rhodecode_user
1365 auth_user=self._rhodecode_user
1373 )
1366 )
1374
1367
1375 if allowed_to_change_status:
1368 if allowed_to_change_status:
1376 # calculate old status before we change it
1369 # calculate old status before we change it
1377 old_calculated_status = pull_request.calculated_review_status()
1370 old_calculated_status = pull_request.calculated_review_status()
1378
1371
1379 # get status if set !
1372 # get status if set !
1380 if status:
1373 if status:
1381 ChangesetStatusModel().set_status(
1374 ChangesetStatusModel().set_status(
1382 self.db_repo.repo_id,
1375 self.db_repo.repo_id,
1383 status,
1376 status,
1384 self._rhodecode_user.user_id,
1377 self._rhodecode_user.user_id,
1385 comment,
1378 comment,
1386 pull_request=pull_request
1379 pull_request=pull_request
1387 )
1380 )
1388
1381
1389 Session().flush()
1382 Session().flush()
1390 # this is somehow required to get access to some relationship
1383 # this is somehow required to get access to some relationship
1391 # loaded on comment
1384 # loaded on comment
1392 Session().refresh(comment)
1385 Session().refresh(comment)
1393
1386
1394 PullRequestModel().trigger_pull_request_hook(
1387 PullRequestModel().trigger_pull_request_hook(
1395 pull_request, self._rhodecode_user, 'comment',
1388 pull_request, self._rhodecode_user, 'comment',
1396 data={'comment': comment})
1389 data={'comment': comment})
1397
1390
1398 # we now calculate the status of pull request, and based on that
1391 # we now calculate the status of pull request, and based on that
1399 # calculation we set the commits status
1392 # calculation we set the commits status
1400 calculated_status = pull_request.calculated_review_status()
1393 calculated_status = pull_request.calculated_review_status()
1401 if old_calculated_status != calculated_status:
1394 if old_calculated_status != calculated_status:
1402 PullRequestModel().trigger_pull_request_hook(
1395 PullRequestModel().trigger_pull_request_hook(
1403 pull_request, self._rhodecode_user, 'review_status_change',
1396 pull_request, self._rhodecode_user, 'review_status_change',
1404 data={'status': calculated_status})
1397 data={'status': calculated_status})
1405
1398
1406 Session().commit()
1399 Session().commit()
1407
1400
1408 data = {
1401 data = {
1409 'target_id': h.safeid(h.safe_unicode(
1402 'target_id': h.safeid(h.safe_unicode(
1410 self.request.POST.get('f_path'))),
1403 self.request.POST.get('f_path'))),
1411 }
1404 }
1412 if comment:
1405 if comment:
1413 c.co = comment
1406 c.co = comment
1414 rendered_comment = render(
1407 rendered_comment = render(
1415 'rhodecode:templates/changeset/changeset_comment_block.mako',
1408 'rhodecode:templates/changeset/changeset_comment_block.mako',
1416 self._get_template_context(c), self.request)
1409 self._get_template_context(c), self.request)
1417
1410
1418 data.update(comment.get_dict())
1411 data.update(comment.get_dict())
1419 data.update({'rendered_text': rendered_comment})
1412 data.update({'rendered_text': rendered_comment})
1420
1413
1421 return data
1414 return data
1422
1415
1423 @LoginRequired()
1416 @LoginRequired()
1424 @NotAnonymous()
1417 @NotAnonymous()
1425 @HasRepoPermissionAnyDecorator(
1418 @HasRepoPermissionAnyDecorator(
1426 'repository.read', 'repository.write', 'repository.admin')
1419 'repository.read', 'repository.write', 'repository.admin')
1427 @CSRFRequired()
1420 @CSRFRequired()
1428 @view_config(
1421 @view_config(
1429 route_name='pullrequest_comment_delete', request_method='POST',
1422 route_name='pullrequest_comment_delete', request_method='POST',
1430 renderer='json_ext')
1423 renderer='json_ext')
1431 def pull_request_comment_delete(self):
1424 def pull_request_comment_delete(self):
1432 pull_request = PullRequest.get_or_404(
1425 pull_request = PullRequest.get_or_404(
1433 self.request.matchdict['pull_request_id'])
1426 self.request.matchdict['pull_request_id'])
1434
1427
1435 comment = ChangesetComment.get_or_404(
1428 comment = ChangesetComment.get_or_404(
1436 self.request.matchdict['comment_id'])
1429 self.request.matchdict['comment_id'])
1437 comment_id = comment.comment_id
1430 comment_id = comment.comment_id
1438
1431
1439 if pull_request.is_closed():
1432 if pull_request.is_closed():
1440 log.debug('comment: forbidden because pull request is closed')
1433 log.debug('comment: forbidden because pull request is closed')
1441 raise HTTPForbidden()
1434 raise HTTPForbidden()
1442
1435
1443 if not comment:
1436 if not comment:
1444 log.debug('Comment with id:%s not found, skipping', comment_id)
1437 log.debug('Comment with id:%s not found, skipping', comment_id)
1445 # comment already deleted in another call probably
1438 # comment already deleted in another call probably
1446 return True
1439 return True
1447
1440
1448 if comment.pull_request.is_closed():
1441 if comment.pull_request.is_closed():
1449 # don't allow deleting comments on closed pull request
1442 # don't allow deleting comments on closed pull request
1450 raise HTTPForbidden()
1443 raise HTTPForbidden()
1451
1444
1452 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1445 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1453 super_admin = h.HasPermissionAny('hg.admin')()
1446 super_admin = h.HasPermissionAny('hg.admin')()
1454 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1447 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1455 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1448 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1456 comment_repo_admin = is_repo_admin and is_repo_comment
1449 comment_repo_admin = is_repo_admin and is_repo_comment
1457
1450
1458 if super_admin or comment_owner or comment_repo_admin:
1451 if super_admin or comment_owner or comment_repo_admin:
1459 old_calculated_status = comment.pull_request.calculated_review_status()
1452 old_calculated_status = comment.pull_request.calculated_review_status()
1460 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1453 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1461 Session().commit()
1454 Session().commit()
1462 calculated_status = comment.pull_request.calculated_review_status()
1455 calculated_status = comment.pull_request.calculated_review_status()
1463 if old_calculated_status != calculated_status:
1456 if old_calculated_status != calculated_status:
1464 PullRequestModel().trigger_pull_request_hook(
1457 PullRequestModel().trigger_pull_request_hook(
1465 comment.pull_request, self._rhodecode_user, 'review_status_change',
1458 comment.pull_request, self._rhodecode_user, 'review_status_change',
1466 data={'status': calculated_status})
1459 data={'status': calculated_status})
1467 return True
1460 return True
1468 else:
1461 else:
1469 log.warning('No permissions for user %s to delete comment_id: %s',
1462 log.warning('No permissions for user %s to delete comment_id: %s',
1470 self._rhodecode_db_user, comment_id)
1463 self._rhodecode_db_user, comment_id)
1471 raise HTTPNotFound()
1464 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now