##// END OF EJS Templates
path-permissions: Initial support for path-based permissions
idlsoft -
r2618:940ad8b4 default
parent child Browse files
Show More
@@ -1,564 +1,616 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2018 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.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26
26
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h, diffs
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.model import repo
30 from rhodecode.model import repo
31 from rhodecode.model import repo_group
31 from rhodecode.model import repo_group
32 from rhodecode.model import user_group
32 from rhodecode.model import user_group
33 from rhodecode.model import user
33 from rhodecode.model import user
34 from rhodecode.model.db import User
34 from rhodecode.model.db import User
35 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 ADMIN_PREFIX = '/_admin'
40 ADMIN_PREFIX = '/_admin'
41 STATIC_FILE_PREFIX = '/_static'
41 STATIC_FILE_PREFIX = '/_static'
42
42
43 URL_NAME_REQUIREMENTS = {
43 URL_NAME_REQUIREMENTS = {
44 # group name can have a slash in them, but they must not end with a slash
44 # group name can have a slash in them, but they must not end with a slash
45 'group_name': r'.*?[^/]',
45 'group_name': r'.*?[^/]',
46 'repo_group_name': r'.*?[^/]',
46 'repo_group_name': r'.*?[^/]',
47 # repo names can have a slash in them, but they must not end with a slash
47 # repo names can have a slash in them, but they must not end with a slash
48 'repo_name': r'.*?[^/]',
48 'repo_name': r'.*?[^/]',
49 # file path eats up everything at the end
49 # file path eats up everything at the end
50 'f_path': r'.*',
50 'f_path': r'.*',
51 # reference types
51 # reference types
52 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
53 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
54 }
54 }
55
55
56
56
57 def add_route_with_slash(config,name, pattern, **kw):
57 def add_route_with_slash(config,name, pattern, **kw):
58 config.add_route(name, pattern, **kw)
58 config.add_route(name, pattern, **kw)
59 if not pattern.endswith('/'):
59 if not pattern.endswith('/'):
60 config.add_route(name + '_slash', pattern + '/', **kw)
60 config.add_route(name + '_slash', pattern + '/', **kw)
61
61
62
62
63 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
63 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
64 """
64 """
65 Adds regex requirements to pyramid routes using a mapping dict
65 Adds regex requirements to pyramid routes using a mapping dict
66 e.g::
66 e.g::
67 add_route_requirements('{repo_name}/settings')
67 add_route_requirements('{repo_name}/settings')
68 """
68 """
69 for key, regex in requirements.items():
69 for key, regex in requirements.items():
70 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
70 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
71 return route_path
71 return route_path
72
72
73
73
74 def get_format_ref_id(repo):
74 def get_format_ref_id(repo):
75 """Returns a `repo` specific reference formatter function"""
75 """Returns a `repo` specific reference formatter function"""
76 if h.is_svn(repo):
76 if h.is_svn(repo):
77 return _format_ref_id_svn
77 return _format_ref_id_svn
78 else:
78 else:
79 return _format_ref_id
79 return _format_ref_id
80
80
81
81
82 def _format_ref_id(name, raw_id):
82 def _format_ref_id(name, raw_id):
83 """Default formatting of a given reference `name`"""
83 """Default formatting of a given reference `name`"""
84 return name
84 return name
85
85
86
86
87 def _format_ref_id_svn(name, raw_id):
87 def _format_ref_id_svn(name, raw_id):
88 """Special way of formatting a reference for Subversion including path"""
88 """Special way of formatting a reference for Subversion including path"""
89 return '%s@%s' % (name, raw_id)
89 return '%s@%s' % (name, raw_id)
90
90
91
91
92 class TemplateArgs(StrictAttributeDict):
92 class TemplateArgs(StrictAttributeDict):
93 pass
93 pass
94
94
95
95
96 class BaseAppView(object):
96 class BaseAppView(object):
97
97
98 def __init__(self, context, request):
98 def __init__(self, context, request):
99 self.request = request
99 self.request = request
100 self.context = context
100 self.context = context
101 self.session = request.session
101 self.session = request.session
102 self._rhodecode_user = request.user # auth user
102 self._rhodecode_user = request.user # auth user
103 self._rhodecode_db_user = self._rhodecode_user.get_instance()
103 self._rhodecode_db_user = self._rhodecode_user.get_instance()
104 self._maybe_needs_password_change(
104 self._maybe_needs_password_change(
105 request.matched_route.name, self._rhodecode_db_user)
105 request.matched_route.name, self._rhodecode_db_user)
106
106
107 def _maybe_needs_password_change(self, view_name, user_obj):
107 def _maybe_needs_password_change(self, view_name, user_obj):
108 log.debug('Checking if user %s needs password change on view %s',
108 log.debug('Checking if user %s needs password change on view %s',
109 user_obj, view_name)
109 user_obj, view_name)
110 skip_user_views = [
110 skip_user_views = [
111 'logout', 'login',
111 'logout', 'login',
112 'my_account_password', 'my_account_password_update'
112 'my_account_password', 'my_account_password_update'
113 ]
113 ]
114
114
115 if not user_obj:
115 if not user_obj:
116 return
116 return
117
117
118 if user_obj.username == User.DEFAULT_USER:
118 if user_obj.username == User.DEFAULT_USER:
119 return
119 return
120
120
121 now = time.time()
121 now = time.time()
122 should_change = user_obj.user_data.get('force_password_change')
122 should_change = user_obj.user_data.get('force_password_change')
123 change_after = safe_int(should_change) or 0
123 change_after = safe_int(should_change) or 0
124 if should_change and now > change_after:
124 if should_change and now > change_after:
125 log.debug('User %s requires password change', user_obj)
125 log.debug('User %s requires password change', user_obj)
126 h.flash('You are required to change your password', 'warning',
126 h.flash('You are required to change your password', 'warning',
127 ignore_duplicate=True)
127 ignore_duplicate=True)
128
128
129 if view_name not in skip_user_views:
129 if view_name not in skip_user_views:
130 raise HTTPFound(
130 raise HTTPFound(
131 self.request.route_path('my_account_password'))
131 self.request.route_path('my_account_password'))
132
132
133 def _log_creation_exception(self, e, repo_name):
133 def _log_creation_exception(self, e, repo_name):
134 _ = self.request.translate
134 _ = self.request.translate
135 reason = None
135 reason = None
136 if len(e.args) == 2:
136 if len(e.args) == 2:
137 reason = e.args[1]
137 reason = e.args[1]
138
138
139 if reason == 'INVALID_CERTIFICATE':
139 if reason == 'INVALID_CERTIFICATE':
140 log.exception(
140 log.exception(
141 'Exception creating a repository: invalid certificate')
141 'Exception creating a repository: invalid certificate')
142 msg = (_('Error creating repository %s: invalid certificate')
142 msg = (_('Error creating repository %s: invalid certificate')
143 % repo_name)
143 % repo_name)
144 else:
144 else:
145 log.exception("Exception creating a repository")
145 log.exception("Exception creating a repository")
146 msg = (_('Error creating repository %s')
146 msg = (_('Error creating repository %s')
147 % repo_name)
147 % repo_name)
148 return msg
148 return msg
149
149
150 def _get_local_tmpl_context(self, include_app_defaults=True):
150 def _get_local_tmpl_context(self, include_app_defaults=True):
151 c = TemplateArgs()
151 c = TemplateArgs()
152 c.auth_user = self.request.user
152 c.auth_user = self.request.user
153 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
153 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
154 c.rhodecode_user = self.request.user
154 c.rhodecode_user = self.request.user
155
155
156 if include_app_defaults:
156 if include_app_defaults:
157 from rhodecode.lib.base import attach_context_attributes
157 from rhodecode.lib.base import attach_context_attributes
158 attach_context_attributes(c, self.request, self.request.user.user_id)
158 attach_context_attributes(c, self.request, self.request.user.user_id)
159
159
160 return c
160 return c
161
161
162 def _get_template_context(self, tmpl_args, **kwargs):
162 def _get_template_context(self, tmpl_args, **kwargs):
163
163
164 local_tmpl_args = {
164 local_tmpl_args = {
165 'defaults': {},
165 'defaults': {},
166 'errors': {},
166 'errors': {},
167 'c': tmpl_args
167 'c': tmpl_args
168 }
168 }
169 local_tmpl_args.update(kwargs)
169 local_tmpl_args.update(kwargs)
170 return local_tmpl_args
170 return local_tmpl_args
171
171
172 def load_default_context(self):
172 def load_default_context(self):
173 """
173 """
174 example:
174 example:
175
175
176 def load_default_context(self):
176 def load_default_context(self):
177 c = self._get_local_tmpl_context()
177 c = self._get_local_tmpl_context()
178 c.custom_var = 'foobar'
178 c.custom_var = 'foobar'
179
179
180 return c
180 return c
181 """
181 """
182 raise NotImplementedError('Needs implementation in view class')
182 raise NotImplementedError('Needs implementation in view class')
183
183
184
184
185 class RepoAppView(BaseAppView):
185 class RepoAppView(BaseAppView):
186
186
187 def __init__(self, context, request):
187 def __init__(self, context, request):
188 super(RepoAppView, self).__init__(context, request)
188 super(RepoAppView, self).__init__(context, request)
189 self.db_repo = request.db_repo
189 self.db_repo = request.db_repo
190 self.db_repo_name = self.db_repo.repo_name
190 self.db_repo_name = self.db_repo.repo_name
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192
192
193 def _handle_missing_requirements(self, error):
193 def _handle_missing_requirements(self, error):
194 log.error(
194 log.error(
195 'Requirements are missing for repository %s: %s',
195 'Requirements are missing for repository %s: %s',
196 self.db_repo_name, error.message)
196 self.db_repo_name, error.message)
197
197
198 def _get_local_tmpl_context(self, include_app_defaults=True):
198 def _get_local_tmpl_context(self, include_app_defaults=True):
199 _ = self.request.translate
199 _ = self.request.translate
200 c = super(RepoAppView, self)._get_local_tmpl_context(
200 c = super(RepoAppView, self)._get_local_tmpl_context(
201 include_app_defaults=include_app_defaults)
201 include_app_defaults=include_app_defaults)
202
202
203 # register common vars for this type of view
203 # register common vars for this type of view
204 c.rhodecode_db_repo = self.db_repo
204 c.rhodecode_db_repo = self.db_repo
205 c.repo_name = self.db_repo_name
205 c.repo_name = self.db_repo_name
206 c.repository_pull_requests = self.db_repo_pull_requests
206 c.repository_pull_requests = self.db_repo_pull_requests
207
207
208 c.repository_requirements_missing = False
208 c.repository_requirements_missing = False
209 try:
209 try:
210 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
210 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
211 self.path_filter = PathFilter(self.rhodecode_vcs_repo.get_path_permissions(c.auth_user.username))
211 except RepositoryRequirementError as e:
212 except RepositoryRequirementError as e:
212 c.repository_requirements_missing = True
213 c.repository_requirements_missing = True
213 self._handle_missing_requirements(e)
214 self._handle_missing_requirements(e)
214 self.rhodecode_vcs_repo = None
215 self.rhodecode_vcs_repo = None
216 self.path_filter = None
217
218 c.path_filter = self.path_filter # used by atom_feed_entry.mako
215
219
216 if (not c.repository_requirements_missing
220 if (not c.repository_requirements_missing
217 and self.rhodecode_vcs_repo is None):
221 and self.rhodecode_vcs_repo is None):
218 # unable to fetch this repo as vcs instance, report back to user
222 # unable to fetch this repo as vcs instance, report back to user
219 h.flash(_(
223 h.flash(_(
220 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
224 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
221 "Please check if it exist, or is not damaged.") %
225 "Please check if it exist, or is not damaged.") %
222 {'repo_name': c.repo_name},
226 {'repo_name': c.repo_name},
223 category='error', ignore_duplicate=True)
227 category='error', ignore_duplicate=True)
224 raise HTTPFound(h.route_path('home'))
228 raise HTTPFound(h.route_path('home'))
225
229
226 return c
230 return c
227
231
228 def _get_f_path(self, matchdict, default=None):
232 def _get_f_path(self, matchdict, default=None):
229 f_path = matchdict.get('f_path')
233 f_path = matchdict.get('f_path')
230 if f_path:
234 if f_path:
231 # fix for multiple initial slashes that causes errors for GIT
235 # fix for multiple initial slashes that causes errors for GIT
232 return f_path.lstrip('/')
236 return self.path_filter.assert_path_permissions(f_path.lstrip('/'))
237
238 return self.path_filter.assert_path_permissions(default)
239
240
241 class PathFilter(object):
242
243 # Expects and instance of BasePathPermissionChecker or None
244 def __init__(self, permission_checker):
245 self.permission_checker = permission_checker
246
247 def assert_path_permissions(self, path):
248 if path and self.permission_checker and not self.permission_checker.has_access(path):
249 raise HTTPForbidden()
250 return path
233
251
234 return default
252 def filter_patchset(self, patchset):
253 if not self.permission_checker or not patchset:
254 return patchset, False
255 had_filtered = False
256 filtered_patchset = []
257 for patch in patchset:
258 filename = patch.get('filename', None)
259 if not filename or self.permission_checker.has_access(filename):
260 filtered_patchset.append(patch)
261 else:
262 had_filtered = True
263 if had_filtered:
264 if isinstance(patchset, diffs.LimitedDiffContainer):
265 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
266 return filtered_patchset, True
267 else:
268 return patchset, False
269
270 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
271 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
272 result = diffset.render_patchset(filtered_patchset, source_ref=source_ref, target_ref=target_ref)
273 result.has_hidden_changes = has_hidden_changes
274 return result
275
276 def get_raw_patch(self, diff_processor):
277 if self.permission_checker is None:
278 return diff_processor.as_raw()
279 elif self.permission_checker.has_full_access:
280 return diff_processor.as_raw()
281 else:
282 return '# Repository has user-specific filters, raw patch generation is disabled.'
283
284 @property
285 def is_enabled(self):
286 return self.permission_checker is not None
235
287
236
288
237 class RepoGroupAppView(BaseAppView):
289 class RepoGroupAppView(BaseAppView):
238 def __init__(self, context, request):
290 def __init__(self, context, request):
239 super(RepoGroupAppView, self).__init__(context, request)
291 super(RepoGroupAppView, self).__init__(context, request)
240 self.db_repo_group = request.db_repo_group
292 self.db_repo_group = request.db_repo_group
241 self.db_repo_group_name = self.db_repo_group.group_name
293 self.db_repo_group_name = self.db_repo_group.group_name
242
294
243 def _revoke_perms_on_yourself(self, form_result):
295 def _revoke_perms_on_yourself(self, form_result):
244 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
296 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
245 form_result['perm_updates'])
297 form_result['perm_updates'])
246 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
298 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
247 form_result['perm_additions'])
299 form_result['perm_additions'])
248 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
300 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
249 form_result['perm_deletions'])
301 form_result['perm_deletions'])
250 admin_perm = 'group.admin'
302 admin_perm = 'group.admin'
251 if _updates and _updates[0][1] != admin_perm or \
303 if _updates and _updates[0][1] != admin_perm or \
252 _additions and _additions[0][1] != admin_perm or \
304 _additions and _additions[0][1] != admin_perm or \
253 _deletions and _deletions[0][1] != admin_perm:
305 _deletions and _deletions[0][1] != admin_perm:
254 return True
306 return True
255 return False
307 return False
256
308
257
309
258 class UserGroupAppView(BaseAppView):
310 class UserGroupAppView(BaseAppView):
259 def __init__(self, context, request):
311 def __init__(self, context, request):
260 super(UserGroupAppView, self).__init__(context, request)
312 super(UserGroupAppView, self).__init__(context, request)
261 self.db_user_group = request.db_user_group
313 self.db_user_group = request.db_user_group
262 self.db_user_group_name = self.db_user_group.users_group_name
314 self.db_user_group_name = self.db_user_group.users_group_name
263
315
264
316
265 class UserAppView(BaseAppView):
317 class UserAppView(BaseAppView):
266 def __init__(self, context, request):
318 def __init__(self, context, request):
267 super(UserAppView, self).__init__(context, request)
319 super(UserAppView, self).__init__(context, request)
268 self.db_user = request.db_user
320 self.db_user = request.db_user
269 self.db_user_id = self.db_user.user_id
321 self.db_user_id = self.db_user.user_id
270
322
271 _ = self.request.translate
323 _ = self.request.translate
272 if not request.db_user_supports_default:
324 if not request.db_user_supports_default:
273 if self.db_user.username == User.DEFAULT_USER:
325 if self.db_user.username == User.DEFAULT_USER:
274 h.flash(_("Editing user `{}` is disabled.".format(
326 h.flash(_("Editing user `{}` is disabled.".format(
275 User.DEFAULT_USER)), category='warning')
327 User.DEFAULT_USER)), category='warning')
276 raise HTTPFound(h.route_path('users'))
328 raise HTTPFound(h.route_path('users'))
277
329
278
330
279 class DataGridAppView(object):
331 class DataGridAppView(object):
280 """
332 """
281 Common class to have re-usable grid rendering components
333 Common class to have re-usable grid rendering components
282 """
334 """
283
335
284 def _extract_ordering(self, request, column_map=None):
336 def _extract_ordering(self, request, column_map=None):
285 column_map = column_map or {}
337 column_map = column_map or {}
286 column_index = safe_int(request.GET.get('order[0][column]'))
338 column_index = safe_int(request.GET.get('order[0][column]'))
287 order_dir = request.GET.get(
339 order_dir = request.GET.get(
288 'order[0][dir]', 'desc')
340 'order[0][dir]', 'desc')
289 order_by = request.GET.get(
341 order_by = request.GET.get(
290 'columns[%s][data][sort]' % column_index, 'name_raw')
342 'columns[%s][data][sort]' % column_index, 'name_raw')
291
343
292 # translate datatable to DB columns
344 # translate datatable to DB columns
293 order_by = column_map.get(order_by) or order_by
345 order_by = column_map.get(order_by) or order_by
294
346
295 search_q = request.GET.get('search[value]')
347 search_q = request.GET.get('search[value]')
296 return search_q, order_by, order_dir
348 return search_q, order_by, order_dir
297
349
298 def _extract_chunk(self, request):
350 def _extract_chunk(self, request):
299 start = safe_int(request.GET.get('start'), 0)
351 start = safe_int(request.GET.get('start'), 0)
300 length = safe_int(request.GET.get('length'), 25)
352 length = safe_int(request.GET.get('length'), 25)
301 draw = safe_int(request.GET.get('draw'))
353 draw = safe_int(request.GET.get('draw'))
302 return draw, start, length
354 return draw, start, length
303
355
304 def _get_order_col(self, order_by, model):
356 def _get_order_col(self, order_by, model):
305 if isinstance(order_by, basestring):
357 if isinstance(order_by, basestring):
306 try:
358 try:
307 return operator.attrgetter(order_by)(model)
359 return operator.attrgetter(order_by)(model)
308 except AttributeError:
360 except AttributeError:
309 return None
361 return None
310 else:
362 else:
311 return order_by
363 return order_by
312
364
313
365
314 class BaseReferencesView(RepoAppView):
366 class BaseReferencesView(RepoAppView):
315 """
367 """
316 Base for reference view for branches, tags and bookmarks.
368 Base for reference view for branches, tags and bookmarks.
317 """
369 """
318 def load_default_context(self):
370 def load_default_context(self):
319 c = self._get_local_tmpl_context()
371 c = self._get_local_tmpl_context()
320
372
321
373
322 return c
374 return c
323
375
324 def load_refs_context(self, ref_items, partials_template):
376 def load_refs_context(self, ref_items, partials_template):
325 _render = self.request.get_partial_renderer(partials_template)
377 _render = self.request.get_partial_renderer(partials_template)
326 pre_load = ["author", "date", "message"]
378 pre_load = ["author", "date", "message"]
327
379
328 is_svn = h.is_svn(self.rhodecode_vcs_repo)
380 is_svn = h.is_svn(self.rhodecode_vcs_repo)
329 is_hg = h.is_hg(self.rhodecode_vcs_repo)
381 is_hg = h.is_hg(self.rhodecode_vcs_repo)
330
382
331 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
383 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
332
384
333 closed_refs = {}
385 closed_refs = {}
334 if is_hg:
386 if is_hg:
335 closed_refs = self.rhodecode_vcs_repo.branches_closed
387 closed_refs = self.rhodecode_vcs_repo.branches_closed
336
388
337 data = []
389 data = []
338 for ref_name, commit_id in ref_items:
390 for ref_name, commit_id in ref_items:
339 commit = self.rhodecode_vcs_repo.get_commit(
391 commit = self.rhodecode_vcs_repo.get_commit(
340 commit_id=commit_id, pre_load=pre_load)
392 commit_id=commit_id, pre_load=pre_load)
341 closed = ref_name in closed_refs
393 closed = ref_name in closed_refs
342
394
343 # TODO: johbo: Unify generation of reference links
395 # TODO: johbo: Unify generation of reference links
344 use_commit_id = '/' in ref_name or is_svn
396 use_commit_id = '/' in ref_name or is_svn
345
397
346 if use_commit_id:
398 if use_commit_id:
347 files_url = h.route_path(
399 files_url = h.route_path(
348 'repo_files',
400 'repo_files',
349 repo_name=self.db_repo_name,
401 repo_name=self.db_repo_name,
350 f_path=ref_name if is_svn else '',
402 f_path=ref_name if is_svn else '',
351 commit_id=commit_id)
403 commit_id=commit_id)
352
404
353 else:
405 else:
354 files_url = h.route_path(
406 files_url = h.route_path(
355 'repo_files',
407 'repo_files',
356 repo_name=self.db_repo_name,
408 repo_name=self.db_repo_name,
357 f_path=ref_name if is_svn else '',
409 f_path=ref_name if is_svn else '',
358 commit_id=ref_name,
410 commit_id=ref_name,
359 _query=dict(at=ref_name))
411 _query=dict(at=ref_name))
360
412
361 data.append({
413 data.append({
362 "name": _render('name', ref_name, files_url, closed),
414 "name": _render('name', ref_name, files_url, closed),
363 "name_raw": ref_name,
415 "name_raw": ref_name,
364 "date": _render('date', commit.date),
416 "date": _render('date', commit.date),
365 "date_raw": datetime_to_time(commit.date),
417 "date_raw": datetime_to_time(commit.date),
366 "author": _render('author', commit.author),
418 "author": _render('author', commit.author),
367 "commit": _render(
419 "commit": _render(
368 'commit', commit.message, commit.raw_id, commit.idx),
420 'commit', commit.message, commit.raw_id, commit.idx),
369 "commit_raw": commit.idx,
421 "commit_raw": commit.idx,
370 "compare": _render(
422 "compare": _render(
371 'compare', format_ref_id(ref_name, commit.raw_id)),
423 'compare', format_ref_id(ref_name, commit.raw_id)),
372 })
424 })
373
425
374 return data
426 return data
375
427
376
428
377 class RepoRoutePredicate(object):
429 class RepoRoutePredicate(object):
378 def __init__(self, val, config):
430 def __init__(self, val, config):
379 self.val = val
431 self.val = val
380
432
381 def text(self):
433 def text(self):
382 return 'repo_route = %s' % self.val
434 return 'repo_route = %s' % self.val
383
435
384 phash = text
436 phash = text
385
437
386 def __call__(self, info, request):
438 def __call__(self, info, request):
387
439
388 if hasattr(request, 'vcs_call'):
440 if hasattr(request, 'vcs_call'):
389 # skip vcs calls
441 # skip vcs calls
390 return
442 return
391
443
392 repo_name = info['match']['repo_name']
444 repo_name = info['match']['repo_name']
393 repo_model = repo.RepoModel()
445 repo_model = repo.RepoModel()
394 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
446 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
395
447
396 def redirect_if_creating(db_repo):
448 def redirect_if_creating(db_repo):
397 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
449 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
398 raise HTTPFound(
450 raise HTTPFound(
399 request.route_path('repo_creating',
451 request.route_path('repo_creating',
400 repo_name=db_repo.repo_name))
452 repo_name=db_repo.repo_name))
401
453
402 if by_name_match:
454 if by_name_match:
403 # register this as request object we can re-use later
455 # register this as request object we can re-use later
404 request.db_repo = by_name_match
456 request.db_repo = by_name_match
405 redirect_if_creating(by_name_match)
457 redirect_if_creating(by_name_match)
406 return True
458 return True
407
459
408 by_id_match = repo_model.get_repo_by_id(repo_name)
460 by_id_match = repo_model.get_repo_by_id(repo_name)
409 if by_id_match:
461 if by_id_match:
410 request.db_repo = by_id_match
462 request.db_repo = by_id_match
411 redirect_if_creating(by_id_match)
463 redirect_if_creating(by_id_match)
412 return True
464 return True
413
465
414 return False
466 return False
415
467
416
468
417 class RepoTypeRoutePredicate(object):
469 class RepoTypeRoutePredicate(object):
418 def __init__(self, val, config):
470 def __init__(self, val, config):
419 self.val = val or ['hg', 'git', 'svn']
471 self.val = val or ['hg', 'git', 'svn']
420
472
421 def text(self):
473 def text(self):
422 return 'repo_accepted_type = %s' % self.val
474 return 'repo_accepted_type = %s' % self.val
423
475
424 phash = text
476 phash = text
425
477
426 def __call__(self, info, request):
478 def __call__(self, info, request):
427 if hasattr(request, 'vcs_call'):
479 if hasattr(request, 'vcs_call'):
428 # skip vcs calls
480 # skip vcs calls
429 return
481 return
430
482
431 rhodecode_db_repo = request.db_repo
483 rhodecode_db_repo = request.db_repo
432
484
433 log.debug(
485 log.debug(
434 '%s checking repo type for %s in %s',
486 '%s checking repo type for %s in %s',
435 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
487 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
436
488
437 if rhodecode_db_repo.repo_type in self.val:
489 if rhodecode_db_repo.repo_type in self.val:
438 return True
490 return True
439 else:
491 else:
440 log.warning('Current view is not supported for repo type:%s',
492 log.warning('Current view is not supported for repo type:%s',
441 rhodecode_db_repo.repo_type)
493 rhodecode_db_repo.repo_type)
442 #
494 #
443 # h.flash(h.literal(
495 # h.flash(h.literal(
444 # _('Action not supported for %s.' % rhodecode_repo.alias)),
496 # _('Action not supported for %s.' % rhodecode_repo.alias)),
445 # category='warning')
497 # category='warning')
446 # return redirect(
498 # return redirect(
447 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
499 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
448
500
449 return False
501 return False
450
502
451
503
452 class RepoGroupRoutePredicate(object):
504 class RepoGroupRoutePredicate(object):
453 def __init__(self, val, config):
505 def __init__(self, val, config):
454 self.val = val
506 self.val = val
455
507
456 def text(self):
508 def text(self):
457 return 'repo_group_route = %s' % self.val
509 return 'repo_group_route = %s' % self.val
458
510
459 phash = text
511 phash = text
460
512
461 def __call__(self, info, request):
513 def __call__(self, info, request):
462 if hasattr(request, 'vcs_call'):
514 if hasattr(request, 'vcs_call'):
463 # skip vcs calls
515 # skip vcs calls
464 return
516 return
465
517
466 repo_group_name = info['match']['repo_group_name']
518 repo_group_name = info['match']['repo_group_name']
467 repo_group_model = repo_group.RepoGroupModel()
519 repo_group_model = repo_group.RepoGroupModel()
468 by_name_match = repo_group_model.get_by_group_name(
520 by_name_match = repo_group_model.get_by_group_name(
469 repo_group_name, cache=True)
521 repo_group_name, cache=True)
470
522
471 if by_name_match:
523 if by_name_match:
472 # register this as request object we can re-use later
524 # register this as request object we can re-use later
473 request.db_repo_group = by_name_match
525 request.db_repo_group = by_name_match
474 return True
526 return True
475
527
476 return False
528 return False
477
529
478
530
479 class UserGroupRoutePredicate(object):
531 class UserGroupRoutePredicate(object):
480 def __init__(self, val, config):
532 def __init__(self, val, config):
481 self.val = val
533 self.val = val
482
534
483 def text(self):
535 def text(self):
484 return 'user_group_route = %s' % self.val
536 return 'user_group_route = %s' % self.val
485
537
486 phash = text
538 phash = text
487
539
488 def __call__(self, info, request):
540 def __call__(self, info, request):
489 if hasattr(request, 'vcs_call'):
541 if hasattr(request, 'vcs_call'):
490 # skip vcs calls
542 # skip vcs calls
491 return
543 return
492
544
493 user_group_id = info['match']['user_group_id']
545 user_group_id = info['match']['user_group_id']
494 user_group_model = user_group.UserGroup()
546 user_group_model = user_group.UserGroup()
495 by_id_match = user_group_model.get(
547 by_id_match = user_group_model.get(
496 user_group_id, cache=True)
548 user_group_id, cache=True)
497
549
498 if by_id_match:
550 if by_id_match:
499 # register this as request object we can re-use later
551 # register this as request object we can re-use later
500 request.db_user_group = by_id_match
552 request.db_user_group = by_id_match
501 return True
553 return True
502
554
503 return False
555 return False
504
556
505
557
506 class UserRoutePredicateBase(object):
558 class UserRoutePredicateBase(object):
507 supports_default = None
559 supports_default = None
508
560
509 def __init__(self, val, config):
561 def __init__(self, val, config):
510 self.val = val
562 self.val = val
511
563
512 def text(self):
564 def text(self):
513 raise NotImplementedError()
565 raise NotImplementedError()
514
566
515 def __call__(self, info, request):
567 def __call__(self, info, request):
516 if hasattr(request, 'vcs_call'):
568 if hasattr(request, 'vcs_call'):
517 # skip vcs calls
569 # skip vcs calls
518 return
570 return
519
571
520 user_id = info['match']['user_id']
572 user_id = info['match']['user_id']
521 user_model = user.User()
573 user_model = user.User()
522 by_id_match = user_model.get(
574 by_id_match = user_model.get(
523 user_id, cache=True)
575 user_id, cache=True)
524
576
525 if by_id_match:
577 if by_id_match:
526 # register this as request object we can re-use later
578 # register this as request object we can re-use later
527 request.db_user = by_id_match
579 request.db_user = by_id_match
528 request.db_user_supports_default = self.supports_default
580 request.db_user_supports_default = self.supports_default
529 return True
581 return True
530
582
531 return False
583 return False
532
584
533
585
534 class UserRoutePredicate(UserRoutePredicateBase):
586 class UserRoutePredicate(UserRoutePredicateBase):
535 supports_default = False
587 supports_default = False
536
588
537 def text(self):
589 def text(self):
538 return 'user_route = %s' % self.val
590 return 'user_route = %s' % self.val
539
591
540 phash = text
592 phash = text
541
593
542
594
543 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
595 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
544 supports_default = True
596 supports_default = True
545
597
546 def text(self):
598 def text(self):
547 return 'user_with_default_route = %s' % self.val
599 return 'user_with_default_route = %s' % self.val
548
600
549 phash = text
601 phash = text
550
602
551
603
552 def includeme(config):
604 def includeme(config):
553 config.add_route_predicate(
605 config.add_route_predicate(
554 'repo_route', RepoRoutePredicate)
606 'repo_route', RepoRoutePredicate)
555 config.add_route_predicate(
607 config.add_route_predicate(
556 'repo_accepted_types', RepoTypeRoutePredicate)
608 'repo_accepted_types', RepoTypeRoutePredicate)
557 config.add_route_predicate(
609 config.add_route_predicate(
558 'repo_group_route', RepoGroupRoutePredicate)
610 'repo_group_route', RepoGroupRoutePredicate)
559 config.add_route_predicate(
611 config.add_route_predicate(
560 'user_group_route', UserGroupRoutePredicate)
612 'user_group_route', UserGroupRoutePredicate)
561 config.add_route_predicate(
613 config.add_route_predicate(
562 'user_route_with_default', UserRouteWithDefaultPredicate)
614 'user_route_with_default', UserRouteWithDefaultPredicate)
563 config.add_route_predicate(
615 config.add_route_predicate(
564 'user_route', UserRoutePredicate) No newline at end of file
616 'user_route', UserRoutePredicate)
@@ -1,562 +1,562 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31
31
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
35
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 import rhodecode.lib.helpers as h
38 import rhodecode.lib.helpers as h
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 def _update_with_GET(params, request):
52 def _update_with_GET(params, request):
53 for k in ['diff1', 'diff2', 'diff']:
53 for k in ['diff1', 'diff2', 'diff']:
54 params[k] += request.GET.getall(k)
54 params[k] += request.GET.getall(k)
55
55
56
56
57 def get_ignore_ws(fid, request):
57 def get_ignore_ws(fid, request):
58 ig_ws_global = request.GET.get('ignorews')
58 ig_ws_global = request.GET.get('ignorews')
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 if ig_ws:
60 if ig_ws:
61 try:
61 try:
62 return int(ig_ws[0].split(':')[-1])
62 return int(ig_ws[0].split(':')[-1])
63 except Exception:
63 except Exception:
64 pass
64 pass
65 return ig_ws_global
65 return ig_ws_global
66
66
67
67
68 def _ignorews_url(request, fileid=None):
68 def _ignorews_url(request, fileid=None):
69 _ = request.translate
69 _ = request.translate
70 fileid = str(fileid) if fileid else None
70 fileid = str(fileid) if fileid else None
71 params = collections.defaultdict(list)
71 params = collections.defaultdict(list)
72 _update_with_GET(params, request)
72 _update_with_GET(params, request)
73 label = _('Show whitespace')
73 label = _('Show whitespace')
74 tooltiplbl = _('Show whitespace for all diffs')
74 tooltiplbl = _('Show whitespace for all diffs')
75 ig_ws = get_ignore_ws(fileid, request)
75 ig_ws = get_ignore_ws(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
77
77
78 if ig_ws is None:
78 if ig_ws is None:
79 params['ignorews'] += [1]
79 params['ignorews'] += [1]
80 label = _('Ignore whitespace')
80 label = _('Ignore whitespace')
81 tooltiplbl = _('Ignore whitespace for all diffs')
81 tooltiplbl = _('Ignore whitespace for all diffs')
82 ctx_key = 'context'
82 ctx_key = 'context'
83 ctx_val = ln_ctx
83 ctx_val = ln_ctx
84
84
85 # if we have passed in ln_ctx pass it along to our params
85 # if we have passed in ln_ctx pass it along to our params
86 if ln_ctx:
86 if ln_ctx:
87 params[ctx_key] += [ctx_val]
87 params[ctx_key] += [ctx_val]
88
88
89 if fileid:
89 if fileid:
90 params['anchor'] = 'a_' + fileid
90 params['anchor'] = 'a_' + fileid
91 return h.link_to(label, request.current_route_path(_query=params),
91 return h.link_to(label, request.current_route_path(_query=params),
92 title=tooltiplbl, class_='tooltip')
92 title=tooltiplbl, class_='tooltip')
93
93
94
94
95 def get_line_ctx(fid, request):
95 def get_line_ctx(fid, request):
96 ln_ctx_global = request.GET.get('context')
96 ln_ctx_global = request.GET.get('context')
97 if fid:
97 if fid:
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 else:
99 else:
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 if ln_ctx:
102 if ln_ctx:
103 ln_ctx = [ln_ctx]
103 ln_ctx = [ln_ctx]
104
104
105 if ln_ctx:
105 if ln_ctx:
106 retval = ln_ctx[0].split(':')[-1]
106 retval = ln_ctx[0].split(':')[-1]
107 else:
107 else:
108 retval = ln_ctx_global
108 retval = ln_ctx_global
109
109
110 try:
110 try:
111 return int(retval)
111 return int(retval)
112 except Exception:
112 except Exception:
113 return 3
113 return 3
114
114
115
115
116 def _context_url(request, fileid=None):
116 def _context_url(request, fileid=None):
117 """
117 """
118 Generates a url for context lines.
118 Generates a url for context lines.
119
119
120 :param fileid:
120 :param fileid:
121 """
121 """
122
122
123 _ = request.translate
123 _ = request.translate
124 fileid = str(fileid) if fileid else None
124 fileid = str(fileid) if fileid else None
125 ig_ws = get_ignore_ws(fileid, request)
125 ig_ws = get_ignore_ws(fileid, request)
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127
127
128 params = collections.defaultdict(list)
128 params = collections.defaultdict(list)
129 _update_with_GET(params, request)
129 _update_with_GET(params, request)
130
130
131 if ln_ctx > 0:
131 if ln_ctx > 0:
132 params['context'] += [ln_ctx]
132 params['context'] += [ln_ctx]
133
133
134 if ig_ws:
134 if ig_ws:
135 ig_ws_key = 'ignorews'
135 ig_ws_key = 'ignorews'
136 ig_ws_val = 1
136 ig_ws_val = 1
137 params[ig_ws_key] += [ig_ws_val]
137 params[ig_ws_key] += [ig_ws_val]
138
138
139 lbl = _('Increase context')
139 lbl = _('Increase context')
140 tooltiplbl = _('Increase context for all diffs')
140 tooltiplbl = _('Increase context for all diffs')
141
141
142 if fileid:
142 if fileid:
143 params['anchor'] = 'a_' + fileid
143 params['anchor'] = 'a_' + fileid
144 return h.link_to(lbl, request.current_route_path(_query=params),
144 return h.link_to(lbl, request.current_route_path(_query=params),
145 title=tooltiplbl, class_='tooltip')
145 title=tooltiplbl, class_='tooltip')
146
146
147
147
148 class RepoCommitsView(RepoAppView):
148 class RepoCommitsView(RepoAppView):
149 def load_default_context(self):
149 def load_default_context(self):
150 c = self._get_local_tmpl_context(include_app_defaults=True)
150 c = self._get_local_tmpl_context(include_app_defaults=True)
151 c.rhodecode_repo = self.rhodecode_vcs_repo
151 c.rhodecode_repo = self.rhodecode_vcs_repo
152
152
153 return c
153 return c
154
154
155 def _commit(self, commit_id_range, method):
155 def _commit(self, commit_id_range, method):
156 _ = self.request.translate
156 _ = self.request.translate
157 c = self.load_default_context()
157 c = self.load_default_context()
158 c.ignorews_url = _ignorews_url
158 c.ignorews_url = _ignorews_url
159 c.context_url = _context_url
159 c.context_url = _context_url
160 c.fulldiff = self.request.GET.get('fulldiff')
160 c.fulldiff = self.request.GET.get('fulldiff')
161
161
162 # fetch global flags of ignore ws or context lines
162 # fetch global flags of ignore ws or context lines
163 context_lcl = get_line_ctx('', self.request)
163 context_lcl = get_line_ctx('', self.request)
164 ign_whitespace_lcl = get_ignore_ws('', self.request)
164 ign_whitespace_lcl = get_ignore_ws('', self.request)
165
165
166 # diff_limit will cut off the whole diff if the limit is applied
166 # diff_limit will cut off the whole diff if the limit is applied
167 # otherwise it will just hide the big files from the front-end
167 # otherwise it will just hide the big files from the front-end
168 diff_limit = c.visual.cut_off_limit_diff
168 diff_limit = c.visual.cut_off_limit_diff
169 file_limit = c.visual.cut_off_limit_file
169 file_limit = c.visual.cut_off_limit_file
170
170
171 # get ranges of commit ids if preset
171 # get ranges of commit ids if preset
172 commit_range = commit_id_range.split('...')[:2]
172 commit_range = commit_id_range.split('...')[:2]
173
173
174 try:
174 try:
175 pre_load = ['affected_files', 'author', 'branch', 'date',
175 pre_load = ['affected_files', 'author', 'branch', 'date',
176 'message', 'parents']
176 'message', 'parents']
177
177
178 if len(commit_range) == 2:
178 if len(commit_range) == 2:
179 commits = self.rhodecode_vcs_repo.get_commits(
179 commits = self.rhodecode_vcs_repo.get_commits(
180 start_id=commit_range[0], end_id=commit_range[1],
180 start_id=commit_range[0], end_id=commit_range[1],
181 pre_load=pre_load)
181 pre_load=pre_load)
182 commits = list(commits)
182 commits = list(commits)
183 else:
183 else:
184 commits = [self.rhodecode_vcs_repo.get_commit(
184 commits = [self.rhodecode_vcs_repo.get_commit(
185 commit_id=commit_id_range, pre_load=pre_load)]
185 commit_id=commit_id_range, pre_load=pre_load)]
186
186
187 c.commit_ranges = commits
187 c.commit_ranges = commits
188 if not c.commit_ranges:
188 if not c.commit_ranges:
189 raise RepositoryError(
189 raise RepositoryError(
190 'The commit range returned an empty result')
190 'The commit range returned an empty result')
191 except CommitDoesNotExistError:
191 except CommitDoesNotExistError:
192 msg = _('No such commit exists for this repository')
192 msg = _('No such commit exists for this repository')
193 h.flash(msg, category='error')
193 h.flash(msg, category='error')
194 raise HTTPNotFound()
194 raise HTTPNotFound()
195 except Exception:
195 except Exception:
196 log.exception("General failure")
196 log.exception("General failure")
197 raise HTTPNotFound()
197 raise HTTPNotFound()
198
198
199 c.changes = OrderedDict()
199 c.changes = OrderedDict()
200 c.lines_added = 0
200 c.lines_added = 0
201 c.lines_deleted = 0
201 c.lines_deleted = 0
202
202
203 # auto collapse if we have more than limit
203 # auto collapse if we have more than limit
204 collapse_limit = diffs.DiffProcessor._collapse_commits_over
204 collapse_limit = diffs.DiffProcessor._collapse_commits_over
205 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
205 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
206
206
207 c.commit_statuses = ChangesetStatus.STATUSES
207 c.commit_statuses = ChangesetStatus.STATUSES
208 c.inline_comments = []
208 c.inline_comments = []
209 c.files = []
209 c.files = []
210
210
211 c.statuses = []
211 c.statuses = []
212 c.comments = []
212 c.comments = []
213 c.unresolved_comments = []
213 c.unresolved_comments = []
214 if len(c.commit_ranges) == 1:
214 if len(c.commit_ranges) == 1:
215 commit = c.commit_ranges[0]
215 commit = c.commit_ranges[0]
216 c.comments = CommentsModel().get_comments(
216 c.comments = CommentsModel().get_comments(
217 self.db_repo.repo_id,
217 self.db_repo.repo_id,
218 revision=commit.raw_id)
218 revision=commit.raw_id)
219 c.statuses.append(ChangesetStatusModel().get_status(
219 c.statuses.append(ChangesetStatusModel().get_status(
220 self.db_repo.repo_id, commit.raw_id))
220 self.db_repo.repo_id, commit.raw_id))
221 # comments from PR
221 # comments from PR
222 statuses = ChangesetStatusModel().get_statuses(
222 statuses = ChangesetStatusModel().get_statuses(
223 self.db_repo.repo_id, commit.raw_id,
223 self.db_repo.repo_id, commit.raw_id,
224 with_revisions=True)
224 with_revisions=True)
225 prs = set(st.pull_request for st in statuses
225 prs = set(st.pull_request for st in statuses
226 if st.pull_request is not None)
226 if st.pull_request is not None)
227 # from associated statuses, check the pull requests, and
227 # from associated statuses, check the pull requests, and
228 # show comments from them
228 # show comments from them
229 for pr in prs:
229 for pr in prs:
230 c.comments.extend(pr.comments)
230 c.comments.extend(pr.comments)
231
231
232 c.unresolved_comments = CommentsModel()\
232 c.unresolved_comments = CommentsModel()\
233 .get_commit_unresolved_todos(commit.raw_id)
233 .get_commit_unresolved_todos(commit.raw_id)
234
234
235 diff = None
235 diff = None
236 # Iterate over ranges (default commit view is always one commit)
236 # Iterate over ranges (default commit view is always one commit)
237 for commit in c.commit_ranges:
237 for commit in c.commit_ranges:
238 c.changes[commit.raw_id] = []
238 c.changes[commit.raw_id] = []
239
239
240 commit2 = commit
240 commit2 = commit
241 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
241 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
242
242
243 _diff = self.rhodecode_vcs_repo.get_diff(
243 _diff = self.rhodecode_vcs_repo.get_diff(
244 commit1, commit2,
244 commit1, commit2,
245 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
245 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
246 diff_processor = diffs.DiffProcessor(
246 diff_processor = diffs.DiffProcessor(
247 _diff, format='newdiff', diff_limit=diff_limit,
247 _diff, format='newdiff', diff_limit=diff_limit,
248 file_limit=file_limit, show_full_diff=c.fulldiff)
248 file_limit=file_limit, show_full_diff=c.fulldiff)
249
249
250 commit_changes = OrderedDict()
250 commit_changes = OrderedDict()
251 if method == 'show':
251 if method == 'show':
252 _parsed = diff_processor.prepare()
252 _parsed = diff_processor.prepare()
253 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
253 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
254
254
255 _parsed = diff_processor.prepare()
255 _parsed = diff_processor.prepare()
256
256
257 def _node_getter(commit):
257 def _node_getter(commit):
258 def get_node(fname):
258 def get_node(fname):
259 try:
259 try:
260 return commit.get_node(fname)
260 return commit.get_node(fname)
261 except NodeDoesNotExistError:
261 except NodeDoesNotExistError:
262 return None
262 return None
263 return get_node
263 return get_node
264
264
265 inline_comments = CommentsModel().get_inline_comments(
265 inline_comments = CommentsModel().get_inline_comments(
266 self.db_repo.repo_id, revision=commit.raw_id)
266 self.db_repo.repo_id, revision=commit.raw_id)
267 c.inline_cnt = CommentsModel().get_inline_comments_count(
267 c.inline_cnt = CommentsModel().get_inline_comments_count(
268 inline_comments)
268 inline_comments)
269
269
270 diffset = codeblocks.DiffSet(
270 diffset = codeblocks.DiffSet(
271 repo_name=self.db_repo_name,
271 repo_name=self.db_repo_name,
272 source_node_getter=_node_getter(commit1),
272 source_node_getter=_node_getter(commit1),
273 target_node_getter=_node_getter(commit2),
273 target_node_getter=_node_getter(commit2),
274 comments=inline_comments)
274 comments=inline_comments)
275 diffset = diffset.render_patchset(
275 diffset = self.path_filter.render_patchset_filtered(
276 _parsed, commit1.raw_id, commit2.raw_id)
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277
277
278 c.changes[commit.raw_id] = diffset
278 c.changes[commit.raw_id] = diffset
279 else:
279 else:
280 # downloads/raw we only need RAW diff nothing else
280 # downloads/raw we only need RAW diff nothing else
281 diff = diff_processor.as_raw()
281 diff = self.path_filter.get_raw_patch(diff_processor)
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
283
283
284 # sort comments by how they were generated
284 # sort comments by how they were generated
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286
286
287 if len(c.commit_ranges) == 1:
287 if len(c.commit_ranges) == 1:
288 c.commit = c.commit_ranges[0]
288 c.commit = c.commit_ranges[0]
289 c.parent_tmpl = ''.join(
289 c.parent_tmpl = ''.join(
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291
291
292 if method == 'download':
292 if method == 'download':
293 response = Response(diff)
293 response = Response(diff)
294 response.content_type = 'text/plain'
294 response.content_type = 'text/plain'
295 response.content_disposition = (
295 response.content_disposition = (
296 'attachment; filename=%s.diff' % commit_id_range[:12])
296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 return response
297 return response
298 elif method == 'patch':
298 elif method == 'patch':
299 c.diff = safe_unicode(diff)
299 c.diff = safe_unicode(diff)
300 patch = render(
300 patch = render(
301 'rhodecode:templates/changeset/patch_changeset.mako',
301 'rhodecode:templates/changeset/patch_changeset.mako',
302 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
303 response = Response(patch)
303 response = Response(patch)
304 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
305 return response
305 return response
306 elif method == 'raw':
306 elif method == 'raw':
307 response = Response(diff)
307 response = Response(diff)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'show':
310 elif method == 'show':
311 if len(c.commit_ranges) == 1:
311 if len(c.commit_ranges) == 1:
312 html = render(
312 html = render(
313 'rhodecode:templates/changeset/changeset.mako',
313 'rhodecode:templates/changeset/changeset.mako',
314 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
315 return Response(html)
315 return Response(html)
316 else:
316 else:
317 c.ancestor = None
317 c.ancestor = None
318 c.target_repo = self.db_repo
318 c.target_repo = self.db_repo
319 html = render(
319 html = render(
320 'rhodecode:templates/changeset/changeset_range.mako',
320 'rhodecode:templates/changeset/changeset_range.mako',
321 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
322 return Response(html)
322 return Response(html)
323
323
324 raise HTTPBadRequest()
324 raise HTTPBadRequest()
325
325
326 @LoginRequired()
326 @LoginRequired()
327 @HasRepoPermissionAnyDecorator(
327 @HasRepoPermissionAnyDecorator(
328 'repository.read', 'repository.write', 'repository.admin')
328 'repository.read', 'repository.write', 'repository.admin')
329 @view_config(
329 @view_config(
330 route_name='repo_commit', request_method='GET',
330 route_name='repo_commit', request_method='GET',
331 renderer=None)
331 renderer=None)
332 def repo_commit_show(self):
332 def repo_commit_show(self):
333 commit_id = self.request.matchdict['commit_id']
333 commit_id = self.request.matchdict['commit_id']
334 return self._commit(commit_id, method='show')
334 return self._commit(commit_id, method='show')
335
335
336 @LoginRequired()
336 @LoginRequired()
337 @HasRepoPermissionAnyDecorator(
337 @HasRepoPermissionAnyDecorator(
338 'repository.read', 'repository.write', 'repository.admin')
338 'repository.read', 'repository.write', 'repository.admin')
339 @view_config(
339 @view_config(
340 route_name='repo_commit_raw', request_method='GET',
340 route_name='repo_commit_raw', request_method='GET',
341 renderer=None)
341 renderer=None)
342 @view_config(
342 @view_config(
343 route_name='repo_commit_raw_deprecated', request_method='GET',
343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 renderer=None)
344 renderer=None)
345 def repo_commit_raw(self):
345 def repo_commit_raw(self):
346 commit_id = self.request.matchdict['commit_id']
346 commit_id = self.request.matchdict['commit_id']
347 return self._commit(commit_id, method='raw')
347 return self._commit(commit_id, method='raw')
348
348
349 @LoginRequired()
349 @LoginRequired()
350 @HasRepoPermissionAnyDecorator(
350 @HasRepoPermissionAnyDecorator(
351 'repository.read', 'repository.write', 'repository.admin')
351 'repository.read', 'repository.write', 'repository.admin')
352 @view_config(
352 @view_config(
353 route_name='repo_commit_patch', request_method='GET',
353 route_name='repo_commit_patch', request_method='GET',
354 renderer=None)
354 renderer=None)
355 def repo_commit_patch(self):
355 def repo_commit_patch(self):
356 commit_id = self.request.matchdict['commit_id']
356 commit_id = self.request.matchdict['commit_id']
357 return self._commit(commit_id, method='patch')
357 return self._commit(commit_id, method='patch')
358
358
359 @LoginRequired()
359 @LoginRequired()
360 @HasRepoPermissionAnyDecorator(
360 @HasRepoPermissionAnyDecorator(
361 'repository.read', 'repository.write', 'repository.admin')
361 'repository.read', 'repository.write', 'repository.admin')
362 @view_config(
362 @view_config(
363 route_name='repo_commit_download', request_method='GET',
363 route_name='repo_commit_download', request_method='GET',
364 renderer=None)
364 renderer=None)
365 def repo_commit_download(self):
365 def repo_commit_download(self):
366 commit_id = self.request.matchdict['commit_id']
366 commit_id = self.request.matchdict['commit_id']
367 return self._commit(commit_id, method='download')
367 return self._commit(commit_id, method='download')
368
368
369 @LoginRequired()
369 @LoginRequired()
370 @NotAnonymous()
370 @NotAnonymous()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @CSRFRequired()
373 @CSRFRequired()
374 @view_config(
374 @view_config(
375 route_name='repo_commit_comment_create', request_method='POST',
375 route_name='repo_commit_comment_create', request_method='POST',
376 renderer='json_ext')
376 renderer='json_ext')
377 def repo_commit_comment_create(self):
377 def repo_commit_comment_create(self):
378 _ = self.request.translate
378 _ = self.request.translate
379 commit_id = self.request.matchdict['commit_id']
379 commit_id = self.request.matchdict['commit_id']
380
380
381 c = self.load_default_context()
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
382 status = self.request.POST.get('changeset_status', None)
383 text = self.request.POST.get('text')
383 text = self.request.POST.get('text')
384 comment_type = self.request.POST.get('comment_type')
384 comment_type = self.request.POST.get('comment_type')
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386
386
387 if status:
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
391
391
392 multi_commit_ids = []
392 multi_commit_ids = []
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 if _commit_id not in multi_commit_ids:
395 if _commit_id not in multi_commit_ids:
396 multi_commit_ids.append(_commit_id)
396 multi_commit_ids.append(_commit_id)
397
397
398 commit_ids = multi_commit_ids or [commit_id]
398 commit_ids = multi_commit_ids or [commit_id]
399
399
400 comment = None
400 comment = None
401 for current_id in filter(None, commit_ids):
401 for current_id in filter(None, commit_ids):
402 comment = CommentsModel().create(
402 comment = CommentsModel().create(
403 text=text,
403 text=text,
404 repo=self.db_repo.repo_id,
404 repo=self.db_repo.repo_id,
405 user=self._rhodecode_db_user.user_id,
405 user=self._rhodecode_db_user.user_id,
406 commit_id=current_id,
406 commit_id=current_id,
407 f_path=self.request.POST.get('f_path'),
407 f_path=self.request.POST.get('f_path'),
408 line_no=self.request.POST.get('line'),
408 line_no=self.request.POST.get('line'),
409 status_change=(ChangesetStatus.get_status_lbl(status)
409 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status else None),
410 if status else None),
411 status_change_type=status,
411 status_change_type=status,
412 comment_type=comment_type,
412 comment_type=comment_type,
413 resolves_comment_id=resolves_comment_id
413 resolves_comment_id=resolves_comment_id
414 )
414 )
415
415
416 # get status if set !
416 # get status if set !
417 if status:
417 if status:
418 # if latest status was from pull request and it's closed
418 # if latest status was from pull request and it's closed
419 # disallow changing status !
419 # disallow changing status !
420 # dont_allow_on_closed_pull_request = True !
420 # dont_allow_on_closed_pull_request = True !
421
421
422 try:
422 try:
423 ChangesetStatusModel().set_status(
423 ChangesetStatusModel().set_status(
424 self.db_repo.repo_id,
424 self.db_repo.repo_id,
425 status,
425 status,
426 self._rhodecode_db_user.user_id,
426 self._rhodecode_db_user.user_id,
427 comment,
427 comment,
428 revision=current_id,
428 revision=current_id,
429 dont_allow_on_closed_pull_request=True
429 dont_allow_on_closed_pull_request=True
430 )
430 )
431 except StatusChangeOnClosedPullRequestError:
431 except StatusChangeOnClosedPullRequestError:
432 msg = _('Changing the status of a commit associated with '
432 msg = _('Changing the status of a commit associated with '
433 'a closed pull request is not allowed')
433 'a closed pull request is not allowed')
434 log.exception(msg)
434 log.exception(msg)
435 h.flash(msg, category='warning')
435 h.flash(msg, category='warning')
436 raise HTTPFound(h.route_path(
436 raise HTTPFound(h.route_path(
437 'repo_commit', repo_name=self.db_repo_name,
437 'repo_commit', repo_name=self.db_repo_name,
438 commit_id=current_id))
438 commit_id=current_id))
439
439
440 # finalize, commit and redirect
440 # finalize, commit and redirect
441 Session().commit()
441 Session().commit()
442
442
443 data = {
443 data = {
444 'target_id': h.safeid(h.safe_unicode(
444 'target_id': h.safeid(h.safe_unicode(
445 self.request.POST.get('f_path'))),
445 self.request.POST.get('f_path'))),
446 }
446 }
447 if comment:
447 if comment:
448 c.co = comment
448 c.co = comment
449 rendered_comment = render(
449 rendered_comment = render(
450 'rhodecode:templates/changeset/changeset_comment_block.mako',
450 'rhodecode:templates/changeset/changeset_comment_block.mako',
451 self._get_template_context(c), self.request)
451 self._get_template_context(c), self.request)
452
452
453 data.update(comment.get_dict())
453 data.update(comment.get_dict())
454 data.update({'rendered_text': rendered_comment})
454 data.update({'rendered_text': rendered_comment})
455
455
456 return data
456 return data
457
457
458 @LoginRequired()
458 @LoginRequired()
459 @NotAnonymous()
459 @NotAnonymous()
460 @HasRepoPermissionAnyDecorator(
460 @HasRepoPermissionAnyDecorator(
461 'repository.read', 'repository.write', 'repository.admin')
461 'repository.read', 'repository.write', 'repository.admin')
462 @CSRFRequired()
462 @CSRFRequired()
463 @view_config(
463 @view_config(
464 route_name='repo_commit_comment_preview', request_method='POST',
464 route_name='repo_commit_comment_preview', request_method='POST',
465 renderer='string', xhr=True)
465 renderer='string', xhr=True)
466 def repo_commit_comment_preview(self):
466 def repo_commit_comment_preview(self):
467 # Technically a CSRF token is not needed as no state changes with this
467 # Technically a CSRF token is not needed as no state changes with this
468 # call. However, as this is a POST is better to have it, so automated
468 # call. However, as this is a POST is better to have it, so automated
469 # tools don't flag it as potential CSRF.
469 # tools don't flag it as potential CSRF.
470 # Post is required because the payload could be bigger than the maximum
470 # Post is required because the payload could be bigger than the maximum
471 # allowed by GET.
471 # allowed by GET.
472
472
473 text = self.request.POST.get('text')
473 text = self.request.POST.get('text')
474 renderer = self.request.POST.get('renderer') or 'rst'
474 renderer = self.request.POST.get('renderer') or 'rst'
475 if text:
475 if text:
476 return h.render(text, renderer=renderer, mentions=True)
476 return h.render(text, renderer=renderer, mentions=True)
477 return ''
477 return ''
478
478
479 @LoginRequired()
479 @LoginRequired()
480 @NotAnonymous()
480 @NotAnonymous()
481 @HasRepoPermissionAnyDecorator(
481 @HasRepoPermissionAnyDecorator(
482 'repository.read', 'repository.write', 'repository.admin')
482 'repository.read', 'repository.write', 'repository.admin')
483 @CSRFRequired()
483 @CSRFRequired()
484 @view_config(
484 @view_config(
485 route_name='repo_commit_comment_delete', request_method='POST',
485 route_name='repo_commit_comment_delete', request_method='POST',
486 renderer='json_ext')
486 renderer='json_ext')
487 def repo_commit_comment_delete(self):
487 def repo_commit_comment_delete(self):
488 commit_id = self.request.matchdict['commit_id']
488 commit_id = self.request.matchdict['commit_id']
489 comment_id = self.request.matchdict['comment_id']
489 comment_id = self.request.matchdict['comment_id']
490
490
491 comment = ChangesetComment.get_or_404(comment_id)
491 comment = ChangesetComment.get_or_404(comment_id)
492 if not comment:
492 if not comment:
493 log.debug('Comment with id:%s not found, skipping', comment_id)
493 log.debug('Comment with id:%s not found, skipping', comment_id)
494 # comment already deleted in another call probably
494 # comment already deleted in another call probably
495 return True
495 return True
496
496
497 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
497 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
498 super_admin = h.HasPermissionAny('hg.admin')()
498 super_admin = h.HasPermissionAny('hg.admin')()
499 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
499 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
500 is_repo_comment = comment.repo.repo_name == self.db_repo_name
500 is_repo_comment = comment.repo.repo_name == self.db_repo_name
501 comment_repo_admin = is_repo_admin and is_repo_comment
501 comment_repo_admin = is_repo_admin and is_repo_comment
502
502
503 if super_admin or comment_owner or comment_repo_admin:
503 if super_admin or comment_owner or comment_repo_admin:
504 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
504 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
505 Session().commit()
505 Session().commit()
506 return True
506 return True
507 else:
507 else:
508 log.warning('No permissions for user %s to delete comment_id: %s',
508 log.warning('No permissions for user %s to delete comment_id: %s',
509 self._rhodecode_db_user, comment_id)
509 self._rhodecode_db_user, comment_id)
510 raise HTTPNotFound()
510 raise HTTPNotFound()
511
511
512 @LoginRequired()
512 @LoginRequired()
513 @HasRepoPermissionAnyDecorator(
513 @HasRepoPermissionAnyDecorator(
514 'repository.read', 'repository.write', 'repository.admin')
514 'repository.read', 'repository.write', 'repository.admin')
515 @view_config(
515 @view_config(
516 route_name='repo_commit_data', request_method='GET',
516 route_name='repo_commit_data', request_method='GET',
517 renderer='json_ext', xhr=True)
517 renderer='json_ext', xhr=True)
518 def repo_commit_data(self):
518 def repo_commit_data(self):
519 commit_id = self.request.matchdict['commit_id']
519 commit_id = self.request.matchdict['commit_id']
520 self.load_default_context()
520 self.load_default_context()
521
521
522 try:
522 try:
523 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
523 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
524 except CommitDoesNotExistError as e:
524 except CommitDoesNotExistError as e:
525 return EmptyCommit(message=str(e))
525 return EmptyCommit(message=str(e))
526
526
527 @LoginRequired()
527 @LoginRequired()
528 @HasRepoPermissionAnyDecorator(
528 @HasRepoPermissionAnyDecorator(
529 'repository.read', 'repository.write', 'repository.admin')
529 'repository.read', 'repository.write', 'repository.admin')
530 @view_config(
530 @view_config(
531 route_name='repo_commit_children', request_method='GET',
531 route_name='repo_commit_children', request_method='GET',
532 renderer='json_ext', xhr=True)
532 renderer='json_ext', xhr=True)
533 def repo_commit_children(self):
533 def repo_commit_children(self):
534 commit_id = self.request.matchdict['commit_id']
534 commit_id = self.request.matchdict['commit_id']
535 self.load_default_context()
535 self.load_default_context()
536
536
537 try:
537 try:
538 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
538 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
539 children = commit.children
539 children = commit.children
540 except CommitDoesNotExistError:
540 except CommitDoesNotExistError:
541 children = []
541 children = []
542
542
543 result = {"results": children}
543 result = {"results": children}
544 return result
544 return result
545
545
546 @LoginRequired()
546 @LoginRequired()
547 @HasRepoPermissionAnyDecorator(
547 @HasRepoPermissionAnyDecorator(
548 'repository.read', 'repository.write', 'repository.admin')
548 'repository.read', 'repository.write', 'repository.admin')
549 @view_config(
549 @view_config(
550 route_name='repo_commit_parents', request_method='GET',
550 route_name='repo_commit_parents', request_method='GET',
551 renderer='json_ext')
551 renderer='json_ext')
552 def repo_commit_parents(self):
552 def repo_commit_parents(self):
553 commit_id = self.request.matchdict['commit_id']
553 commit_id = self.request.matchdict['commit_id']
554 self.load_default_context()
554 self.load_default_context()
555
555
556 try:
556 try:
557 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
557 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
558 parents = commit.parents
558 parents = commit.parents
559 except CommitDoesNotExistError:
559 except CommitDoesNotExistError:
560 parents = []
560 parents = []
561 result = {"results": parents}
561 result = {"results": parents}
562 return result
562 return result
@@ -1,322 +1,322 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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
21
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.vcs.exceptions import (
36 from rhodecode.lib.vcs.exceptions import (
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 NodeDoesNotExistError)
38 NodeDoesNotExistError)
39 from rhodecode.model.db import Repository, ChangesetStatus
39 from rhodecode.model.db import Repository, ChangesetStatus
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class RepoCompareView(RepoAppView):
44 class RepoCompareView(RepoAppView):
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context(include_app_defaults=True)
46 c = self._get_local_tmpl_context(include_app_defaults=True)
47
47
48 c.rhodecode_repo = self.rhodecode_vcs_repo
48 c.rhodecode_repo = self.rhodecode_vcs_repo
49
49
50
50
51 return c
51 return c
52
52
53 def _get_commit_or_redirect(
53 def _get_commit_or_redirect(
54 self, ref, ref_type, repo, redirect_after=True, partial=False):
54 self, ref, ref_type, repo, redirect_after=True, partial=False):
55 """
55 """
56 This is a safe way to get a commit. If an error occurs it
56 This is a safe way to get a commit. If an error occurs it
57 redirects to a commit with a proper message. If partial is set
57 redirects to a commit with a proper message. If partial is set
58 then it does not do redirect raise and throws an exception instead.
58 then it does not do redirect raise and throws an exception instead.
59 """
59 """
60 _ = self.request.translate
60 _ = self.request.translate
61 try:
61 try:
62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
63 except EmptyRepositoryError:
63 except EmptyRepositoryError:
64 if not redirect_after:
64 if not redirect_after:
65 return repo.scm_instance().EMPTY_COMMIT
65 return repo.scm_instance().EMPTY_COMMIT
66 h.flash(h.literal(_('There are no commits yet')),
66 h.flash(h.literal(_('There are no commits yet')),
67 category='warning')
67 category='warning')
68 if not partial:
68 if not partial:
69 raise HTTPFound(
69 raise HTTPFound(
70 h.route_path('repo_summary', repo_name=repo.repo_name))
70 h.route_path('repo_summary', repo_name=repo.repo_name))
71 raise HTTPBadRequest()
71 raise HTTPBadRequest()
72
72
73 except RepositoryError as e:
73 except RepositoryError as e:
74 log.exception(safe_str(e))
74 log.exception(safe_str(e))
75 h.flash(safe_str(h.escape(e)), category='warning')
75 h.flash(safe_str(h.escape(e)), category='warning')
76 if not partial:
76 if not partial:
77 raise HTTPFound(
77 raise HTTPFound(
78 h.route_path('repo_summary', repo_name=repo.repo_name))
78 h.route_path('repo_summary', repo_name=repo.repo_name))
79 raise HTTPBadRequest()
79 raise HTTPBadRequest()
80
80
81 @LoginRequired()
81 @LoginRequired()
82 @HasRepoPermissionAnyDecorator(
82 @HasRepoPermissionAnyDecorator(
83 'repository.read', 'repository.write', 'repository.admin')
83 'repository.read', 'repository.write', 'repository.admin')
84 @view_config(
84 @view_config(
85 route_name='repo_compare_select', request_method='GET',
85 route_name='repo_compare_select', request_method='GET',
86 renderer='rhodecode:templates/compare/compare_diff.mako')
86 renderer='rhodecode:templates/compare/compare_diff.mako')
87 def compare_select(self):
87 def compare_select(self):
88 _ = self.request.translate
88 _ = self.request.translate
89 c = self.load_default_context()
89 c = self.load_default_context()
90
90
91 source_repo = self.db_repo_name
91 source_repo = self.db_repo_name
92 target_repo = self.request.GET.get('target_repo', source_repo)
92 target_repo = self.request.GET.get('target_repo', source_repo)
93 c.source_repo = Repository.get_by_repo_name(source_repo)
93 c.source_repo = Repository.get_by_repo_name(source_repo)
94 c.target_repo = Repository.get_by_repo_name(target_repo)
94 c.target_repo = Repository.get_by_repo_name(target_repo)
95
95
96 if c.source_repo is None or c.target_repo is None:
96 if c.source_repo is None or c.target_repo is None:
97 raise HTTPNotFound()
97 raise HTTPNotFound()
98
98
99 c.compare_home = True
99 c.compare_home = True
100 c.commit_ranges = []
100 c.commit_ranges = []
101 c.collapse_all_commits = False
101 c.collapse_all_commits = False
102 c.diffset = None
102 c.diffset = None
103 c.limited_diff = False
103 c.limited_diff = False
104 c.source_ref = c.target_ref = _('Select commit')
104 c.source_ref = c.target_ref = _('Select commit')
105 c.source_ref_type = ""
105 c.source_ref_type = ""
106 c.target_ref_type = ""
106 c.target_ref_type = ""
107 c.commit_statuses = ChangesetStatus.STATUSES
107 c.commit_statuses = ChangesetStatus.STATUSES
108 c.preview_mode = False
108 c.preview_mode = False
109 c.file_path = None
109 c.file_path = None
110
110
111 return self._get_template_context(c)
111 return self._get_template_context(c)
112
112
113 @LoginRequired()
113 @LoginRequired()
114 @HasRepoPermissionAnyDecorator(
114 @HasRepoPermissionAnyDecorator(
115 'repository.read', 'repository.write', 'repository.admin')
115 'repository.read', 'repository.write', 'repository.admin')
116 @view_config(
116 @view_config(
117 route_name='repo_compare', request_method='GET',
117 route_name='repo_compare', request_method='GET',
118 renderer=None)
118 renderer=None)
119 def compare(self):
119 def compare(self):
120 _ = self.request.translate
120 _ = self.request.translate
121 c = self.load_default_context()
121 c = self.load_default_context()
122
122
123 source_ref_type = self.request.matchdict['source_ref_type']
123 source_ref_type = self.request.matchdict['source_ref_type']
124 source_ref = self.request.matchdict['source_ref']
124 source_ref = self.request.matchdict['source_ref']
125 target_ref_type = self.request.matchdict['target_ref_type']
125 target_ref_type = self.request.matchdict['target_ref_type']
126 target_ref = self.request.matchdict['target_ref']
126 target_ref = self.request.matchdict['target_ref']
127
127
128 # source_ref will be evaluated in source_repo
128 # source_ref will be evaluated in source_repo
129 source_repo_name = self.db_repo_name
129 source_repo_name = self.db_repo_name
130 source_path, source_id = parse_path_ref(source_ref)
130 source_path, source_id = parse_path_ref(source_ref)
131
131
132 # target_ref will be evaluated in target_repo
132 # target_ref will be evaluated in target_repo
133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
134 target_path, target_id = parse_path_ref(
134 target_path, target_id = parse_path_ref(
135 target_ref, default_path=self.request.GET.get('f_path', ''))
135 target_ref, default_path=self.request.GET.get('f_path', ''))
136
136
137 # if merge is True
137 # if merge is True
138 # Show what changes since the shared ancestor commit of target/source
138 # Show what changes since the shared ancestor commit of target/source
139 # the source would get if it was merged with target. Only commits
139 # the source would get if it was merged with target. Only commits
140 # which are in target but not in source will be shown.
140 # which are in target but not in source will be shown.
141 merge = str2bool(self.request.GET.get('merge'))
141 merge = str2bool(self.request.GET.get('merge'))
142 # if merge is False
142 # if merge is False
143 # Show a raw diff of source/target refs even if no ancestor exists
143 # Show a raw diff of source/target refs even if no ancestor exists
144
144
145 # c.fulldiff disables cut_off_limit
145 # c.fulldiff disables cut_off_limit
146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
147
147
148 c.file_path = target_path
148 c.file_path = target_path
149 c.commit_statuses = ChangesetStatus.STATUSES
149 c.commit_statuses = ChangesetStatus.STATUSES
150
150
151 # if partial, returns just compare_commits.html (commits log)
151 # if partial, returns just compare_commits.html (commits log)
152 partial = self.request.is_xhr
152 partial = self.request.is_xhr
153
153
154 # swap url for compare_diff page
154 # swap url for compare_diff page
155 c.swap_url = h.route_path(
155 c.swap_url = h.route_path(
156 'repo_compare',
156 'repo_compare',
157 repo_name=target_repo_name,
157 repo_name=target_repo_name,
158 source_ref_type=target_ref_type,
158 source_ref_type=target_ref_type,
159 source_ref=target_ref,
159 source_ref=target_ref,
160 target_repo=source_repo_name,
160 target_repo=source_repo_name,
161 target_ref_type=source_ref_type,
161 target_ref_type=source_ref_type,
162 target_ref=source_ref,
162 target_ref=source_ref,
163 _query=dict(merge=merge and '1' or '', f_path=target_path))
163 _query=dict(merge=merge and '1' or '', f_path=target_path))
164
164
165 source_repo = Repository.get_by_repo_name(source_repo_name)
165 source_repo = Repository.get_by_repo_name(source_repo_name)
166 target_repo = Repository.get_by_repo_name(target_repo_name)
166 target_repo = Repository.get_by_repo_name(target_repo_name)
167
167
168 if source_repo is None:
168 if source_repo is None:
169 log.error('Could not find the source repo: {}'
169 log.error('Could not find the source repo: {}'
170 .format(source_repo_name))
170 .format(source_repo_name))
171 h.flash(_('Could not find the source repo: `{}`')
171 h.flash(_('Could not find the source repo: `{}`')
172 .format(h.escape(source_repo_name)), category='error')
172 .format(h.escape(source_repo_name)), category='error')
173 raise HTTPFound(
173 raise HTTPFound(
174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175
175
176 if target_repo is None:
176 if target_repo is None:
177 log.error('Could not find the target repo: {}'
177 log.error('Could not find the target repo: {}'
178 .format(source_repo_name))
178 .format(source_repo_name))
179 h.flash(_('Could not find the target repo: `{}`')
179 h.flash(_('Could not find the target repo: `{}`')
180 .format(h.escape(target_repo_name)), category='error')
180 .format(h.escape(target_repo_name)), category='error')
181 raise HTTPFound(
181 raise HTTPFound(
182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183
183
184 source_scm = source_repo.scm_instance()
184 source_scm = source_repo.scm_instance()
185 target_scm = target_repo.scm_instance()
185 target_scm = target_repo.scm_instance()
186
186
187 source_alias = source_scm.alias
187 source_alias = source_scm.alias
188 target_alias = target_scm.alias
188 target_alias = target_scm.alias
189 if source_alias != target_alias:
189 if source_alias != target_alias:
190 msg = _('The comparison of two different kinds of remote repos '
190 msg = _('The comparison of two different kinds of remote repos '
191 'is not available')
191 'is not available')
192 log.error(msg)
192 log.error(msg)
193 h.flash(msg, category='error')
193 h.flash(msg, category='error')
194 raise HTTPFound(
194 raise HTTPFound(
195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196
196
197 source_commit = self._get_commit_or_redirect(
197 source_commit = self._get_commit_or_redirect(
198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 partial=partial)
199 partial=partial)
200 target_commit = self._get_commit_or_redirect(
200 target_commit = self._get_commit_or_redirect(
201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 partial=partial)
202 partial=partial)
203
203
204 c.compare_home = False
204 c.compare_home = False
205 c.source_repo = source_repo
205 c.source_repo = source_repo
206 c.target_repo = target_repo
206 c.target_repo = target_repo
207 c.source_ref = source_ref
207 c.source_ref = source_ref
208 c.target_ref = target_ref
208 c.target_ref = target_ref
209 c.source_ref_type = source_ref_type
209 c.source_ref_type = source_ref_type
210 c.target_ref_type = target_ref_type
210 c.target_ref_type = target_ref_type
211
211
212 pre_load = ["author", "branch", "date", "message"]
212 pre_load = ["author", "branch", "date", "message"]
213 c.ancestor = None
213 c.ancestor = None
214
214
215 if c.file_path:
215 if c.file_path:
216 if source_commit == target_commit:
216 if source_commit == target_commit:
217 c.commit_ranges = []
217 c.commit_ranges = []
218 else:
218 else:
219 c.commit_ranges = [target_commit]
219 c.commit_ranges = [target_commit]
220 else:
220 else:
221 try:
221 try:
222 c.commit_ranges = source_scm.compare(
222 c.commit_ranges = source_scm.compare(
223 source_commit.raw_id, target_commit.raw_id,
223 source_commit.raw_id, target_commit.raw_id,
224 target_scm, merge, pre_load=pre_load)
224 target_scm, merge, pre_load=pre_load)
225 if merge:
225 if merge:
226 c.ancestor = source_scm.get_common_ancestor(
226 c.ancestor = source_scm.get_common_ancestor(
227 source_commit.raw_id, target_commit.raw_id, target_scm)
227 source_commit.raw_id, target_commit.raw_id, target_scm)
228 except RepositoryRequirementError:
228 except RepositoryRequirementError:
229 msg = _('Could not compare repos with different '
229 msg = _('Could not compare repos with different '
230 'large file settings')
230 'large file settings')
231 log.error(msg)
231 log.error(msg)
232 if partial:
232 if partial:
233 return Response(msg)
233 return Response(msg)
234 h.flash(msg, category='error')
234 h.flash(msg, category='error')
235 raise HTTPFound(
235 raise HTTPFound(
236 h.route_path('repo_compare_select',
236 h.route_path('repo_compare_select',
237 repo_name=self.db_repo_name))
237 repo_name=self.db_repo_name))
238
238
239 c.statuses = self.db_repo.statuses(
239 c.statuses = self.db_repo.statuses(
240 [x.raw_id for x in c.commit_ranges])
240 [x.raw_id for x in c.commit_ranges])
241
241
242 # auto collapse if we have more than limit
242 # auto collapse if we have more than limit
243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245
245
246 if partial: # for PR ajax commits loader
246 if partial: # for PR ajax commits loader
247 if not c.ancestor:
247 if not c.ancestor:
248 return Response('') # cannot merge if there is no ancestor
248 return Response('') # cannot merge if there is no ancestor
249
249
250 html = render(
250 html = render(
251 'rhodecode:templates/compare/compare_commits.mako',
251 'rhodecode:templates/compare/compare_commits.mako',
252 self._get_template_context(c), self.request)
252 self._get_template_context(c), self.request)
253 return Response(html)
253 return Response(html)
254
254
255 if c.ancestor:
255 if c.ancestor:
256 # case we want a simple diff without incoming commits,
256 # case we want a simple diff without incoming commits,
257 # previewing what will be merged.
257 # previewing what will be merged.
258 # Make the diff on target repo (which is known to have target_ref)
258 # Make the diff on target repo (which is known to have target_ref)
259 log.debug('Using ancestor %s as source_ref instead of %s'
259 log.debug('Using ancestor %s as source_ref instead of %s'
260 % (c.ancestor, source_ref))
260 % (c.ancestor, source_ref))
261 source_repo = target_repo
261 source_repo = target_repo
262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263
263
264 # diff_limit will cut off the whole diff if the limit is applied
264 # diff_limit will cut off the whole diff if the limit is applied
265 # otherwise it will just hide the big files from the front-end
265 # otherwise it will just hide the big files from the front-end
266 diff_limit = c.visual.cut_off_limit_diff
266 diff_limit = c.visual.cut_off_limit_diff
267 file_limit = c.visual.cut_off_limit_file
267 file_limit = c.visual.cut_off_limit_file
268
268
269 log.debug('calculating diff between '
269 log.debug('calculating diff between '
270 'source_ref:%s and target_ref:%s for repo `%s`',
270 'source_ref:%s and target_ref:%s for repo `%s`',
271 source_commit, target_commit,
271 source_commit, target_commit,
272 safe_unicode(source_repo.scm_instance().path))
272 safe_unicode(source_repo.scm_instance().path))
273
273
274 if source_commit.repository != target_commit.repository:
274 if source_commit.repository != target_commit.repository:
275 msg = _(
275 msg = _(
276 "Repositories unrelated. "
276 "Repositories unrelated. "
277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 "with commit %(commit2)s from repository %(repo2)s.") % {
278 "with commit %(commit2)s from repository %(repo2)s.") % {
279 'commit1': h.show_id(source_commit),
279 'commit1': h.show_id(source_commit),
280 'repo1': source_repo.repo_name,
280 'repo1': source_repo.repo_name,
281 'commit2': h.show_id(target_commit),
281 'commit2': h.show_id(target_commit),
282 'repo2': target_repo.repo_name,
282 'repo2': target_repo.repo_name,
283 }
283 }
284 h.flash(msg, category='error')
284 h.flash(msg, category='error')
285 raise HTTPFound(
285 raise HTTPFound(
286 h.route_path('repo_compare_select',
286 h.route_path('repo_compare_select',
287 repo_name=self.db_repo_name))
287 repo_name=self.db_repo_name))
288
288
289 txt_diff = source_repo.scm_instance().get_diff(
289 txt_diff = source_repo.scm_instance().get_diff(
290 commit1=source_commit, commit2=target_commit,
290 commit1=source_commit, commit2=target_commit,
291 path=target_path, path1=source_path)
291 path=target_path, path1=source_path)
292
292
293 diff_processor = diffs.DiffProcessor(
293 diff_processor = diffs.DiffProcessor(
294 txt_diff, format='newdiff', diff_limit=diff_limit,
294 txt_diff, format='newdiff', diff_limit=diff_limit,
295 file_limit=file_limit, show_full_diff=c.fulldiff)
295 file_limit=file_limit, show_full_diff=c.fulldiff)
296 _parsed = diff_processor.prepare()
296 _parsed = diff_processor.prepare()
297
297
298 def _node_getter(commit):
298 def _node_getter(commit):
299 """ Returns a function that returns a node for a commit or None """
299 """ Returns a function that returns a node for a commit or None """
300 def get_node(fname):
300 def get_node(fname):
301 try:
301 try:
302 return commit.get_node(fname)
302 return commit.get_node(fname)
303 except NodeDoesNotExistError:
303 except NodeDoesNotExistError:
304 return None
304 return None
305 return get_node
305 return get_node
306
306
307 diffset = codeblocks.DiffSet(
307 diffset = codeblocks.DiffSet(
308 repo_name=source_repo.repo_name,
308 repo_name=source_repo.repo_name,
309 source_node_getter=_node_getter(source_commit),
309 source_node_getter=_node_getter(source_commit),
310 target_node_getter=_node_getter(target_commit),
310 target_node_getter=_node_getter(target_commit),
311 )
311 )
312 c.diffset = diffset.render_patchset(
312 c.diffset = self.path_filter.render_patchset_filtered(
313 _parsed, source_ref, target_ref)
313 diffset, _parsed, source_ref, target_ref)
314
314
315 c.preview_mode = merge
315 c.preview_mode = merge
316 c.source_commit = source_commit
316 c.source_commit = source_commit
317 c.target_commit = target_commit
317 c.target_commit = target_commit
318
318
319 html = render(
319 html = render(
320 'rhodecode:templates/compare/compare_diff.mako',
320 'rhodecode:templates/compare/compare_diff.mako',
321 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
322 return Response(html) No newline at end of file
322 return Response(html)
@@ -1,205 +1,218 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2018 RhodeCode GmbH
3 # Copyright (C) 2017-2018 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 pytz
21 import pytz
22 import logging
22 import logging
23
23
24 from beaker.cache import cache_region
24 from beaker.cache import cache_region
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.response import Response
26 from pyramid.response import Response
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasRepoPermissionAnyDecorator)
33 LoginRequired, HasRepoPermissionAnyDecorator)
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 from rhodecode.model.db import UserApiKeys, CacheKey
36 from rhodecode.model.db import UserApiKeys, CacheKey
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class RepoFeedView(RepoAppView):
41 class RepoFeedView(RepoAppView):
42 def load_default_context(self):
42 def load_default_context(self):
43 c = self._get_local_tmpl_context()
43 c = self._get_local_tmpl_context()
44
44
45
45
46 self._load_defaults()
46 self._load_defaults()
47 return c
47 return c
48
48
49 def _get_config(self):
49 def _get_config(self):
50 import rhodecode
50 import rhodecode
51 config = rhodecode.CONFIG
51 config = rhodecode.CONFIG
52
52
53 return {
53 return {
54 'language': 'en-us',
54 'language': 'en-us',
55 'feed_ttl': '5', # TTL of feed,
55 'feed_ttl': '5', # TTL of feed,
56 'feed_include_diff':
56 'feed_include_diff':
57 str2bool(config.get('rss_include_diff', False)),
57 str2bool(config.get('rss_include_diff', False)),
58 'feed_items_per_page':
58 'feed_items_per_page':
59 safe_int(config.get('rss_items_per_page', 20)),
59 safe_int(config.get('rss_items_per_page', 20)),
60 'feed_diff_limit':
60 'feed_diff_limit':
61 # we need to protect from parsing huge diffs here other way
61 # we need to protect from parsing huge diffs here other way
62 # we can kill the server
62 # we can kill the server
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 }
64 }
65
65
66 def _load_defaults(self):
66 def _load_defaults(self):
67 _ = self.request.translate
67 _ = self.request.translate
68 config = self._get_config()
68 config = self._get_config()
69 # common values for feeds
69 # common values for feeds
70 self.description = _('Changes on %s repository')
70 self.description = _('Changes on %s repository')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 self.language = config["language"]
72 self.language = config["language"]
73 self.ttl = config["feed_ttl"]
73 self.ttl = config["feed_ttl"]
74 self.feed_include_diff = config['feed_include_diff']
74 self.feed_include_diff = config['feed_include_diff']
75 self.feed_diff_limit = config['feed_diff_limit']
75 self.feed_diff_limit = config['feed_diff_limit']
76 self.feed_items_per_page = config['feed_items_per_page']
76 self.feed_items_per_page = config['feed_items_per_page']
77
77
78 def _changes(self, commit):
78 def _changes(self, commit):
79 diff_processor = DiffProcessor(
79 diff_processor = DiffProcessor(
80 commit.diff(), diff_limit=self.feed_diff_limit)
80 commit.diff(), diff_limit=self.feed_diff_limit)
81 _parsed = diff_processor.prepare(inline_diff=False)
81 _parsed = diff_processor.prepare(inline_diff=False)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83
83
84 return diff_processor, _parsed, limited_diff
84 return diff_processor, _parsed, limited_diff
85
85
86 def _get_title(self, commit):
86 def _get_title(self, commit):
87 return h.shorter(commit.message, 160)
87 return h.shorter(commit.message, 160)
88
88
89 def _get_description(self, commit):
89 def _get_description(self, commit):
90 _renderer = self.request.get_partial_renderer(
90 _renderer = self.request.get_partial_renderer(
91 'rhodecode:templates/feed/atom_feed_entry.mako')
91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
92 diff_processor, parsed_diff, limited_diff = self._changes(commit)
93 filtered_parsed_diff, has_hidden_changes = self.path_filter.filter_patchset(parsed_diff)
93 return _renderer(
94 return _renderer(
94 'body',
95 'body',
95 commit=commit,
96 commit=commit,
96 parsed_diff=parsed_diff,
97 parsed_diff=filtered_parsed_diff,
97 limited_diff=limited_diff,
98 limited_diff=limited_diff,
98 feed_include_diff=self.feed_include_diff,
99 feed_include_diff=self.feed_include_diff,
99 diff_processor=diff_processor,
100 diff_processor=diff_processor,
101 has_hidden_changes=has_hidden_changes
100 )
102 )
101
103
102 def _set_timezone(self, date, tzinfo=pytz.utc):
104 def _set_timezone(self, date, tzinfo=pytz.utc):
103 if not getattr(date, "tzinfo", None):
105 if not getattr(date, "tzinfo", None):
104 date.replace(tzinfo=tzinfo)
106 date.replace(tzinfo=tzinfo)
105 return date
107 return date
106
108
107 def _get_commits(self):
109 def _get_commits(self):
108 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
109
111
110 def uid(self, repo_id, commit_id):
112 def uid(self, repo_id, commit_id):
111 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
112
114
113 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
114 @HasRepoPermissionAnyDecorator(
116 @HasRepoPermissionAnyDecorator(
115 'repository.read', 'repository.write', 'repository.admin')
117 'repository.read', 'repository.write', 'repository.admin')
116 @view_config(
118 @view_config(
117 route_name='atom_feed_home', request_method='GET',
119 route_name='atom_feed_home', request_method='GET',
118 renderer=None)
120 renderer=None)
119 def atom(self):
121 def atom(self):
120 """
122 """
121 Produce an atom-1.0 feed via feedgenerator module
123 Produce an atom-1.0 feed via feedgenerator module
122 """
124 """
123 self.load_default_context()
125 self.load_default_context()
124
126
125 @cache_region('long_term')
127 def _generate_feed():
126 def _generate_feed(cache_key):
127 feed = Atom1Feed(
128 feed = Atom1Feed(
128 title=self.title % self.db_repo_name,
129 title=self.title % self.db_repo_name,
129 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 description=self.description % self.db_repo_name,
131 description=self.description % self.db_repo_name,
131 language=self.language,
132 language=self.language,
132 ttl=self.ttl
133 ttl=self.ttl
133 )
134 )
134
135
135 for commit in reversed(self._get_commits()):
136 for commit in reversed(self._get_commits()):
136 date = self._set_timezone(commit.date)
137 date = self._set_timezone(commit.date)
137 feed.add_item(
138 feed.add_item(
138 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
139 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
139 title=self._get_title(commit),
140 title=self._get_title(commit),
140 author_name=commit.author,
141 author_name=commit.author,
141 description=self._get_description(commit),
142 description=self._get_description(commit),
142 link=h.route_url(
143 link=h.route_url(
143 'repo_commit', repo_name=self.db_repo_name,
144 'repo_commit', repo_name=self.db_repo_name,
144 commit_id=commit.raw_id),
145 commit_id=commit.raw_id),
145 pubdate=date,)
146 pubdate=date,)
146
147
147 return feed.mime_type, feed.writeString('utf-8')
148 return feed.mime_type, feed.writeString('utf-8')
148
149
149 invalidator_context = CacheKey.repo_context_cache(
150 @cache_region('long_term')
150 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
151 def _generate_feed_and_cache(cache_key):
152 return _generate_feed()
151
153
152 with invalidator_context as context:
154 if self.path_filter.is_enabled:
153 context.invalidate()
155 invalidator_context = CacheKey.repo_context_cache(
154 mime_type, feed = context.compute()
156 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
157 with invalidator_context as context:
158 context.invalidate()
159 mime_type, feed = context.compute()
160 else:
161 mime_type, feed = _generate_feed()
155
162
156 response = Response(feed)
163 response = Response(feed)
157 response.content_type = mime_type
164 response.content_type = mime_type
158 return response
165 return response
159
166
160 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
167 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
161 @HasRepoPermissionAnyDecorator(
168 @HasRepoPermissionAnyDecorator(
162 'repository.read', 'repository.write', 'repository.admin')
169 'repository.read', 'repository.write', 'repository.admin')
163 @view_config(
170 @view_config(
164 route_name='rss_feed_home', request_method='GET',
171 route_name='rss_feed_home', request_method='GET',
165 renderer=None)
172 renderer=None)
166 def rss(self):
173 def rss(self):
167 """
174 """
168 Produce an rss2 feed via feedgenerator module
175 Produce an rss2 feed via feedgenerator module
169 """
176 """
170 self.load_default_context()
177 self.load_default_context()
171
178
172 @cache_region('long_term')
179 def _generate_feed():
173 def _generate_feed(cache_key):
174 feed = Rss201rev2Feed(
180 feed = Rss201rev2Feed(
175 title=self.title % self.db_repo_name,
181 title=self.title % self.db_repo_name,
176 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
182 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
177 description=self.description % self.db_repo_name,
183 description=self.description % self.db_repo_name,
178 language=self.language,
184 language=self.language,
179 ttl=self.ttl
185 ttl=self.ttl
180 )
186 )
181
187
182 for commit in reversed(self._get_commits()):
188 for commit in reversed(self._get_commits()):
183 date = self._set_timezone(commit.date)
189 date = self._set_timezone(commit.date)
184 feed.add_item(
190 feed.add_item(
185 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
191 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
186 title=self._get_title(commit),
192 title=self._get_title(commit),
187 author_name=commit.author,
193 author_name=commit.author,
188 description=self._get_description(commit),
194 description=self._get_description(commit),
189 link=h.route_url(
195 link=h.route_url(
190 'repo_commit', repo_name=self.db_repo_name,
196 'repo_commit', repo_name=self.db_repo_name,
191 commit_id=commit.raw_id),
197 commit_id=commit.raw_id),
192 pubdate=date,)
198 pubdate=date,)
193
199
194 return feed.mime_type, feed.writeString('utf-8')
200 return feed.mime_type, feed.writeString('utf-8')
195
201
196 invalidator_context = CacheKey.repo_context_cache(
202 @cache_region('long_term')
197 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
203 def _generate_feed_and_cache(cache_key):
204 return _generate_feed()
198
205
199 with invalidator_context as context:
206 if self.path_filter.is_enabled:
200 context.invalidate()
207 invalidator_context = CacheKey.repo_context_cache(
201 mime_type, feed = context.compute()
208 _generate_feed_and_cache, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
209
210 with invalidator_context as context:
211 context.invalidate()
212 mime_type, feed = context.compute()
213 else:
214 mime_type, feed = _generate_feed()
202
215
203 response = Response(feed)
216 response = Response(feed)
204 response.content_type = mime_type
217 response.content_type = mime_type
205 return response
218 return response
@@ -1,1292 +1,1292 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
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 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 from rhodecode.apps._base import RepoAppView
33 from rhodecode.apps._base import RepoAppView
34
34
35 from rhodecode.controllers.utils import parse_path_ref
35 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.lib import diffs, helpers as h, caches
36 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.exceptions import NonRelativePathError
38 from rhodecode.lib.exceptions import NonRelativePathError
39 from rhodecode.lib.codeblocks import (
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 from rhodecode.lib.utils2 import (
41 from rhodecode.lib.utils2 import (
42 convert_line_endings, detect_mode, safe_str, str2bool)
42 convert_line_endings, detect_mode, safe_str, str2bool)
43 from rhodecode.lib.auth import (
43 from rhodecode.lib.auth import (
44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 from rhodecode.lib.vcs import path as vcspath
45 from rhodecode.lib.vcs import path as vcspath
46 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 from rhodecode.lib.vcs.conf import settings
47 from rhodecode.lib.vcs.conf import settings
48 from rhodecode.lib.vcs.nodes import FileNode
48 from rhodecode.lib.vcs.nodes import FileNode
49 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 NodeDoesNotExistError, CommitError, NodeError)
52 NodeDoesNotExistError, CommitError, NodeError)
53
53
54 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.db import Repository
55 from rhodecode.model.db import Repository
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoFilesView(RepoAppView):
60 class RepoFilesView(RepoAppView):
61
61
62 @staticmethod
62 @staticmethod
63 def adjust_file_path_for_svn(f_path, repo):
63 def adjust_file_path_for_svn(f_path, repo):
64 """
64 """
65 Computes the relative path of `f_path`.
65 Computes the relative path of `f_path`.
66
66
67 This is mainly based on prefix matching of the recognized tags and
67 This is mainly based on prefix matching of the recognized tags and
68 branches in the underlying repository.
68 branches in the underlying repository.
69 """
69 """
70 tags_and_branches = itertools.chain(
70 tags_and_branches = itertools.chain(
71 repo.branches.iterkeys(),
71 repo.branches.iterkeys(),
72 repo.tags.iterkeys())
72 repo.tags.iterkeys())
73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74
74
75 for name in tags_and_branches:
75 for name in tags_and_branches:
76 if f_path.startswith('{}/'.format(name)):
76 if f_path.startswith('{}/'.format(name)):
77 f_path = vcspath.relpath(f_path, name)
77 f_path = vcspath.relpath(f_path, name)
78 break
78 break
79 return f_path
79 return f_path
80
80
81 def load_default_context(self):
81 def load_default_context(self):
82 c = self._get_local_tmpl_context(include_app_defaults=True)
82 c = self._get_local_tmpl_context(include_app_defaults=True)
83
83
84 c.rhodecode_repo = self.rhodecode_vcs_repo
84 c.rhodecode_repo = self.rhodecode_vcs_repo
85
85
86
86
87 return c
87 return c
88
88
89 def _ensure_not_locked(self):
89 def _ensure_not_locked(self):
90 _ = self.request.translate
90 _ = self.request.translate
91
91
92 repo = self.db_repo
92 repo = self.db_repo
93 if repo.enable_locking and repo.locked[0]:
93 if repo.enable_locking and repo.locked[0]:
94 h.flash(_('This repository has been locked by %s on %s')
94 h.flash(_('This repository has been locked by %s on %s')
95 % (h.person_by_id(repo.locked[0]),
95 % (h.person_by_id(repo.locked[0]),
96 h.format_date(h.time_to_datetime(repo.locked[1]))),
96 h.format_date(h.time_to_datetime(repo.locked[1]))),
97 'warning')
97 'warning')
98 files_url = h.route_path(
98 files_url = h.route_path(
99 'repo_files:default_path',
99 'repo_files:default_path',
100 repo_name=self.db_repo_name, commit_id='tip')
100 repo_name=self.db_repo_name, commit_id='tip')
101 raise HTTPFound(files_url)
101 raise HTTPFound(files_url)
102
102
103 def _get_commit_and_path(self):
103 def _get_commit_and_path(self):
104 default_commit_id = self.db_repo.landing_rev[1]
104 default_commit_id = self.db_repo.landing_rev[1]
105 default_f_path = '/'
105 default_f_path = '/'
106
106
107 commit_id = self.request.matchdict.get(
107 commit_id = self.request.matchdict.get(
108 'commit_id', default_commit_id)
108 'commit_id', default_commit_id)
109 f_path = self._get_f_path(self.request.matchdict, default_f_path)
109 f_path = self._get_f_path(self.request.matchdict, default_f_path)
110 return commit_id, f_path
110 return commit_id, f_path
111
111
112 def _get_default_encoding(self, c):
112 def _get_default_encoding(self, c):
113 enc_list = getattr(c, 'default_encodings', [])
113 enc_list = getattr(c, 'default_encodings', [])
114 return enc_list[0] if enc_list else 'UTF-8'
114 return enc_list[0] if enc_list else 'UTF-8'
115
115
116 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
116 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
117 """
117 """
118 This is a safe way to get commit. If an error occurs it redirects to
118 This is a safe way to get commit. If an error occurs it redirects to
119 tip with proper message
119 tip with proper message
120
120
121 :param commit_id: id of commit to fetch
121 :param commit_id: id of commit to fetch
122 :param redirect_after: toggle redirection
122 :param redirect_after: toggle redirection
123 """
123 """
124 _ = self.request.translate
124 _ = self.request.translate
125
125
126 try:
126 try:
127 return self.rhodecode_vcs_repo.get_commit(commit_id)
127 return self.rhodecode_vcs_repo.get_commit(commit_id)
128 except EmptyRepositoryError:
128 except EmptyRepositoryError:
129 if not redirect_after:
129 if not redirect_after:
130 return None
130 return None
131
131
132 _url = h.route_path(
132 _url = h.route_path(
133 'repo_files_add_file',
133 'repo_files_add_file',
134 repo_name=self.db_repo_name, commit_id=0, f_path='',
134 repo_name=self.db_repo_name, commit_id=0, f_path='',
135 _anchor='edit')
135 _anchor='edit')
136
136
137 if h.HasRepoPermissionAny(
137 if h.HasRepoPermissionAny(
138 'repository.write', 'repository.admin')(self.db_repo_name):
138 'repository.write', 'repository.admin')(self.db_repo_name):
139 add_new = h.link_to(
139 add_new = h.link_to(
140 _('Click here to add a new file.'), _url, class_="alert-link")
140 _('Click here to add a new file.'), _url, class_="alert-link")
141 else:
141 else:
142 add_new = ""
142 add_new = ""
143
143
144 h.flash(h.literal(
144 h.flash(h.literal(
145 _('There are no files yet. %s') % add_new), category='warning')
145 _('There are no files yet. %s') % add_new), category='warning')
146 raise HTTPFound(
146 raise HTTPFound(
147 h.route_path('repo_summary', repo_name=self.db_repo_name))
147 h.route_path('repo_summary', repo_name=self.db_repo_name))
148
148
149 except (CommitDoesNotExistError, LookupError):
149 except (CommitDoesNotExistError, LookupError):
150 msg = _('No such commit exists for this repository')
150 msg = _('No such commit exists for this repository')
151 h.flash(msg, category='error')
151 h.flash(msg, category='error')
152 raise HTTPNotFound()
152 raise HTTPNotFound()
153 except RepositoryError as e:
153 except RepositoryError as e:
154 h.flash(safe_str(h.escape(e)), category='error')
154 h.flash(safe_str(h.escape(e)), category='error')
155 raise HTTPNotFound()
155 raise HTTPNotFound()
156
156
157 def _get_filenode_or_redirect(self, commit_obj, path):
157 def _get_filenode_or_redirect(self, commit_obj, path):
158 """
158 """
159 Returns file_node, if error occurs or given path is directory,
159 Returns file_node, if error occurs or given path is directory,
160 it'll redirect to top level path
160 it'll redirect to top level path
161 """
161 """
162 _ = self.request.translate
162 _ = self.request.translate
163
163
164 try:
164 try:
165 file_node = commit_obj.get_node(path)
165 file_node = commit_obj.get_node(path)
166 if file_node.is_dir():
166 if file_node.is_dir():
167 raise RepositoryError('The given path is a directory')
167 raise RepositoryError('The given path is a directory')
168 except CommitDoesNotExistError:
168 except CommitDoesNotExistError:
169 log.exception('No such commit exists for this repository')
169 log.exception('No such commit exists for this repository')
170 h.flash(_('No such commit exists for this repository'), category='error')
170 h.flash(_('No such commit exists for this repository'), category='error')
171 raise HTTPNotFound()
171 raise HTTPNotFound()
172 except RepositoryError as e:
172 except RepositoryError as e:
173 log.warning('Repository error while fetching '
173 log.warning('Repository error while fetching '
174 'filenode `%s`. Err:%s', path, e)
174 'filenode `%s`. Err:%s', path, e)
175 h.flash(safe_str(h.escape(e)), category='error')
175 h.flash(safe_str(h.escape(e)), category='error')
176 raise HTTPNotFound()
176 raise HTTPNotFound()
177
177
178 return file_node
178 return file_node
179
179
180 def _is_valid_head(self, commit_id, repo):
180 def _is_valid_head(self, commit_id, repo):
181 # check if commit is a branch identifier- basically we cannot
181 # check if commit is a branch identifier- basically we cannot
182 # create multiple heads via file editing
182 # create multiple heads via file editing
183 valid_heads = repo.branches.keys() + repo.branches.values()
183 valid_heads = repo.branches.keys() + repo.branches.values()
184
184
185 if h.is_svn(repo) and not repo.is_empty():
185 if h.is_svn(repo) and not repo.is_empty():
186 # Note: Subversion only has one head, we add it here in case there
186 # Note: Subversion only has one head, we add it here in case there
187 # is no branch matched.
187 # is no branch matched.
188 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
188 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
189
189
190 # check if commit is a branch name or branch hash
190 # check if commit is a branch name or branch hash
191 return commit_id in valid_heads
191 return commit_id in valid_heads
192
192
193 def _get_tree_cache_manager(self, namespace_type):
193 def _get_tree_cache_manager(self, namespace_type):
194 _namespace = caches.get_repo_namespace_key(
194 _namespace = caches.get_repo_namespace_key(
195 namespace_type, self.db_repo_name)
195 namespace_type, self.db_repo_name)
196 return caches.get_cache_manager('repo_cache_long', _namespace)
196 return caches.get_cache_manager('repo_cache_long', _namespace)
197
197
198 def _get_tree_at_commit(
198 def _get_tree_at_commit(
199 self, c, commit_id, f_path, full_load=False, force=False):
199 self, c, commit_id, f_path, full_load=False, force=False):
200 def _cached_tree():
200 def _cached_tree():
201 log.debug('Generating cached file tree for %s, %s, %s',
201 log.debug('Generating cached file tree for %s, %s, %s',
202 self.db_repo_name, commit_id, f_path)
202 self.db_repo_name, commit_id, f_path)
203
203
204 c.full_load = full_load
204 c.full_load = full_load
205 return render(
205 return render(
206 'rhodecode:templates/files/files_browser_tree.mako',
206 'rhodecode:templates/files/files_browser_tree.mako',
207 self._get_template_context(c), self.request)
207 self._get_template_context(c), self.request)
208
208
209 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
209 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
210
210
211 cache_key = caches.compute_key_from_params(
211 cache_key = caches.compute_key_from_params(
212 self.db_repo_name, commit_id, f_path)
212 self.db_repo_name, commit_id, f_path)
213
213
214 if force:
214 if force:
215 # we want to force recompute of caches
215 # we want to force recompute of caches
216 cache_manager.remove_value(cache_key)
216 cache_manager.remove_value(cache_key)
217
217
218 return cache_manager.get(cache_key, createfunc=_cached_tree)
218 return cache_manager.get(cache_key, createfunc=_cached_tree)
219
219
220 def _get_archive_spec(self, fname):
220 def _get_archive_spec(self, fname):
221 log.debug('Detecting archive spec for: `%s`', fname)
221 log.debug('Detecting archive spec for: `%s`', fname)
222
222
223 fileformat = None
223 fileformat = None
224 ext = None
224 ext = None
225 content_type = None
225 content_type = None
226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
227 content_type, extension = ext_data
227 content_type, extension = ext_data
228
228
229 if fname.endswith(extension):
229 if fname.endswith(extension):
230 fileformat = a_type
230 fileformat = a_type
231 log.debug('archive is of type: %s', fileformat)
231 log.debug('archive is of type: %s', fileformat)
232 ext = extension
232 ext = extension
233 break
233 break
234
234
235 if not fileformat:
235 if not fileformat:
236 raise ValueError()
236 raise ValueError()
237
237
238 # left over part of whole fname is the commit
238 # left over part of whole fname is the commit
239 commit_id = fname[:-len(ext)]
239 commit_id = fname[:-len(ext)]
240
240
241 return commit_id, ext, fileformat, content_type
241 return commit_id, ext, fileformat, content_type
242
242
243 @LoginRequired()
243 @LoginRequired()
244 @HasRepoPermissionAnyDecorator(
244 @HasRepoPermissionAnyDecorator(
245 'repository.read', 'repository.write', 'repository.admin')
245 'repository.read', 'repository.write', 'repository.admin')
246 @view_config(
246 @view_config(
247 route_name='repo_archivefile', request_method='GET',
247 route_name='repo_archivefile', request_method='GET',
248 renderer=None)
248 renderer=None)
249 def repo_archivefile(self):
249 def repo_archivefile(self):
250 # archive cache config
250 # archive cache config
251 from rhodecode import CONFIG
251 from rhodecode import CONFIG
252 _ = self.request.translate
252 _ = self.request.translate
253 self.load_default_context()
253 self.load_default_context()
254
254
255 fname = self.request.matchdict['fname']
255 fname = self.request.matchdict['fname']
256 subrepos = self.request.GET.get('subrepos') == 'true'
256 subrepos = self.request.GET.get('subrepos') == 'true'
257
257
258 if not self.db_repo.enable_downloads:
258 if not self.db_repo.enable_downloads:
259 return Response(_('Downloads disabled'))
259 return Response(_('Downloads disabled'))
260
260
261 try:
261 try:
262 commit_id, ext, fileformat, content_type = \
262 commit_id, ext, fileformat, content_type = \
263 self._get_archive_spec(fname)
263 self._get_archive_spec(fname)
264 except ValueError:
264 except ValueError:
265 return Response(_('Unknown archive type for: `{}`').format(
265 return Response(_('Unknown archive type for: `{}`').format(
266 h.escape(fname)))
266 h.escape(fname)))
267
267
268 try:
268 try:
269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
270 except CommitDoesNotExistError:
270 except CommitDoesNotExistError:
271 return Response(_('Unknown commit_id {}').format(
271 return Response(_('Unknown commit_id {}').format(
272 h.escape(commit_id)))
272 h.escape(commit_id)))
273 except EmptyRepositoryError:
273 except EmptyRepositoryError:
274 return Response(_('Empty repository'))
274 return Response(_('Empty repository'))
275
275
276 archive_name = '%s-%s%s%s' % (
276 archive_name = '%s-%s%s%s' % (
277 safe_str(self.db_repo_name.replace('/', '_')),
277 safe_str(self.db_repo_name.replace('/', '_')),
278 '-sub' if subrepos else '',
278 '-sub' if subrepos else '',
279 safe_str(commit.short_id), ext)
279 safe_str(commit.short_id), ext)
280
280
281 use_cached_archive = False
281 use_cached_archive = False
282 archive_cache_enabled = CONFIG.get(
282 archive_cache_enabled = CONFIG.get(
283 'archive_cache_dir') and not self.request.GET.get('no_cache')
283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284
284
285 if archive_cache_enabled:
285 if archive_cache_enabled:
286 # check if we it's ok to write
286 # check if we it's ok to write
287 if not os.path.isdir(CONFIG['archive_cache_dir']):
287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 os.makedirs(CONFIG['archive_cache_dir'])
288 os.makedirs(CONFIG['archive_cache_dir'])
289 cached_archive_path = os.path.join(
289 cached_archive_path = os.path.join(
290 CONFIG['archive_cache_dir'], archive_name)
290 CONFIG['archive_cache_dir'], archive_name)
291 if os.path.isfile(cached_archive_path):
291 if os.path.isfile(cached_archive_path):
292 log.debug('Found cached archive in %s', cached_archive_path)
292 log.debug('Found cached archive in %s', cached_archive_path)
293 fd, archive = None, cached_archive_path
293 fd, archive = None, cached_archive_path
294 use_cached_archive = True
294 use_cached_archive = True
295 else:
295 else:
296 log.debug('Archive %s is not yet cached', archive_name)
296 log.debug('Archive %s is not yet cached', archive_name)
297
297
298 if not use_cached_archive:
298 if not use_cached_archive:
299 # generate new archive
299 # generate new archive
300 fd, archive = tempfile.mkstemp()
300 fd, archive = tempfile.mkstemp()
301 log.debug('Creating new temp archive in %s', archive)
301 log.debug('Creating new temp archive in %s', archive)
302 try:
302 try:
303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 except ImproperArchiveTypeError:
304 except ImproperArchiveTypeError:
305 return _('Unknown archive type')
305 return _('Unknown archive type')
306 if archive_cache_enabled:
306 if archive_cache_enabled:
307 # if we generated the archive and we have cache enabled
307 # if we generated the archive and we have cache enabled
308 # let's use this for future
308 # let's use this for future
309 log.debug('Storing new archive in %s', cached_archive_path)
309 log.debug('Storing new archive in %s', cached_archive_path)
310 shutil.move(archive, cached_archive_path)
310 shutil.move(archive, cached_archive_path)
311 archive = cached_archive_path
311 archive = cached_archive_path
312
312
313 # store download action
313 # store download action
314 audit_logger.store_web(
314 audit_logger.store_web(
315 'repo.archive.download', action_data={
315 'repo.archive.download', action_data={
316 'user_agent': self.request.user_agent,
316 'user_agent': self.request.user_agent,
317 'archive_name': archive_name,
317 'archive_name': archive_name,
318 'archive_spec': fname,
318 'archive_spec': fname,
319 'archive_cached': use_cached_archive},
319 'archive_cached': use_cached_archive},
320 user=self._rhodecode_user,
320 user=self._rhodecode_user,
321 repo=self.db_repo,
321 repo=self.db_repo,
322 commit=True
322 commit=True
323 )
323 )
324
324
325 def get_chunked_archive(archive):
325 def get_chunked_archive(archive):
326 with open(archive, 'rb') as stream:
326 with open(archive, 'rb') as stream:
327 while True:
327 while True:
328 data = stream.read(16 * 1024)
328 data = stream.read(16 * 1024)
329 if not data:
329 if not data:
330 if fd: # fd means we used temporary file
330 if fd: # fd means we used temporary file
331 os.close(fd)
331 os.close(fd)
332 if not archive_cache_enabled:
332 if not archive_cache_enabled:
333 log.debug('Destroying temp archive %s', archive)
333 log.debug('Destroying temp archive %s', archive)
334 os.remove(archive)
334 os.remove(archive)
335 break
335 break
336 yield data
336 yield data
337
337
338 response = Response(app_iter=get_chunked_archive(archive))
338 response = Response(app_iter=get_chunked_archive(archive))
339 response.content_disposition = str(
339 response.content_disposition = str(
340 'attachment; filename=%s' % archive_name)
340 'attachment; filename=%s' % archive_name)
341 response.content_type = str(content_type)
341 response.content_type = str(content_type)
342
342
343 return response
343 return response
344
344
345 def _get_file_node(self, commit_id, f_path):
345 def _get_file_node(self, commit_id, f_path):
346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 try:
348 try:
349 node = commit.get_node(f_path)
349 node = commit.get_node(f_path)
350 if node.is_dir():
350 if node.is_dir():
351 raise NodeError('%s path is a %s not a file'
351 raise NodeError('%s path is a %s not a file'
352 % (node, type(node)))
352 % (node, type(node)))
353 except NodeDoesNotExistError:
353 except NodeDoesNotExistError:
354 commit = EmptyCommit(
354 commit = EmptyCommit(
355 commit_id=commit_id,
355 commit_id=commit_id,
356 idx=commit.idx,
356 idx=commit.idx,
357 repo=commit.repository,
357 repo=commit.repository,
358 alias=commit.repository.alias,
358 alias=commit.repository.alias,
359 message=commit.message,
359 message=commit.message,
360 author=commit.author,
360 author=commit.author,
361 date=commit.date)
361 date=commit.date)
362 node = FileNode(f_path, '', commit=commit)
362 node = FileNode(f_path, '', commit=commit)
363 else:
363 else:
364 commit = EmptyCommit(
364 commit = EmptyCommit(
365 repo=self.rhodecode_vcs_repo,
365 repo=self.rhodecode_vcs_repo,
366 alias=self.rhodecode_vcs_repo.alias)
366 alias=self.rhodecode_vcs_repo.alias)
367 node = FileNode(f_path, '', commit=commit)
367 node = FileNode(f_path, '', commit=commit)
368 return node
368 return node
369
369
370 @LoginRequired()
370 @LoginRequired()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @view_config(
373 @view_config(
374 route_name='repo_files_diff', request_method='GET',
374 route_name='repo_files_diff', request_method='GET',
375 renderer=None)
375 renderer=None)
376 def repo_files_diff(self):
376 def repo_files_diff(self):
377 c = self.load_default_context()
377 c = self.load_default_context()
378 f_path = self._get_f_path(self.request.matchdict)
378 f_path = self._get_f_path(self.request.matchdict)
379 diff1 = self.request.GET.get('diff1', '')
379 diff1 = self.request.GET.get('diff1', '')
380 diff2 = self.request.GET.get('diff2', '')
380 diff2 = self.request.GET.get('diff2', '')
381
381
382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383
383
384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 line_context = self.request.GET.get('context', 3)
385 line_context = self.request.GET.get('context', 3)
386
386
387 if not any((diff1, diff2)):
387 if not any((diff1, diff2)):
388 h.flash(
388 h.flash(
389 'Need query parameter "diff1" or "diff2" to generate a diff.',
389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 category='error')
390 category='error')
391 raise HTTPBadRequest()
391 raise HTTPBadRequest()
392
392
393 c.action = self.request.GET.get('diff')
393 c.action = self.request.GET.get('diff')
394 if c.action not in ['download', 'raw']:
394 if c.action not in ['download', 'raw']:
395 compare_url = h.route_path(
395 compare_url = h.route_path(
396 'repo_compare',
396 'repo_compare',
397 repo_name=self.db_repo_name,
397 repo_name=self.db_repo_name,
398 source_ref_type='rev',
398 source_ref_type='rev',
399 source_ref=diff1,
399 source_ref=diff1,
400 target_repo=self.db_repo_name,
400 target_repo=self.db_repo_name,
401 target_ref_type='rev',
401 target_ref_type='rev',
402 target_ref=diff2,
402 target_ref=diff2,
403 _query=dict(f_path=f_path))
403 _query=dict(f_path=f_path))
404 # redirect to new view if we render diff
404 # redirect to new view if we render diff
405 raise HTTPFound(compare_url)
405 raise HTTPFound(compare_url)
406
406
407 try:
407 try:
408 node1 = self._get_file_node(diff1, path1)
408 node1 = self._get_file_node(diff1, path1)
409 node2 = self._get_file_node(diff2, f_path)
409 node2 = self._get_file_node(diff2, f_path)
410 except (RepositoryError, NodeError):
410 except (RepositoryError, NodeError):
411 log.exception("Exception while trying to get node from repository")
411 log.exception("Exception while trying to get node from repository")
412 raise HTTPFound(
412 raise HTTPFound(
413 h.route_path('repo_files', repo_name=self.db_repo_name,
413 h.route_path('repo_files', repo_name=self.db_repo_name,
414 commit_id='tip', f_path=f_path))
414 commit_id='tip', f_path=f_path))
415
415
416 if all(isinstance(node.commit, EmptyCommit)
416 if all(isinstance(node.commit, EmptyCommit)
417 for node in (node1, node2)):
417 for node in (node1, node2)):
418 raise HTTPNotFound()
418 raise HTTPNotFound()
419
419
420 c.commit_1 = node1.commit
420 c.commit_1 = node1.commit
421 c.commit_2 = node2.commit
421 c.commit_2 = node2.commit
422
422
423 if c.action == 'download':
423 if c.action == 'download':
424 _diff = diffs.get_gitdiff(node1, node2,
424 _diff = diffs.get_gitdiff(node1, node2,
425 ignore_whitespace=ignore_whitespace,
425 ignore_whitespace=ignore_whitespace,
426 context=line_context)
426 context=line_context)
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428
428
429 response = Response(diff.as_raw())
429 response = Response(self.path_filter.get_raw_patch(diff))
430 response.content_type = 'text/plain'
430 response.content_type = 'text/plain'
431 response.content_disposition = (
431 response.content_disposition = (
432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
433 )
433 )
434 charset = self._get_default_encoding(c)
434 charset = self._get_default_encoding(c)
435 if charset:
435 if charset:
436 response.charset = charset
436 response.charset = charset
437 return response
437 return response
438
438
439 elif c.action == 'raw':
439 elif c.action == 'raw':
440 _diff = diffs.get_gitdiff(node1, node2,
440 _diff = diffs.get_gitdiff(node1, node2,
441 ignore_whitespace=ignore_whitespace,
441 ignore_whitespace=ignore_whitespace,
442 context=line_context)
442 context=line_context)
443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
444
444
445 response = Response(diff.as_raw())
445 response = Response(self.path_filter.get_raw_patch(diff))
446 response.content_type = 'text/plain'
446 response.content_type = 'text/plain'
447 charset = self._get_default_encoding(c)
447 charset = self._get_default_encoding(c)
448 if charset:
448 if charset:
449 response.charset = charset
449 response.charset = charset
450 return response
450 return response
451
451
452 # in case we ever end up here
452 # in case we ever end up here
453 raise HTTPNotFound()
453 raise HTTPNotFound()
454
454
455 @LoginRequired()
455 @LoginRequired()
456 @HasRepoPermissionAnyDecorator(
456 @HasRepoPermissionAnyDecorator(
457 'repository.read', 'repository.write', 'repository.admin')
457 'repository.read', 'repository.write', 'repository.admin')
458 @view_config(
458 @view_config(
459 route_name='repo_files_diff_2way_redirect', request_method='GET',
459 route_name='repo_files_diff_2way_redirect', request_method='GET',
460 renderer=None)
460 renderer=None)
461 def repo_files_diff_2way_redirect(self):
461 def repo_files_diff_2way_redirect(self):
462 """
462 """
463 Kept only to make OLD links work
463 Kept only to make OLD links work
464 """
464 """
465 f_path = self._get_f_path(self.request.matchdict)
465 f_path = self._get_f_path(self.request.matchdict)
466 diff1 = self.request.GET.get('diff1', '')
466 diff1 = self.request.GET.get('diff1', '')
467 diff2 = self.request.GET.get('diff2', '')
467 diff2 = self.request.GET.get('diff2', '')
468
468
469 if not any((diff1, diff2)):
469 if not any((diff1, diff2)):
470 h.flash(
470 h.flash(
471 'Need query parameter "diff1" or "diff2" to generate a diff.',
471 'Need query parameter "diff1" or "diff2" to generate a diff.',
472 category='error')
472 category='error')
473 raise HTTPBadRequest()
473 raise HTTPBadRequest()
474
474
475 compare_url = h.route_path(
475 compare_url = h.route_path(
476 'repo_compare',
476 'repo_compare',
477 repo_name=self.db_repo_name,
477 repo_name=self.db_repo_name,
478 source_ref_type='rev',
478 source_ref_type='rev',
479 source_ref=diff1,
479 source_ref=diff1,
480 target_ref_type='rev',
480 target_ref_type='rev',
481 target_ref=diff2,
481 target_ref=diff2,
482 _query=dict(f_path=f_path, diffmode='sideside',
482 _query=dict(f_path=f_path, diffmode='sideside',
483 target_repo=self.db_repo_name,))
483 target_repo=self.db_repo_name,))
484 raise HTTPFound(compare_url)
484 raise HTTPFound(compare_url)
485
485
486 @LoginRequired()
486 @LoginRequired()
487 @HasRepoPermissionAnyDecorator(
487 @HasRepoPermissionAnyDecorator(
488 'repository.read', 'repository.write', 'repository.admin')
488 'repository.read', 'repository.write', 'repository.admin')
489 @view_config(
489 @view_config(
490 route_name='repo_files', request_method='GET',
490 route_name='repo_files', request_method='GET',
491 renderer=None)
491 renderer=None)
492 @view_config(
492 @view_config(
493 route_name='repo_files:default_path', request_method='GET',
493 route_name='repo_files:default_path', request_method='GET',
494 renderer=None)
494 renderer=None)
495 @view_config(
495 @view_config(
496 route_name='repo_files:default_commit', request_method='GET',
496 route_name='repo_files:default_commit', request_method='GET',
497 renderer=None)
497 renderer=None)
498 @view_config(
498 @view_config(
499 route_name='repo_files:rendered', request_method='GET',
499 route_name='repo_files:rendered', request_method='GET',
500 renderer=None)
500 renderer=None)
501 @view_config(
501 @view_config(
502 route_name='repo_files:annotated', request_method='GET',
502 route_name='repo_files:annotated', request_method='GET',
503 renderer=None)
503 renderer=None)
504 def repo_files(self):
504 def repo_files(self):
505 c = self.load_default_context()
505 c = self.load_default_context()
506
506
507 view_name = getattr(self.request.matched_route, 'name', None)
507 view_name = getattr(self.request.matched_route, 'name', None)
508
508
509 c.annotate = view_name == 'repo_files:annotated'
509 c.annotate = view_name == 'repo_files:annotated'
510 # default is false, but .rst/.md files later are auto rendered, we can
510 # default is false, but .rst/.md files later are auto rendered, we can
511 # overwrite auto rendering by setting this GET flag
511 # overwrite auto rendering by setting this GET flag
512 c.renderer = view_name == 'repo_files:rendered' or \
512 c.renderer = view_name == 'repo_files:rendered' or \
513 not self.request.GET.get('no-render', False)
513 not self.request.GET.get('no-render', False)
514
514
515 # redirect to given commit_id from form if given
515 # redirect to given commit_id from form if given
516 get_commit_id = self.request.GET.get('at_rev', None)
516 get_commit_id = self.request.GET.get('at_rev', None)
517 if get_commit_id:
517 if get_commit_id:
518 self._get_commit_or_redirect(get_commit_id)
518 self._get_commit_or_redirect(get_commit_id)
519
519
520 commit_id, f_path = self._get_commit_and_path()
520 commit_id, f_path = self._get_commit_and_path()
521 c.commit = self._get_commit_or_redirect(commit_id)
521 c.commit = self._get_commit_or_redirect(commit_id)
522 c.branch = self.request.GET.get('branch', None)
522 c.branch = self.request.GET.get('branch', None)
523 c.f_path = f_path
523 c.f_path = f_path
524
524
525 # prev link
525 # prev link
526 try:
526 try:
527 prev_commit = c.commit.prev(c.branch)
527 prev_commit = c.commit.prev(c.branch)
528 c.prev_commit = prev_commit
528 c.prev_commit = prev_commit
529 c.url_prev = h.route_path(
529 c.url_prev = h.route_path(
530 'repo_files', repo_name=self.db_repo_name,
530 'repo_files', repo_name=self.db_repo_name,
531 commit_id=prev_commit.raw_id, f_path=f_path)
531 commit_id=prev_commit.raw_id, f_path=f_path)
532 if c.branch:
532 if c.branch:
533 c.url_prev += '?branch=%s' % c.branch
533 c.url_prev += '?branch=%s' % c.branch
534 except (CommitDoesNotExistError, VCSError):
534 except (CommitDoesNotExistError, VCSError):
535 c.url_prev = '#'
535 c.url_prev = '#'
536 c.prev_commit = EmptyCommit()
536 c.prev_commit = EmptyCommit()
537
537
538 # next link
538 # next link
539 try:
539 try:
540 next_commit = c.commit.next(c.branch)
540 next_commit = c.commit.next(c.branch)
541 c.next_commit = next_commit
541 c.next_commit = next_commit
542 c.url_next = h.route_path(
542 c.url_next = h.route_path(
543 'repo_files', repo_name=self.db_repo_name,
543 'repo_files', repo_name=self.db_repo_name,
544 commit_id=next_commit.raw_id, f_path=f_path)
544 commit_id=next_commit.raw_id, f_path=f_path)
545 if c.branch:
545 if c.branch:
546 c.url_next += '?branch=%s' % c.branch
546 c.url_next += '?branch=%s' % c.branch
547 except (CommitDoesNotExistError, VCSError):
547 except (CommitDoesNotExistError, VCSError):
548 c.url_next = '#'
548 c.url_next = '#'
549 c.next_commit = EmptyCommit()
549 c.next_commit = EmptyCommit()
550
550
551 # files or dirs
551 # files or dirs
552 try:
552 try:
553 c.file = c.commit.get_node(f_path)
553 c.file = c.commit.get_node(f_path)
554 c.file_author = True
554 c.file_author = True
555 c.file_tree = ''
555 c.file_tree = ''
556
556
557 # load file content
557 # load file content
558 if c.file.is_file():
558 if c.file.is_file():
559 c.lf_node = c.file.get_largefile_node()
559 c.lf_node = c.file.get_largefile_node()
560
560
561 c.file_source_page = 'true'
561 c.file_source_page = 'true'
562 c.file_last_commit = c.file.last_commit
562 c.file_last_commit = c.file.last_commit
563 if c.file.size < c.visual.cut_off_limit_diff:
563 if c.file.size < c.visual.cut_off_limit_diff:
564 if c.annotate: # annotation has precedence over renderer
564 if c.annotate: # annotation has precedence over renderer
565 c.annotated_lines = filenode_as_annotated_lines_tokens(
565 c.annotated_lines = filenode_as_annotated_lines_tokens(
566 c.file
566 c.file
567 )
567 )
568 else:
568 else:
569 c.renderer = (
569 c.renderer = (
570 c.renderer and h.renderer_from_filename(c.file.path)
570 c.renderer and h.renderer_from_filename(c.file.path)
571 )
571 )
572 if not c.renderer:
572 if not c.renderer:
573 c.lines = filenode_as_lines_tokens(c.file)
573 c.lines = filenode_as_lines_tokens(c.file)
574
574
575 c.on_branch_head = self._is_valid_head(
575 c.on_branch_head = self._is_valid_head(
576 commit_id, self.rhodecode_vcs_repo)
576 commit_id, self.rhodecode_vcs_repo)
577
577
578 branch = c.commit.branch if (
578 branch = c.commit.branch if (
579 c.commit.branch and '/' not in c.commit.branch) else None
579 c.commit.branch and '/' not in c.commit.branch) else None
580 c.branch_or_raw_id = branch or c.commit.raw_id
580 c.branch_or_raw_id = branch or c.commit.raw_id
581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
582
582
583 author = c.file_last_commit.author
583 author = c.file_last_commit.author
584 c.authors = [[
584 c.authors = [[
585 h.email(author),
585 h.email(author),
586 h.person(author, 'username_or_name_or_email'),
586 h.person(author, 'username_or_name_or_email'),
587 1
587 1
588 ]]
588 ]]
589
589
590 else: # load tree content at path
590 else: # load tree content at path
591 c.file_source_page = 'false'
591 c.file_source_page = 'false'
592 c.authors = []
592 c.authors = []
593 # this loads a simple tree without metadata to speed things up
593 # this loads a simple tree without metadata to speed things up
594 # later via ajax we call repo_nodetree_full and fetch whole
594 # later via ajax we call repo_nodetree_full and fetch whole
595 c.file_tree = self._get_tree_at_commit(
595 c.file_tree = self._get_tree_at_commit(
596 c, c.commit.raw_id, f_path)
596 c, c.commit.raw_id, f_path)
597
597
598 except RepositoryError as e:
598 except RepositoryError as e:
599 h.flash(safe_str(h.escape(e)), category='error')
599 h.flash(safe_str(h.escape(e)), category='error')
600 raise HTTPNotFound()
600 raise HTTPNotFound()
601
601
602 if self.request.environ.get('HTTP_X_PJAX'):
602 if self.request.environ.get('HTTP_X_PJAX'):
603 html = render('rhodecode:templates/files/files_pjax.mako',
603 html = render('rhodecode:templates/files/files_pjax.mako',
604 self._get_template_context(c), self.request)
604 self._get_template_context(c), self.request)
605 else:
605 else:
606 html = render('rhodecode:templates/files/files.mako',
606 html = render('rhodecode:templates/files/files.mako',
607 self._get_template_context(c), self.request)
607 self._get_template_context(c), self.request)
608 return Response(html)
608 return Response(html)
609
609
610 @HasRepoPermissionAnyDecorator(
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
612 @view_config(
613 route_name='repo_files:annotated_previous', request_method='GET',
613 route_name='repo_files:annotated_previous', request_method='GET',
614 renderer=None)
614 renderer=None)
615 def repo_files_annotated_previous(self):
615 def repo_files_annotated_previous(self):
616 self.load_default_context()
616 self.load_default_context()
617
617
618 commit_id, f_path = self._get_commit_and_path()
618 commit_id, f_path = self._get_commit_and_path()
619 commit = self._get_commit_or_redirect(commit_id)
619 commit = self._get_commit_or_redirect(commit_id)
620 prev_commit_id = commit.raw_id
620 prev_commit_id = commit.raw_id
621 line_anchor = self.request.GET.get('line_anchor')
621 line_anchor = self.request.GET.get('line_anchor')
622 is_file = False
622 is_file = False
623 try:
623 try:
624 _file = commit.get_node(f_path)
624 _file = commit.get_node(f_path)
625 is_file = _file.is_file()
625 is_file = _file.is_file()
626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
627 pass
627 pass
628
628
629 if is_file:
629 if is_file:
630 history = commit.get_file_history(f_path)
630 history = commit.get_file_history(f_path)
631 prev_commit_id = history[1].raw_id \
631 prev_commit_id = history[1].raw_id \
632 if len(history) > 1 else prev_commit_id
632 if len(history) > 1 else prev_commit_id
633 prev_url = h.route_path(
633 prev_url = h.route_path(
634 'repo_files:annotated', repo_name=self.db_repo_name,
634 'repo_files:annotated', repo_name=self.db_repo_name,
635 commit_id=prev_commit_id, f_path=f_path,
635 commit_id=prev_commit_id, f_path=f_path,
636 _anchor='L{}'.format(line_anchor))
636 _anchor='L{}'.format(line_anchor))
637
637
638 raise HTTPFound(prev_url)
638 raise HTTPFound(prev_url)
639
639
640 @LoginRequired()
640 @LoginRequired()
641 @HasRepoPermissionAnyDecorator(
641 @HasRepoPermissionAnyDecorator(
642 'repository.read', 'repository.write', 'repository.admin')
642 'repository.read', 'repository.write', 'repository.admin')
643 @view_config(
643 @view_config(
644 route_name='repo_nodetree_full', request_method='GET',
644 route_name='repo_nodetree_full', request_method='GET',
645 renderer=None, xhr=True)
645 renderer=None, xhr=True)
646 @view_config(
646 @view_config(
647 route_name='repo_nodetree_full:default_path', request_method='GET',
647 route_name='repo_nodetree_full:default_path', request_method='GET',
648 renderer=None, xhr=True)
648 renderer=None, xhr=True)
649 def repo_nodetree_full(self):
649 def repo_nodetree_full(self):
650 """
650 """
651 Returns rendered html of file tree that contains commit date,
651 Returns rendered html of file tree that contains commit date,
652 author, commit_id for the specified combination of
652 author, commit_id for the specified combination of
653 repo, commit_id and file path
653 repo, commit_id and file path
654 """
654 """
655 c = self.load_default_context()
655 c = self.load_default_context()
656
656
657 commit_id, f_path = self._get_commit_and_path()
657 commit_id, f_path = self._get_commit_and_path()
658 commit = self._get_commit_or_redirect(commit_id)
658 commit = self._get_commit_or_redirect(commit_id)
659 try:
659 try:
660 dir_node = commit.get_node(f_path)
660 dir_node = commit.get_node(f_path)
661 except RepositoryError as e:
661 except RepositoryError as e:
662 return Response('error: {}'.format(h.escape(safe_str(e))))
662 return Response('error: {}'.format(h.escape(safe_str(e))))
663
663
664 if dir_node.is_file():
664 if dir_node.is_file():
665 return Response('')
665 return Response('')
666
666
667 c.file = dir_node
667 c.file = dir_node
668 c.commit = commit
668 c.commit = commit
669
669
670 # using force=True here, make a little trick. We flush the cache and
670 # using force=True here, make a little trick. We flush the cache and
671 # compute it using the same key as without previous full_load, so now
671 # compute it using the same key as without previous full_load, so now
672 # the fully loaded tree is now returned instead of partial,
672 # the fully loaded tree is now returned instead of partial,
673 # and we store this in caches
673 # and we store this in caches
674 html = self._get_tree_at_commit(
674 html = self._get_tree_at_commit(
675 c, commit.raw_id, dir_node.path, full_load=True, force=True)
675 c, commit.raw_id, dir_node.path, full_load=True, force=True)
676
676
677 return Response(html)
677 return Response(html)
678
678
679 def _get_attachement_disposition(self, f_path):
679 def _get_attachement_disposition(self, f_path):
680 return 'attachment; filename=%s' % \
680 return 'attachment; filename=%s' % \
681 safe_str(f_path.split(Repository.NAME_SEP)[-1])
681 safe_str(f_path.split(Repository.NAME_SEP)[-1])
682
682
683 @LoginRequired()
683 @LoginRequired()
684 @HasRepoPermissionAnyDecorator(
684 @HasRepoPermissionAnyDecorator(
685 'repository.read', 'repository.write', 'repository.admin')
685 'repository.read', 'repository.write', 'repository.admin')
686 @view_config(
686 @view_config(
687 route_name='repo_file_raw', request_method='GET',
687 route_name='repo_file_raw', request_method='GET',
688 renderer=None)
688 renderer=None)
689 def repo_file_raw(self):
689 def repo_file_raw(self):
690 """
690 """
691 Action for show as raw, some mimetypes are "rendered",
691 Action for show as raw, some mimetypes are "rendered",
692 those include images, icons.
692 those include images, icons.
693 """
693 """
694 c = self.load_default_context()
694 c = self.load_default_context()
695
695
696 commit_id, f_path = self._get_commit_and_path()
696 commit_id, f_path = self._get_commit_and_path()
697 commit = self._get_commit_or_redirect(commit_id)
697 commit = self._get_commit_or_redirect(commit_id)
698 file_node = self._get_filenode_or_redirect(commit, f_path)
698 file_node = self._get_filenode_or_redirect(commit, f_path)
699
699
700 raw_mimetype_mapping = {
700 raw_mimetype_mapping = {
701 # map original mimetype to a mimetype used for "show as raw"
701 # map original mimetype to a mimetype used for "show as raw"
702 # you can also provide a content-disposition to override the
702 # you can also provide a content-disposition to override the
703 # default "attachment" disposition.
703 # default "attachment" disposition.
704 # orig_type: (new_type, new_dispo)
704 # orig_type: (new_type, new_dispo)
705
705
706 # show images inline:
706 # show images inline:
707 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
707 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
708 # for example render an SVG with javascript inside or even render
708 # for example render an SVG with javascript inside or even render
709 # HTML.
709 # HTML.
710 'image/x-icon': ('image/x-icon', 'inline'),
710 'image/x-icon': ('image/x-icon', 'inline'),
711 'image/png': ('image/png', 'inline'),
711 'image/png': ('image/png', 'inline'),
712 'image/gif': ('image/gif', 'inline'),
712 'image/gif': ('image/gif', 'inline'),
713 'image/jpeg': ('image/jpeg', 'inline'),
713 'image/jpeg': ('image/jpeg', 'inline'),
714 'application/pdf': ('application/pdf', 'inline'),
714 'application/pdf': ('application/pdf', 'inline'),
715 }
715 }
716
716
717 mimetype = file_node.mimetype
717 mimetype = file_node.mimetype
718 try:
718 try:
719 mimetype, disposition = raw_mimetype_mapping[mimetype]
719 mimetype, disposition = raw_mimetype_mapping[mimetype]
720 except KeyError:
720 except KeyError:
721 # we don't know anything special about this, handle it safely
721 # we don't know anything special about this, handle it safely
722 if file_node.is_binary:
722 if file_node.is_binary:
723 # do same as download raw for binary files
723 # do same as download raw for binary files
724 mimetype, disposition = 'application/octet-stream', 'attachment'
724 mimetype, disposition = 'application/octet-stream', 'attachment'
725 else:
725 else:
726 # do not just use the original mimetype, but force text/plain,
726 # do not just use the original mimetype, but force text/plain,
727 # otherwise it would serve text/html and that might be unsafe.
727 # otherwise it would serve text/html and that might be unsafe.
728 # Note: underlying vcs library fakes text/plain mimetype if the
728 # Note: underlying vcs library fakes text/plain mimetype if the
729 # mimetype can not be determined and it thinks it is not
729 # mimetype can not be determined and it thinks it is not
730 # binary.This might lead to erroneous text display in some
730 # binary.This might lead to erroneous text display in some
731 # cases, but helps in other cases, like with text files
731 # cases, but helps in other cases, like with text files
732 # without extension.
732 # without extension.
733 mimetype, disposition = 'text/plain', 'inline'
733 mimetype, disposition = 'text/plain', 'inline'
734
734
735 if disposition == 'attachment':
735 if disposition == 'attachment':
736 disposition = self._get_attachement_disposition(f_path)
736 disposition = self._get_attachement_disposition(f_path)
737
737
738 def stream_node():
738 def stream_node():
739 yield file_node.raw_bytes
739 yield file_node.raw_bytes
740
740
741 response = Response(app_iter=stream_node())
741 response = Response(app_iter=stream_node())
742 response.content_disposition = disposition
742 response.content_disposition = disposition
743 response.content_type = mimetype
743 response.content_type = mimetype
744
744
745 charset = self._get_default_encoding(c)
745 charset = self._get_default_encoding(c)
746 if charset:
746 if charset:
747 response.charset = charset
747 response.charset = charset
748
748
749 return response
749 return response
750
750
751 @LoginRequired()
751 @LoginRequired()
752 @HasRepoPermissionAnyDecorator(
752 @HasRepoPermissionAnyDecorator(
753 'repository.read', 'repository.write', 'repository.admin')
753 'repository.read', 'repository.write', 'repository.admin')
754 @view_config(
754 @view_config(
755 route_name='repo_file_download', request_method='GET',
755 route_name='repo_file_download', request_method='GET',
756 renderer=None)
756 renderer=None)
757 @view_config(
757 @view_config(
758 route_name='repo_file_download:legacy', request_method='GET',
758 route_name='repo_file_download:legacy', request_method='GET',
759 renderer=None)
759 renderer=None)
760 def repo_file_download(self):
760 def repo_file_download(self):
761 c = self.load_default_context()
761 c = self.load_default_context()
762
762
763 commit_id, f_path = self._get_commit_and_path()
763 commit_id, f_path = self._get_commit_and_path()
764 commit = self._get_commit_or_redirect(commit_id)
764 commit = self._get_commit_or_redirect(commit_id)
765 file_node = self._get_filenode_or_redirect(commit, f_path)
765 file_node = self._get_filenode_or_redirect(commit, f_path)
766
766
767 if self.request.GET.get('lf'):
767 if self.request.GET.get('lf'):
768 # only if lf get flag is passed, we download this file
768 # only if lf get flag is passed, we download this file
769 # as LFS/Largefile
769 # as LFS/Largefile
770 lf_node = file_node.get_largefile_node()
770 lf_node = file_node.get_largefile_node()
771 if lf_node:
771 if lf_node:
772 # overwrite our pointer with the REAL large-file
772 # overwrite our pointer with the REAL large-file
773 file_node = lf_node
773 file_node = lf_node
774
774
775 disposition = self._get_attachement_disposition(f_path)
775 disposition = self._get_attachement_disposition(f_path)
776
776
777 def stream_node():
777 def stream_node():
778 yield file_node.raw_bytes
778 yield file_node.raw_bytes
779
779
780 response = Response(app_iter=stream_node())
780 response = Response(app_iter=stream_node())
781 response.content_disposition = disposition
781 response.content_disposition = disposition
782 response.content_type = file_node.mimetype
782 response.content_type = file_node.mimetype
783
783
784 charset = self._get_default_encoding(c)
784 charset = self._get_default_encoding(c)
785 if charset:
785 if charset:
786 response.charset = charset
786 response.charset = charset
787
787
788 return response
788 return response
789
789
790 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
790 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
791 def _cached_nodes():
791 def _cached_nodes():
792 log.debug('Generating cached nodelist for %s, %s, %s',
792 log.debug('Generating cached nodelist for %s, %s, %s',
793 repo_name, commit_id, f_path)
793 repo_name, commit_id, f_path)
794 try:
794 try:
795 _d, _f = ScmModel().get_nodes(
795 _d, _f = ScmModel().get_nodes(
796 repo_name, commit_id, f_path, flat=False)
796 repo_name, commit_id, f_path, flat=False)
797 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
797 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
798 log.exception(safe_str(e))
798 log.exception(safe_str(e))
799 h.flash(safe_str(h.escape(e)), category='error')
799 h.flash(safe_str(h.escape(e)), category='error')
800 raise HTTPFound(h.route_path(
800 raise HTTPFound(h.route_path(
801 'repo_files', repo_name=self.db_repo_name,
801 'repo_files', repo_name=self.db_repo_name,
802 commit_id='tip', f_path='/'))
802 commit_id='tip', f_path='/'))
803 return _d + _f
803 return _d + _f
804
804
805 cache_manager = self._get_tree_cache_manager(
805 cache_manager = self._get_tree_cache_manager(
806 caches.FILE_SEARCH_TREE_META)
806 caches.FILE_SEARCH_TREE_META)
807
807
808 cache_key = caches.compute_key_from_params(
808 cache_key = caches.compute_key_from_params(
809 repo_name, commit_id, f_path)
809 repo_name, commit_id, f_path)
810 return cache_manager.get(cache_key, createfunc=_cached_nodes)
810 return cache_manager.get(cache_key, createfunc=_cached_nodes)
811
811
812 @LoginRequired()
812 @LoginRequired()
813 @HasRepoPermissionAnyDecorator(
813 @HasRepoPermissionAnyDecorator(
814 'repository.read', 'repository.write', 'repository.admin')
814 'repository.read', 'repository.write', 'repository.admin')
815 @view_config(
815 @view_config(
816 route_name='repo_files_nodelist', request_method='GET',
816 route_name='repo_files_nodelist', request_method='GET',
817 renderer='json_ext', xhr=True)
817 renderer='json_ext', xhr=True)
818 def repo_nodelist(self):
818 def repo_nodelist(self):
819 self.load_default_context()
819 self.load_default_context()
820
820
821 commit_id, f_path = self._get_commit_and_path()
821 commit_id, f_path = self._get_commit_and_path()
822 commit = self._get_commit_or_redirect(commit_id)
822 commit = self._get_commit_or_redirect(commit_id)
823
823
824 metadata = self._get_nodelist_at_commit(
824 metadata = self._get_nodelist_at_commit(
825 self.db_repo_name, commit.raw_id, f_path)
825 self.db_repo_name, commit.raw_id, f_path)
826 return {'nodes': metadata}
826 return {'nodes': metadata}
827
827
828 def _create_references(
828 def _create_references(
829 self, branches_or_tags, symbolic_reference, f_path):
829 self, branches_or_tags, symbolic_reference, f_path):
830 items = []
830 items = []
831 for name, commit_id in branches_or_tags.items():
831 for name, commit_id in branches_or_tags.items():
832 sym_ref = symbolic_reference(commit_id, name, f_path)
832 sym_ref = symbolic_reference(commit_id, name, f_path)
833 items.append((sym_ref, name))
833 items.append((sym_ref, name))
834 return items
834 return items
835
835
836 def _symbolic_reference(self, commit_id, name, f_path):
836 def _symbolic_reference(self, commit_id, name, f_path):
837 return commit_id
837 return commit_id
838
838
839 def _symbolic_reference_svn(self, commit_id, name, f_path):
839 def _symbolic_reference_svn(self, commit_id, name, f_path):
840 new_f_path = vcspath.join(name, f_path)
840 new_f_path = vcspath.join(name, f_path)
841 return u'%s@%s' % (new_f_path, commit_id)
841 return u'%s@%s' % (new_f_path, commit_id)
842
842
843 def _get_node_history(self, commit_obj, f_path, commits=None):
843 def _get_node_history(self, commit_obj, f_path, commits=None):
844 """
844 """
845 get commit history for given node
845 get commit history for given node
846
846
847 :param commit_obj: commit to calculate history
847 :param commit_obj: commit to calculate history
848 :param f_path: path for node to calculate history for
848 :param f_path: path for node to calculate history for
849 :param commits: if passed don't calculate history and take
849 :param commits: if passed don't calculate history and take
850 commits defined in this list
850 commits defined in this list
851 """
851 """
852 _ = self.request.translate
852 _ = self.request.translate
853
853
854 # calculate history based on tip
854 # calculate history based on tip
855 tip = self.rhodecode_vcs_repo.get_commit()
855 tip = self.rhodecode_vcs_repo.get_commit()
856 if commits is None:
856 if commits is None:
857 pre_load = ["author", "branch"]
857 pre_load = ["author", "branch"]
858 try:
858 try:
859 commits = tip.get_file_history(f_path, pre_load=pre_load)
859 commits = tip.get_file_history(f_path, pre_load=pre_load)
860 except (NodeDoesNotExistError, CommitError):
860 except (NodeDoesNotExistError, CommitError):
861 # this node is not present at tip!
861 # this node is not present at tip!
862 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
862 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
863
863
864 history = []
864 history = []
865 commits_group = ([], _("Changesets"))
865 commits_group = ([], _("Changesets"))
866 for commit in commits:
866 for commit in commits:
867 branch = ' (%s)' % commit.branch if commit.branch else ''
867 branch = ' (%s)' % commit.branch if commit.branch else ''
868 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
868 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
869 commits_group[0].append((commit.raw_id, n_desc,))
869 commits_group[0].append((commit.raw_id, n_desc,))
870 history.append(commits_group)
870 history.append(commits_group)
871
871
872 symbolic_reference = self._symbolic_reference
872 symbolic_reference = self._symbolic_reference
873
873
874 if self.rhodecode_vcs_repo.alias == 'svn':
874 if self.rhodecode_vcs_repo.alias == 'svn':
875 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
875 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
876 f_path, self.rhodecode_vcs_repo)
876 f_path, self.rhodecode_vcs_repo)
877 if adjusted_f_path != f_path:
877 if adjusted_f_path != f_path:
878 log.debug(
878 log.debug(
879 'Recognized svn tag or branch in file "%s", using svn '
879 'Recognized svn tag or branch in file "%s", using svn '
880 'specific symbolic references', f_path)
880 'specific symbolic references', f_path)
881 f_path = adjusted_f_path
881 f_path = adjusted_f_path
882 symbolic_reference = self._symbolic_reference_svn
882 symbolic_reference = self._symbolic_reference_svn
883
883
884 branches = self._create_references(
884 branches = self._create_references(
885 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
885 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
886 branches_group = (branches, _("Branches"))
886 branches_group = (branches, _("Branches"))
887
887
888 tags = self._create_references(
888 tags = self._create_references(
889 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
889 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
890 tags_group = (tags, _("Tags"))
890 tags_group = (tags, _("Tags"))
891
891
892 history.append(branches_group)
892 history.append(branches_group)
893 history.append(tags_group)
893 history.append(tags_group)
894
894
895 return history, commits
895 return history, commits
896
896
897 @LoginRequired()
897 @LoginRequired()
898 @HasRepoPermissionAnyDecorator(
898 @HasRepoPermissionAnyDecorator(
899 'repository.read', 'repository.write', 'repository.admin')
899 'repository.read', 'repository.write', 'repository.admin')
900 @view_config(
900 @view_config(
901 route_name='repo_file_history', request_method='GET',
901 route_name='repo_file_history', request_method='GET',
902 renderer='json_ext')
902 renderer='json_ext')
903 def repo_file_history(self):
903 def repo_file_history(self):
904 self.load_default_context()
904 self.load_default_context()
905
905
906 commit_id, f_path = self._get_commit_and_path()
906 commit_id, f_path = self._get_commit_and_path()
907 commit = self._get_commit_or_redirect(commit_id)
907 commit = self._get_commit_or_redirect(commit_id)
908 file_node = self._get_filenode_or_redirect(commit, f_path)
908 file_node = self._get_filenode_or_redirect(commit, f_path)
909
909
910 if file_node.is_file():
910 if file_node.is_file():
911 file_history, _hist = self._get_node_history(commit, f_path)
911 file_history, _hist = self._get_node_history(commit, f_path)
912
912
913 res = []
913 res = []
914 for obj in file_history:
914 for obj in file_history:
915 res.append({
915 res.append({
916 'text': obj[1],
916 'text': obj[1],
917 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
917 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
918 })
918 })
919
919
920 data = {
920 data = {
921 'more': False,
921 'more': False,
922 'results': res
922 'results': res
923 }
923 }
924 return data
924 return data
925
925
926 log.warning('Cannot fetch history for directory')
926 log.warning('Cannot fetch history for directory')
927 raise HTTPBadRequest()
927 raise HTTPBadRequest()
928
928
929 @LoginRequired()
929 @LoginRequired()
930 @HasRepoPermissionAnyDecorator(
930 @HasRepoPermissionAnyDecorator(
931 'repository.read', 'repository.write', 'repository.admin')
931 'repository.read', 'repository.write', 'repository.admin')
932 @view_config(
932 @view_config(
933 route_name='repo_file_authors', request_method='GET',
933 route_name='repo_file_authors', request_method='GET',
934 renderer='rhodecode:templates/files/file_authors_box.mako')
934 renderer='rhodecode:templates/files/file_authors_box.mako')
935 def repo_file_authors(self):
935 def repo_file_authors(self):
936 c = self.load_default_context()
936 c = self.load_default_context()
937
937
938 commit_id, f_path = self._get_commit_and_path()
938 commit_id, f_path = self._get_commit_and_path()
939 commit = self._get_commit_or_redirect(commit_id)
939 commit = self._get_commit_or_redirect(commit_id)
940 file_node = self._get_filenode_or_redirect(commit, f_path)
940 file_node = self._get_filenode_or_redirect(commit, f_path)
941
941
942 if not file_node.is_file():
942 if not file_node.is_file():
943 raise HTTPBadRequest()
943 raise HTTPBadRequest()
944
944
945 c.file_last_commit = file_node.last_commit
945 c.file_last_commit = file_node.last_commit
946 if self.request.GET.get('annotate') == '1':
946 if self.request.GET.get('annotate') == '1':
947 # use _hist from annotation if annotation mode is on
947 # use _hist from annotation if annotation mode is on
948 commit_ids = set(x[1] for x in file_node.annotate)
948 commit_ids = set(x[1] for x in file_node.annotate)
949 _hist = (
949 _hist = (
950 self.rhodecode_vcs_repo.get_commit(commit_id)
950 self.rhodecode_vcs_repo.get_commit(commit_id)
951 for commit_id in commit_ids)
951 for commit_id in commit_ids)
952 else:
952 else:
953 _f_history, _hist = self._get_node_history(commit, f_path)
953 _f_history, _hist = self._get_node_history(commit, f_path)
954 c.file_author = False
954 c.file_author = False
955
955
956 unique = collections.OrderedDict()
956 unique = collections.OrderedDict()
957 for commit in _hist:
957 for commit in _hist:
958 author = commit.author
958 author = commit.author
959 if author not in unique:
959 if author not in unique:
960 unique[commit.author] = [
960 unique[commit.author] = [
961 h.email(author),
961 h.email(author),
962 h.person(author, 'username_or_name_or_email'),
962 h.person(author, 'username_or_name_or_email'),
963 1 # counter
963 1 # counter
964 ]
964 ]
965
965
966 else:
966 else:
967 # increase counter
967 # increase counter
968 unique[commit.author][2] += 1
968 unique[commit.author][2] += 1
969
969
970 c.authors = [val for val in unique.values()]
970 c.authors = [val for val in unique.values()]
971
971
972 return self._get_template_context(c)
972 return self._get_template_context(c)
973
973
974 @LoginRequired()
974 @LoginRequired()
975 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
975 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
976 @view_config(
976 @view_config(
977 route_name='repo_files_remove_file', request_method='GET',
977 route_name='repo_files_remove_file', request_method='GET',
978 renderer='rhodecode:templates/files/files_delete.mako')
978 renderer='rhodecode:templates/files/files_delete.mako')
979 def repo_files_remove_file(self):
979 def repo_files_remove_file(self):
980 _ = self.request.translate
980 _ = self.request.translate
981 c = self.load_default_context()
981 c = self.load_default_context()
982 commit_id, f_path = self._get_commit_and_path()
982 commit_id, f_path = self._get_commit_and_path()
983
983
984 self._ensure_not_locked()
984 self._ensure_not_locked()
985
985
986 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
986 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
987 h.flash(_('You can only delete files with commit '
987 h.flash(_('You can only delete files with commit '
988 'being a valid branch '), category='warning')
988 'being a valid branch '), category='warning')
989 raise HTTPFound(
989 raise HTTPFound(
990 h.route_path('repo_files',
990 h.route_path('repo_files',
991 repo_name=self.db_repo_name, commit_id='tip',
991 repo_name=self.db_repo_name, commit_id='tip',
992 f_path=f_path))
992 f_path=f_path))
993
993
994 c.commit = self._get_commit_or_redirect(commit_id)
994 c.commit = self._get_commit_or_redirect(commit_id)
995 c.file = self._get_filenode_or_redirect(c.commit, f_path)
995 c.file = self._get_filenode_or_redirect(c.commit, f_path)
996
996
997 c.default_message = _(
997 c.default_message = _(
998 'Deleted file {} via RhodeCode Enterprise').format(f_path)
998 'Deleted file {} via RhodeCode Enterprise').format(f_path)
999 c.f_path = f_path
999 c.f_path = f_path
1000
1000
1001 return self._get_template_context(c)
1001 return self._get_template_context(c)
1002
1002
1003 @LoginRequired()
1003 @LoginRequired()
1004 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1004 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1005 @CSRFRequired()
1005 @CSRFRequired()
1006 @view_config(
1006 @view_config(
1007 route_name='repo_files_delete_file', request_method='POST',
1007 route_name='repo_files_delete_file', request_method='POST',
1008 renderer=None)
1008 renderer=None)
1009 def repo_files_delete_file(self):
1009 def repo_files_delete_file(self):
1010 _ = self.request.translate
1010 _ = self.request.translate
1011
1011
1012 c = self.load_default_context()
1012 c = self.load_default_context()
1013 commit_id, f_path = self._get_commit_and_path()
1013 commit_id, f_path = self._get_commit_and_path()
1014
1014
1015 self._ensure_not_locked()
1015 self._ensure_not_locked()
1016
1016
1017 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1017 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1018 h.flash(_('You can only delete files with commit '
1018 h.flash(_('You can only delete files with commit '
1019 'being a valid branch '), category='warning')
1019 'being a valid branch '), category='warning')
1020 raise HTTPFound(
1020 raise HTTPFound(
1021 h.route_path('repo_files',
1021 h.route_path('repo_files',
1022 repo_name=self.db_repo_name, commit_id='tip',
1022 repo_name=self.db_repo_name, commit_id='tip',
1023 f_path=f_path))
1023 f_path=f_path))
1024
1024
1025 c.commit = self._get_commit_or_redirect(commit_id)
1025 c.commit = self._get_commit_or_redirect(commit_id)
1026 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1026 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1027
1027
1028 c.default_message = _(
1028 c.default_message = _(
1029 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1029 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1030 c.f_path = f_path
1030 c.f_path = f_path
1031 node_path = f_path
1031 node_path = f_path
1032 author = self._rhodecode_db_user.full_contact
1032 author = self._rhodecode_db_user.full_contact
1033 message = self.request.POST.get('message') or c.default_message
1033 message = self.request.POST.get('message') or c.default_message
1034 try:
1034 try:
1035 nodes = {
1035 nodes = {
1036 node_path: {
1036 node_path: {
1037 'content': ''
1037 'content': ''
1038 }
1038 }
1039 }
1039 }
1040 ScmModel().delete_nodes(
1040 ScmModel().delete_nodes(
1041 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1041 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1042 message=message,
1042 message=message,
1043 nodes=nodes,
1043 nodes=nodes,
1044 parent_commit=c.commit,
1044 parent_commit=c.commit,
1045 author=author,
1045 author=author,
1046 )
1046 )
1047
1047
1048 h.flash(
1048 h.flash(
1049 _('Successfully deleted file `{}`').format(
1049 _('Successfully deleted file `{}`').format(
1050 h.escape(f_path)), category='success')
1050 h.escape(f_path)), category='success')
1051 except Exception:
1051 except Exception:
1052 log.exception('Error during commit operation')
1052 log.exception('Error during commit operation')
1053 h.flash(_('Error occurred during commit'), category='error')
1053 h.flash(_('Error occurred during commit'), category='error')
1054 raise HTTPFound(
1054 raise HTTPFound(
1055 h.route_path('repo_commit', repo_name=self.db_repo_name,
1055 h.route_path('repo_commit', repo_name=self.db_repo_name,
1056 commit_id='tip'))
1056 commit_id='tip'))
1057
1057
1058 @LoginRequired()
1058 @LoginRequired()
1059 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1059 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1060 @view_config(
1060 @view_config(
1061 route_name='repo_files_edit_file', request_method='GET',
1061 route_name='repo_files_edit_file', request_method='GET',
1062 renderer='rhodecode:templates/files/files_edit.mako')
1062 renderer='rhodecode:templates/files/files_edit.mako')
1063 def repo_files_edit_file(self):
1063 def repo_files_edit_file(self):
1064 _ = self.request.translate
1064 _ = self.request.translate
1065 c = self.load_default_context()
1065 c = self.load_default_context()
1066 commit_id, f_path = self._get_commit_and_path()
1066 commit_id, f_path = self._get_commit_and_path()
1067
1067
1068 self._ensure_not_locked()
1068 self._ensure_not_locked()
1069
1069
1070 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1070 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1071 h.flash(_('You can only edit files with commit '
1071 h.flash(_('You can only edit files with commit '
1072 'being a valid branch '), category='warning')
1072 'being a valid branch '), category='warning')
1073 raise HTTPFound(
1073 raise HTTPFound(
1074 h.route_path('repo_files',
1074 h.route_path('repo_files',
1075 repo_name=self.db_repo_name, commit_id='tip',
1075 repo_name=self.db_repo_name, commit_id='tip',
1076 f_path=f_path))
1076 f_path=f_path))
1077
1077
1078 c.commit = self._get_commit_or_redirect(commit_id)
1078 c.commit = self._get_commit_or_redirect(commit_id)
1079 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1079 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1080
1080
1081 if c.file.is_binary:
1081 if c.file.is_binary:
1082 files_url = h.route_path(
1082 files_url = h.route_path(
1083 'repo_files',
1083 'repo_files',
1084 repo_name=self.db_repo_name,
1084 repo_name=self.db_repo_name,
1085 commit_id=c.commit.raw_id, f_path=f_path)
1085 commit_id=c.commit.raw_id, f_path=f_path)
1086 raise HTTPFound(files_url)
1086 raise HTTPFound(files_url)
1087
1087
1088 c.default_message = _(
1088 c.default_message = _(
1089 'Edited file {} via RhodeCode Enterprise').format(f_path)
1089 'Edited file {} via RhodeCode Enterprise').format(f_path)
1090 c.f_path = f_path
1090 c.f_path = f_path
1091
1091
1092 return self._get_template_context(c)
1092 return self._get_template_context(c)
1093
1093
1094 @LoginRequired()
1094 @LoginRequired()
1095 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1095 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1096 @CSRFRequired()
1096 @CSRFRequired()
1097 @view_config(
1097 @view_config(
1098 route_name='repo_files_update_file', request_method='POST',
1098 route_name='repo_files_update_file', request_method='POST',
1099 renderer=None)
1099 renderer=None)
1100 def repo_files_update_file(self):
1100 def repo_files_update_file(self):
1101 _ = self.request.translate
1101 _ = self.request.translate
1102 c = self.load_default_context()
1102 c = self.load_default_context()
1103 commit_id, f_path = self._get_commit_and_path()
1103 commit_id, f_path = self._get_commit_and_path()
1104
1104
1105 self._ensure_not_locked()
1105 self._ensure_not_locked()
1106
1106
1107 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1107 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1108 h.flash(_('You can only edit files with commit '
1108 h.flash(_('You can only edit files with commit '
1109 'being a valid branch '), category='warning')
1109 'being a valid branch '), category='warning')
1110 raise HTTPFound(
1110 raise HTTPFound(
1111 h.route_path('repo_files',
1111 h.route_path('repo_files',
1112 repo_name=self.db_repo_name, commit_id='tip',
1112 repo_name=self.db_repo_name, commit_id='tip',
1113 f_path=f_path))
1113 f_path=f_path))
1114
1114
1115 c.commit = self._get_commit_or_redirect(commit_id)
1115 c.commit = self._get_commit_or_redirect(commit_id)
1116 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1116 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1117
1117
1118 if c.file.is_binary:
1118 if c.file.is_binary:
1119 raise HTTPFound(
1119 raise HTTPFound(
1120 h.route_path('repo_files',
1120 h.route_path('repo_files',
1121 repo_name=self.db_repo_name,
1121 repo_name=self.db_repo_name,
1122 commit_id=c.commit.raw_id,
1122 commit_id=c.commit.raw_id,
1123 f_path=f_path))
1123 f_path=f_path))
1124
1124
1125 c.default_message = _(
1125 c.default_message = _(
1126 'Edited file {} via RhodeCode Enterprise').format(f_path)
1126 'Edited file {} via RhodeCode Enterprise').format(f_path)
1127 c.f_path = f_path
1127 c.f_path = f_path
1128 old_content = c.file.content
1128 old_content = c.file.content
1129 sl = old_content.splitlines(1)
1129 sl = old_content.splitlines(1)
1130 first_line = sl[0] if sl else ''
1130 first_line = sl[0] if sl else ''
1131
1131
1132 r_post = self.request.POST
1132 r_post = self.request.POST
1133 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1133 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1134 mode = detect_mode(first_line, 0)
1134 mode = detect_mode(first_line, 0)
1135 content = convert_line_endings(r_post.get('content', ''), mode)
1135 content = convert_line_endings(r_post.get('content', ''), mode)
1136
1136
1137 message = r_post.get('message') or c.default_message
1137 message = r_post.get('message') or c.default_message
1138 org_f_path = c.file.unicode_path
1138 org_f_path = c.file.unicode_path
1139 filename = r_post['filename']
1139 filename = r_post['filename']
1140 org_filename = c.file.name
1140 org_filename = c.file.name
1141
1141
1142 if content == old_content and filename == org_filename:
1142 if content == old_content and filename == org_filename:
1143 h.flash(_('No changes'), category='warning')
1143 h.flash(_('No changes'), category='warning')
1144 raise HTTPFound(
1144 raise HTTPFound(
1145 h.route_path('repo_commit', repo_name=self.db_repo_name,
1145 h.route_path('repo_commit', repo_name=self.db_repo_name,
1146 commit_id='tip'))
1146 commit_id='tip'))
1147 try:
1147 try:
1148 mapping = {
1148 mapping = {
1149 org_f_path: {
1149 org_f_path: {
1150 'org_filename': org_f_path,
1150 'org_filename': org_f_path,
1151 'filename': os.path.join(c.file.dir_path, filename),
1151 'filename': os.path.join(c.file.dir_path, filename),
1152 'content': content,
1152 'content': content,
1153 'lexer': '',
1153 'lexer': '',
1154 'op': 'mod',
1154 'op': 'mod',
1155 }
1155 }
1156 }
1156 }
1157
1157
1158 ScmModel().update_nodes(
1158 ScmModel().update_nodes(
1159 user=self._rhodecode_db_user.user_id,
1159 user=self._rhodecode_db_user.user_id,
1160 repo=self.db_repo,
1160 repo=self.db_repo,
1161 message=message,
1161 message=message,
1162 nodes=mapping,
1162 nodes=mapping,
1163 parent_commit=c.commit,
1163 parent_commit=c.commit,
1164 )
1164 )
1165
1165
1166 h.flash(
1166 h.flash(
1167 _('Successfully committed changes to file `{}`').format(
1167 _('Successfully committed changes to file `{}`').format(
1168 h.escape(f_path)), category='success')
1168 h.escape(f_path)), category='success')
1169 except Exception:
1169 except Exception:
1170 log.exception('Error occurred during commit')
1170 log.exception('Error occurred during commit')
1171 h.flash(_('Error occurred during commit'), category='error')
1171 h.flash(_('Error occurred during commit'), category='error')
1172 raise HTTPFound(
1172 raise HTTPFound(
1173 h.route_path('repo_commit', repo_name=self.db_repo_name,
1173 h.route_path('repo_commit', repo_name=self.db_repo_name,
1174 commit_id='tip'))
1174 commit_id='tip'))
1175
1175
1176 @LoginRequired()
1176 @LoginRequired()
1177 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1177 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1178 @view_config(
1178 @view_config(
1179 route_name='repo_files_add_file', request_method='GET',
1179 route_name='repo_files_add_file', request_method='GET',
1180 renderer='rhodecode:templates/files/files_add.mako')
1180 renderer='rhodecode:templates/files/files_add.mako')
1181 def repo_files_add_file(self):
1181 def repo_files_add_file(self):
1182 _ = self.request.translate
1182 _ = self.request.translate
1183 c = self.load_default_context()
1183 c = self.load_default_context()
1184 commit_id, f_path = self._get_commit_and_path()
1184 commit_id, f_path = self._get_commit_and_path()
1185
1185
1186 self._ensure_not_locked()
1186 self._ensure_not_locked()
1187
1187
1188 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1188 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1189 if c.commit is None:
1189 if c.commit is None:
1190 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1190 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1191 c.default_message = (_('Added file via RhodeCode Enterprise'))
1191 c.default_message = (_('Added file via RhodeCode Enterprise'))
1192 c.f_path = f_path.lstrip('/') # ensure not relative path
1192 c.f_path = f_path.lstrip('/') # ensure not relative path
1193
1193
1194 return self._get_template_context(c)
1194 return self._get_template_context(c)
1195
1195
1196 @LoginRequired()
1196 @LoginRequired()
1197 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1197 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1198 @CSRFRequired()
1198 @CSRFRequired()
1199 @view_config(
1199 @view_config(
1200 route_name='repo_files_create_file', request_method='POST',
1200 route_name='repo_files_create_file', request_method='POST',
1201 renderer=None)
1201 renderer=None)
1202 def repo_files_create_file(self):
1202 def repo_files_create_file(self):
1203 _ = self.request.translate
1203 _ = self.request.translate
1204 c = self.load_default_context()
1204 c = self.load_default_context()
1205 commit_id, f_path = self._get_commit_and_path()
1205 commit_id, f_path = self._get_commit_and_path()
1206
1206
1207 self._ensure_not_locked()
1207 self._ensure_not_locked()
1208
1208
1209 r_post = self.request.POST
1209 r_post = self.request.POST
1210
1210
1211 c.commit = self._get_commit_or_redirect(
1211 c.commit = self._get_commit_or_redirect(
1212 commit_id, redirect_after=False)
1212 commit_id, redirect_after=False)
1213 if c.commit is None:
1213 if c.commit is None:
1214 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1214 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1215 c.default_message = (_('Added file via RhodeCode Enterprise'))
1215 c.default_message = (_('Added file via RhodeCode Enterprise'))
1216 c.f_path = f_path
1216 c.f_path = f_path
1217 unix_mode = 0
1217 unix_mode = 0
1218 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1218 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1219
1219
1220 message = r_post.get('message') or c.default_message
1220 message = r_post.get('message') or c.default_message
1221 filename = r_post.get('filename')
1221 filename = r_post.get('filename')
1222 location = r_post.get('location', '') # dir location
1222 location = r_post.get('location', '') # dir location
1223 file_obj = r_post.get('upload_file', None)
1223 file_obj = r_post.get('upload_file', None)
1224
1224
1225 if file_obj is not None and hasattr(file_obj, 'filename'):
1225 if file_obj is not None and hasattr(file_obj, 'filename'):
1226 filename = r_post.get('filename_upload')
1226 filename = r_post.get('filename_upload')
1227 content = file_obj.file
1227 content = file_obj.file
1228
1228
1229 if hasattr(content, 'file'):
1229 if hasattr(content, 'file'):
1230 # non posix systems store real file under file attr
1230 # non posix systems store real file under file attr
1231 content = content.file
1231 content = content.file
1232
1232
1233 if self.rhodecode_vcs_repo.is_empty:
1233 if self.rhodecode_vcs_repo.is_empty:
1234 default_redirect_url = h.route_path(
1234 default_redirect_url = h.route_path(
1235 'repo_summary', repo_name=self.db_repo_name)
1235 'repo_summary', repo_name=self.db_repo_name)
1236 else:
1236 else:
1237 default_redirect_url = h.route_path(
1237 default_redirect_url = h.route_path(
1238 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1238 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1239
1239
1240 # If there's no commit, redirect to repo summary
1240 # If there's no commit, redirect to repo summary
1241 if type(c.commit) is EmptyCommit:
1241 if type(c.commit) is EmptyCommit:
1242 redirect_url = h.route_path(
1242 redirect_url = h.route_path(
1243 'repo_summary', repo_name=self.db_repo_name)
1243 'repo_summary', repo_name=self.db_repo_name)
1244 else:
1244 else:
1245 redirect_url = default_redirect_url
1245 redirect_url = default_redirect_url
1246
1246
1247 if not filename:
1247 if not filename:
1248 h.flash(_('No filename'), category='warning')
1248 h.flash(_('No filename'), category='warning')
1249 raise HTTPFound(redirect_url)
1249 raise HTTPFound(redirect_url)
1250
1250
1251 # extract the location from filename,
1251 # extract the location from filename,
1252 # allows using foo/bar.txt syntax to create subdirectories
1252 # allows using foo/bar.txt syntax to create subdirectories
1253 subdir_loc = filename.rsplit('/', 1)
1253 subdir_loc = filename.rsplit('/', 1)
1254 if len(subdir_loc) == 2:
1254 if len(subdir_loc) == 2:
1255 location = os.path.join(location, subdir_loc[0])
1255 location = os.path.join(location, subdir_loc[0])
1256
1256
1257 # strip all crap out of file, just leave the basename
1257 # strip all crap out of file, just leave the basename
1258 filename = os.path.basename(filename)
1258 filename = os.path.basename(filename)
1259 node_path = os.path.join(location, filename)
1259 node_path = os.path.join(location, filename)
1260 author = self._rhodecode_db_user.full_contact
1260 author = self._rhodecode_db_user.full_contact
1261
1261
1262 try:
1262 try:
1263 nodes = {
1263 nodes = {
1264 node_path: {
1264 node_path: {
1265 'content': content
1265 'content': content
1266 }
1266 }
1267 }
1267 }
1268 ScmModel().create_nodes(
1268 ScmModel().create_nodes(
1269 user=self._rhodecode_db_user.user_id,
1269 user=self._rhodecode_db_user.user_id,
1270 repo=self.db_repo,
1270 repo=self.db_repo,
1271 message=message,
1271 message=message,
1272 nodes=nodes,
1272 nodes=nodes,
1273 parent_commit=c.commit,
1273 parent_commit=c.commit,
1274 author=author,
1274 author=author,
1275 )
1275 )
1276
1276
1277 h.flash(
1277 h.flash(
1278 _('Successfully committed new file `{}`').format(
1278 _('Successfully committed new file `{}`').format(
1279 h.escape(node_path)), category='success')
1279 h.escape(node_path)), category='success')
1280 except NonRelativePathError:
1280 except NonRelativePathError:
1281 log.exception('Non Relative path found')
1281 log.exception('Non Relative path found')
1282 h.flash(_(
1282 h.flash(_(
1283 'The location specified must be a relative path and must not '
1283 'The location specified must be a relative path and must not '
1284 'contain .. in the path'), category='warning')
1284 'contain .. in the path'), category='warning')
1285 raise HTTPFound(default_redirect_url)
1285 raise HTTPFound(default_redirect_url)
1286 except (NodeError, NodeAlreadyExistsError) as e:
1286 except (NodeError, NodeAlreadyExistsError) as e:
1287 h.flash(_(h.escape(e)), category='error')
1287 h.flash(_(h.escape(e)), category='error')
1288 except Exception:
1288 except Exception:
1289 log.exception('Error occurred during commit')
1289 log.exception('Error occurred during commit')
1290 h.flash(_('Error occurred during commit'), category='error')
1290 h.flash(_('Error occurred during commit'), category='error')
1291
1291
1292 raise HTTPFound(default_redirect_url)
1292 raise HTTPFound(default_redirect_url)
@@ -1,1242 +1,1242 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
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, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, 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
63
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'rhodecode:templates/data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176 self.load_default_context()
176 self.load_default_context()
177
177
178 # additional filters
178 # additional filters
179 req_get = self.request.GET
179 req_get = self.request.GET
180 source = str2bool(req_get.get('source'))
180 source = str2bool(req_get.get('source'))
181 closed = str2bool(req_get.get('closed'))
181 closed = str2bool(req_get.get('closed'))
182 my = str2bool(req_get.get('my'))
182 my = str2bool(req_get.get('my'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185
185
186 filter_type = 'awaiting_review' if awaiting_review \
186 filter_type = 'awaiting_review' if awaiting_review \
187 else 'awaiting_my_review' if awaiting_my_review \
187 else 'awaiting_my_review' if awaiting_my_review \
188 else None
188 else None
189
189
190 opened_by = None
190 opened_by = None
191 if my:
191 if my:
192 opened_by = [self._rhodecode_user.user_id]
192 opened_by = [self._rhodecode_user.user_id]
193
193
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 if closed:
195 if closed:
196 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
197
197
198 data = self._get_pull_requests_list(
198 data = self._get_pull_requests_list(
199 repo_name=self.db_repo_name, source=source,
199 repo_name=self.db_repo_name, source=source,
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201
201
202 return data
202 return data
203
203
204 def _get_diffset(self, source_repo_name, source_repo,
204 def _get_diffset(self, source_repo_name, source_repo,
205 source_ref_id, target_ref_id,
205 source_ref_id, target_ref_id,
206 target_commit, source_commit, diff_limit, fulldiff,
206 target_commit, source_commit, diff_limit, fulldiff,
207 file_limit, display_inline_comments):
207 file_limit, display_inline_comments):
208
208
209 vcs_diff = PullRequestModel().get_diff(
209 vcs_diff = PullRequestModel().get_diff(
210 source_repo, source_ref_id, target_ref_id)
210 source_repo, source_ref_id, target_ref_id)
211
211
212 diff_processor = diffs.DiffProcessor(
212 diff_processor = diffs.DiffProcessor(
213 vcs_diff, format='newdiff', diff_limit=diff_limit,
213 vcs_diff, format='newdiff', diff_limit=diff_limit,
214 file_limit=file_limit, show_full_diff=fulldiff)
214 file_limit=file_limit, show_full_diff=fulldiff)
215
215
216 _parsed = diff_processor.prepare()
216 _parsed = diff_processor.prepare()
217
217
218 def _node_getter(commit):
218 def _node_getter(commit):
219 def get_node(fname):
219 def get_node(fname):
220 try:
220 try:
221 return commit.get_node(fname)
221 return commit.get_node(fname)
222 except NodeDoesNotExistError:
222 except NodeDoesNotExistError:
223 return None
223 return None
224
224
225 return get_node
225 return get_node
226
226
227 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
228 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
229 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
230 source_node_getter=_node_getter(target_commit),
230 source_node_getter=_node_getter(target_commit),
231 target_node_getter=_node_getter(source_commit),
231 target_node_getter=_node_getter(source_commit),
232 comments=display_inline_comments
232 comments=display_inline_comments
233 )
233 )
234 diffset = diffset.render_patchset(
234 diffset = self.path_filter.render_patchset_filtered(
235 _parsed, target_commit.raw_id, source_commit.raw_id)
235 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
236
236
237 return diffset
237 return diffset
238
238
239 @LoginRequired()
239 @LoginRequired()
240 @HasRepoPermissionAnyDecorator(
240 @HasRepoPermissionAnyDecorator(
241 'repository.read', 'repository.write', 'repository.admin')
241 'repository.read', 'repository.write', 'repository.admin')
242 @view_config(
242 @view_config(
243 route_name='pullrequest_show', request_method='GET',
243 route_name='pullrequest_show', request_method='GET',
244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
245 def pull_request_show(self):
245 def pull_request_show(self):
246 pull_request_id = self.request.matchdict['pull_request_id']
246 pull_request_id = self.request.matchdict['pull_request_id']
247
247
248 c = self.load_default_context()
248 c = self.load_default_context()
249
249
250 version = self.request.GET.get('version')
250 version = self.request.GET.get('version')
251 from_version = self.request.GET.get('from_version') or version
251 from_version = self.request.GET.get('from_version') or version
252 merge_checks = self.request.GET.get('merge_checks')
252 merge_checks = self.request.GET.get('merge_checks')
253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
254
254
255 (pull_request_latest,
255 (pull_request_latest,
256 pull_request_at_ver,
256 pull_request_at_ver,
257 pull_request_display_obj,
257 pull_request_display_obj,
258 at_version) = PullRequestModel().get_pr_version(
258 at_version) = PullRequestModel().get_pr_version(
259 pull_request_id, version=version)
259 pull_request_id, version=version)
260 pr_closed = pull_request_latest.is_closed()
260 pr_closed = pull_request_latest.is_closed()
261
261
262 if pr_closed and (version or from_version):
262 if pr_closed and (version or from_version):
263 # not allow to browse versions
263 # not allow to browse versions
264 raise HTTPFound(h.route_path(
264 raise HTTPFound(h.route_path(
265 'pullrequest_show', repo_name=self.db_repo_name,
265 'pullrequest_show', repo_name=self.db_repo_name,
266 pull_request_id=pull_request_id))
266 pull_request_id=pull_request_id))
267
267
268 versions = pull_request_display_obj.versions()
268 versions = pull_request_display_obj.versions()
269
269
270 c.at_version = at_version
270 c.at_version = at_version
271 c.at_version_num = (at_version
271 c.at_version_num = (at_version
272 if at_version and at_version != 'latest'
272 if at_version and at_version != 'latest'
273 else None)
273 else None)
274 c.at_version_pos = ChangesetComment.get_index_from_version(
274 c.at_version_pos = ChangesetComment.get_index_from_version(
275 c.at_version_num, versions)
275 c.at_version_num, versions)
276
276
277 (prev_pull_request_latest,
277 (prev_pull_request_latest,
278 prev_pull_request_at_ver,
278 prev_pull_request_at_ver,
279 prev_pull_request_display_obj,
279 prev_pull_request_display_obj,
280 prev_at_version) = PullRequestModel().get_pr_version(
280 prev_at_version) = PullRequestModel().get_pr_version(
281 pull_request_id, version=from_version)
281 pull_request_id, version=from_version)
282
282
283 c.from_version = prev_at_version
283 c.from_version = prev_at_version
284 c.from_version_num = (prev_at_version
284 c.from_version_num = (prev_at_version
285 if prev_at_version and prev_at_version != 'latest'
285 if prev_at_version and prev_at_version != 'latest'
286 else None)
286 else None)
287 c.from_version_pos = ChangesetComment.get_index_from_version(
287 c.from_version_pos = ChangesetComment.get_index_from_version(
288 c.from_version_num, versions)
288 c.from_version_num, versions)
289
289
290 # define if we're in COMPARE mode or VIEW at version mode
290 # define if we're in COMPARE mode or VIEW at version mode
291 compare = at_version != prev_at_version
291 compare = at_version != prev_at_version
292
292
293 # pull_requests repo_name we opened it against
293 # pull_requests repo_name we opened it against
294 # ie. target_repo must match
294 # ie. target_repo must match
295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
296 raise HTTPNotFound()
296 raise HTTPNotFound()
297
297
298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
299 pull_request_at_ver)
299 pull_request_at_ver)
300
300
301 c.pull_request = pull_request_display_obj
301 c.pull_request = pull_request_display_obj
302 c.pull_request_latest = pull_request_latest
302 c.pull_request_latest = pull_request_latest
303
303
304 if compare or (at_version and not at_version == 'latest'):
304 if compare or (at_version and not at_version == 'latest'):
305 c.allowed_to_change_status = False
305 c.allowed_to_change_status = False
306 c.allowed_to_update = False
306 c.allowed_to_update = False
307 c.allowed_to_merge = False
307 c.allowed_to_merge = False
308 c.allowed_to_delete = False
308 c.allowed_to_delete = False
309 c.allowed_to_comment = False
309 c.allowed_to_comment = False
310 c.allowed_to_close = False
310 c.allowed_to_close = False
311 else:
311 else:
312 can_change_status = PullRequestModel().check_user_change_status(
312 can_change_status = PullRequestModel().check_user_change_status(
313 pull_request_at_ver, self._rhodecode_user)
313 pull_request_at_ver, self._rhodecode_user)
314 c.allowed_to_change_status = can_change_status and not pr_closed
314 c.allowed_to_change_status = can_change_status and not pr_closed
315
315
316 c.allowed_to_update = PullRequestModel().check_user_update(
316 c.allowed_to_update = PullRequestModel().check_user_update(
317 pull_request_latest, self._rhodecode_user) and not pr_closed
317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
319 pull_request_latest, self._rhodecode_user) and not pr_closed
319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
321 pull_request_latest, self._rhodecode_user) and not pr_closed
321 pull_request_latest, self._rhodecode_user) and not pr_closed
322 c.allowed_to_comment = not pr_closed
322 c.allowed_to_comment = not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
324
324
325 c.forbid_adding_reviewers = False
325 c.forbid_adding_reviewers = False
326 c.forbid_author_to_review = False
326 c.forbid_author_to_review = False
327 c.forbid_commit_author_to_review = False
327 c.forbid_commit_author_to_review = False
328
328
329 if pull_request_latest.reviewer_data and \
329 if pull_request_latest.reviewer_data and \
330 'rules' in pull_request_latest.reviewer_data:
330 'rules' in pull_request_latest.reviewer_data:
331 rules = pull_request_latest.reviewer_data['rules'] or {}
331 rules = pull_request_latest.reviewer_data['rules'] or {}
332 try:
332 try:
333 c.forbid_adding_reviewers = rules.get(
333 c.forbid_adding_reviewers = rules.get(
334 'forbid_adding_reviewers')
334 'forbid_adding_reviewers')
335 c.forbid_author_to_review = rules.get(
335 c.forbid_author_to_review = rules.get(
336 'forbid_author_to_review')
336 'forbid_author_to_review')
337 c.forbid_commit_author_to_review = rules.get(
337 c.forbid_commit_author_to_review = rules.get(
338 'forbid_commit_author_to_review')
338 'forbid_commit_author_to_review')
339 except Exception:
339 except Exception:
340 pass
340 pass
341
341
342 # check merge capabilities
342 # check merge capabilities
343 _merge_check = MergeCheck.validate(
343 _merge_check = MergeCheck.validate(
344 pull_request_latest, user=self._rhodecode_user,
344 pull_request_latest, user=self._rhodecode_user,
345 translator=self.request.translate)
345 translator=self.request.translate)
346 c.pr_merge_errors = _merge_check.error_details
346 c.pr_merge_errors = _merge_check.error_details
347 c.pr_merge_possible = not _merge_check.failed
347 c.pr_merge_possible = not _merge_check.failed
348 c.pr_merge_message = _merge_check.merge_msg
348 c.pr_merge_message = _merge_check.merge_msg
349
349
350 c.pr_merge_info = MergeCheck.get_merge_conditions(
350 c.pr_merge_info = MergeCheck.get_merge_conditions(
351 pull_request_latest, translator=self.request.translate)
351 pull_request_latest, translator=self.request.translate)
352
352
353 c.pull_request_review_status = _merge_check.review_status
353 c.pull_request_review_status = _merge_check.review_status
354 if merge_checks:
354 if merge_checks:
355 self.request.override_renderer = \
355 self.request.override_renderer = \
356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
357 return self._get_template_context(c)
357 return self._get_template_context(c)
358
358
359 comments_model = CommentsModel()
359 comments_model = CommentsModel()
360
360
361 # reviewers and statuses
361 # reviewers and statuses
362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
364
364
365 # GENERAL COMMENTS with versions #
365 # GENERAL COMMENTS with versions #
366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
367 q = q.order_by(ChangesetComment.comment_id.asc())
367 q = q.order_by(ChangesetComment.comment_id.asc())
368 general_comments = q
368 general_comments = q
369
369
370 # pick comments we want to render at current version
370 # pick comments we want to render at current version
371 c.comment_versions = comments_model.aggregate_comments(
371 c.comment_versions = comments_model.aggregate_comments(
372 general_comments, versions, c.at_version_num)
372 general_comments, versions, c.at_version_num)
373 c.comments = c.comment_versions[c.at_version_num]['until']
373 c.comments = c.comment_versions[c.at_version_num]['until']
374
374
375 # INLINE COMMENTS with versions #
375 # INLINE COMMENTS with versions #
376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
377 q = q.order_by(ChangesetComment.comment_id.asc())
377 q = q.order_by(ChangesetComment.comment_id.asc())
378 inline_comments = q
378 inline_comments = q
379
379
380 c.inline_versions = comments_model.aggregate_comments(
380 c.inline_versions = comments_model.aggregate_comments(
381 inline_comments, versions, c.at_version_num, inline=True)
381 inline_comments, versions, c.at_version_num, inline=True)
382
382
383 # inject latest version
383 # inject latest version
384 latest_ver = PullRequest.get_pr_display_object(
384 latest_ver = PullRequest.get_pr_display_object(
385 pull_request_latest, pull_request_latest)
385 pull_request_latest, pull_request_latest)
386
386
387 c.versions = versions + [latest_ver]
387 c.versions = versions + [latest_ver]
388
388
389 # if we use version, then do not show later comments
389 # if we use version, then do not show later comments
390 # than current version
390 # than current version
391 display_inline_comments = collections.defaultdict(
391 display_inline_comments = collections.defaultdict(
392 lambda: collections.defaultdict(list))
392 lambda: collections.defaultdict(list))
393 for co in inline_comments:
393 for co in inline_comments:
394 if c.at_version_num:
394 if c.at_version_num:
395 # pick comments that are at least UPTO given version, so we
395 # pick comments that are at least UPTO given version, so we
396 # don't render comments for higher version
396 # don't render comments for higher version
397 should_render = co.pull_request_version_id and \
397 should_render = co.pull_request_version_id and \
398 co.pull_request_version_id <= c.at_version_num
398 co.pull_request_version_id <= c.at_version_num
399 else:
399 else:
400 # showing all, for 'latest'
400 # showing all, for 'latest'
401 should_render = True
401 should_render = True
402
402
403 if should_render:
403 if should_render:
404 display_inline_comments[co.f_path][co.line_no].append(co)
404 display_inline_comments[co.f_path][co.line_no].append(co)
405
405
406 # load diff data into template context, if we use compare mode then
406 # load diff data into template context, if we use compare mode then
407 # diff is calculated based on changes between versions of PR
407 # diff is calculated based on changes between versions of PR
408
408
409 source_repo = pull_request_at_ver.source_repo
409 source_repo = pull_request_at_ver.source_repo
410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
411
411
412 target_repo = pull_request_at_ver.target_repo
412 target_repo = pull_request_at_ver.target_repo
413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
414
414
415 if compare:
415 if compare:
416 # in compare switch the diff base to latest commit from prev version
416 # in compare switch the diff base to latest commit from prev version
417 target_ref_id = prev_pull_request_display_obj.revisions[0]
417 target_ref_id = prev_pull_request_display_obj.revisions[0]
418
418
419 # despite opening commits for bookmarks/branches/tags, we always
419 # despite opening commits for bookmarks/branches/tags, we always
420 # convert this to rev to prevent changes after bookmark or branch change
420 # convert this to rev to prevent changes after bookmark or branch change
421 c.source_ref_type = 'rev'
421 c.source_ref_type = 'rev'
422 c.source_ref = source_ref_id
422 c.source_ref = source_ref_id
423
423
424 c.target_ref_type = 'rev'
424 c.target_ref_type = 'rev'
425 c.target_ref = target_ref_id
425 c.target_ref = target_ref_id
426
426
427 c.source_repo = source_repo
427 c.source_repo = source_repo
428 c.target_repo = target_repo
428 c.target_repo = target_repo
429
429
430 c.commit_ranges = []
430 c.commit_ranges = []
431 source_commit = EmptyCommit()
431 source_commit = EmptyCommit()
432 target_commit = EmptyCommit()
432 target_commit = EmptyCommit()
433 c.missing_requirements = False
433 c.missing_requirements = False
434
434
435 source_scm = source_repo.scm_instance()
435 source_scm = source_repo.scm_instance()
436 target_scm = target_repo.scm_instance()
436 target_scm = target_repo.scm_instance()
437
437
438 # try first shadow repo, fallback to regular repo
438 # try first shadow repo, fallback to regular repo
439 try:
439 try:
440 commits_source_repo = pull_request_latest.get_shadow_repo()
440 commits_source_repo = pull_request_latest.get_shadow_repo()
441 except Exception:
441 except Exception:
442 log.debug('Failed to get shadow repo', exc_info=True)
442 log.debug('Failed to get shadow repo', exc_info=True)
443 commits_source_repo = source_scm
443 commits_source_repo = source_scm
444
444
445 c.commits_source_repo = commits_source_repo
445 c.commits_source_repo = commits_source_repo
446 commit_cache = {}
446 commit_cache = {}
447 try:
447 try:
448 pre_load = ["author", "branch", "date", "message"]
448 pre_load = ["author", "branch", "date", "message"]
449 show_revs = pull_request_at_ver.revisions
449 show_revs = pull_request_at_ver.revisions
450 for rev in show_revs:
450 for rev in show_revs:
451 comm = commits_source_repo.get_commit(
451 comm = commits_source_repo.get_commit(
452 commit_id=rev, pre_load=pre_load)
452 commit_id=rev, pre_load=pre_load)
453 c.commit_ranges.append(comm)
453 c.commit_ranges.append(comm)
454 commit_cache[comm.raw_id] = comm
454 commit_cache[comm.raw_id] = comm
455
455
456 # Order here matters, we first need to get target, and then
456 # Order here matters, we first need to get target, and then
457 # the source
457 # the source
458 target_commit = commits_source_repo.get_commit(
458 target_commit = commits_source_repo.get_commit(
459 commit_id=safe_str(target_ref_id))
459 commit_id=safe_str(target_ref_id))
460
460
461 source_commit = commits_source_repo.get_commit(
461 source_commit = commits_source_repo.get_commit(
462 commit_id=safe_str(source_ref_id))
462 commit_id=safe_str(source_ref_id))
463
463
464 except CommitDoesNotExistError:
464 except CommitDoesNotExistError:
465 log.warning(
465 log.warning(
466 'Failed to get commit from `{}` repo'.format(
466 'Failed to get commit from `{}` repo'.format(
467 commits_source_repo), exc_info=True)
467 commits_source_repo), exc_info=True)
468 except RepositoryRequirementError:
468 except RepositoryRequirementError:
469 log.warning(
469 log.warning(
470 'Failed to get all required data from repo', exc_info=True)
470 'Failed to get all required data from repo', exc_info=True)
471 c.missing_requirements = True
471 c.missing_requirements = True
472
472
473 c.ancestor = None # set it to None, to hide it from PR view
473 c.ancestor = None # set it to None, to hide it from PR view
474
474
475 try:
475 try:
476 ancestor_id = source_scm.get_common_ancestor(
476 ancestor_id = source_scm.get_common_ancestor(
477 source_commit.raw_id, target_commit.raw_id, target_scm)
477 source_commit.raw_id, target_commit.raw_id, target_scm)
478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
479 except Exception:
479 except Exception:
480 c.ancestor_commit = None
480 c.ancestor_commit = None
481
481
482 c.statuses = source_repo.statuses(
482 c.statuses = source_repo.statuses(
483 [x.raw_id for x in c.commit_ranges])
483 [x.raw_id for x in c.commit_ranges])
484
484
485 # auto collapse if we have more than limit
485 # auto collapse if we have more than limit
486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
488 c.compare_mode = compare
488 c.compare_mode = compare
489
489
490 # diff_limit is the old behavior, will cut off the whole diff
490 # diff_limit is the old behavior, will cut off the whole diff
491 # if the limit is applied otherwise will just hide the
491 # if the limit is applied otherwise will just hide the
492 # big files from the front-end
492 # big files from the front-end
493 diff_limit = c.visual.cut_off_limit_diff
493 diff_limit = c.visual.cut_off_limit_diff
494 file_limit = c.visual.cut_off_limit_file
494 file_limit = c.visual.cut_off_limit_file
495
495
496 c.missing_commits = False
496 c.missing_commits = False
497 if (c.missing_requirements
497 if (c.missing_requirements
498 or isinstance(source_commit, EmptyCommit)
498 or isinstance(source_commit, EmptyCommit)
499 or source_commit == target_commit):
499 or source_commit == target_commit):
500
500
501 c.missing_commits = True
501 c.missing_commits = True
502 else:
502 else:
503
503
504 c.diffset = self._get_diffset(
504 c.diffset = self._get_diffset(
505 c.source_repo.repo_name, commits_source_repo,
505 c.source_repo.repo_name, commits_source_repo,
506 source_ref_id, target_ref_id,
506 source_ref_id, target_ref_id,
507 target_commit, source_commit,
507 target_commit, source_commit,
508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
509
509
510 c.limited_diff = c.diffset.limited_diff
510 c.limited_diff = c.diffset.limited_diff
511
511
512 # calculate removed files that are bound to comments
512 # calculate removed files that are bound to comments
513 comment_deleted_files = [
513 comment_deleted_files = [
514 fname for fname in display_inline_comments
514 fname for fname in display_inline_comments
515 if fname not in c.diffset.file_stats]
515 if fname not in c.diffset.file_stats]
516
516
517 c.deleted_files_comments = collections.defaultdict(dict)
517 c.deleted_files_comments = collections.defaultdict(dict)
518 for fname, per_line_comments in display_inline_comments.items():
518 for fname, per_line_comments in display_inline_comments.items():
519 if fname in comment_deleted_files:
519 if fname in comment_deleted_files:
520 c.deleted_files_comments[fname]['stats'] = 0
520 c.deleted_files_comments[fname]['stats'] = 0
521 c.deleted_files_comments[fname]['comments'] = list()
521 c.deleted_files_comments[fname]['comments'] = list()
522 for lno, comments in per_line_comments.items():
522 for lno, comments in per_line_comments.items():
523 c.deleted_files_comments[fname]['comments'].extend(
523 c.deleted_files_comments[fname]['comments'].extend(
524 comments)
524 comments)
525
525
526 # this is a hack to properly display links, when creating PR, the
526 # this is a hack to properly display links, when creating PR, the
527 # compare view and others uses different notation, and
527 # compare view and others uses different notation, and
528 # compare_commits.mako renders links based on the target_repo.
528 # compare_commits.mako renders links based on the target_repo.
529 # We need to swap that here to generate it properly on the html side
529 # We need to swap that here to generate it properly on the html side
530 c.target_repo = c.source_repo
530 c.target_repo = c.source_repo
531
531
532 c.commit_statuses = ChangesetStatus.STATUSES
532 c.commit_statuses = ChangesetStatus.STATUSES
533
533
534 c.show_version_changes = not pr_closed
534 c.show_version_changes = not pr_closed
535 if c.show_version_changes:
535 if c.show_version_changes:
536 cur_obj = pull_request_at_ver
536 cur_obj = pull_request_at_ver
537 prev_obj = prev_pull_request_at_ver
537 prev_obj = prev_pull_request_at_ver
538
538
539 old_commit_ids = prev_obj.revisions
539 old_commit_ids = prev_obj.revisions
540 new_commit_ids = cur_obj.revisions
540 new_commit_ids = cur_obj.revisions
541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
542 old_commit_ids, new_commit_ids)
542 old_commit_ids, new_commit_ids)
543 c.commit_changes_summary = commit_changes
543 c.commit_changes_summary = commit_changes
544
544
545 # calculate the diff for commits between versions
545 # calculate the diff for commits between versions
546 c.commit_changes = []
546 c.commit_changes = []
547 mark = lambda cs, fw: list(
547 mark = lambda cs, fw: list(
548 h.itertools.izip_longest([], cs, fillvalue=fw))
548 h.itertools.izip_longest([], cs, fillvalue=fw))
549 for c_type, raw_id in mark(commit_changes.added, 'a') \
549 for c_type, raw_id in mark(commit_changes.added, 'a') \
550 + mark(commit_changes.removed, 'r') \
550 + mark(commit_changes.removed, 'r') \
551 + mark(commit_changes.common, 'c'):
551 + mark(commit_changes.common, 'c'):
552
552
553 if raw_id in commit_cache:
553 if raw_id in commit_cache:
554 commit = commit_cache[raw_id]
554 commit = commit_cache[raw_id]
555 else:
555 else:
556 try:
556 try:
557 commit = commits_source_repo.get_commit(raw_id)
557 commit = commits_source_repo.get_commit(raw_id)
558 except CommitDoesNotExistError:
558 except CommitDoesNotExistError:
559 # in case we fail extracting still use "dummy" commit
559 # in case we fail extracting still use "dummy" commit
560 # for display in commit diff
560 # for display in commit diff
561 commit = h.AttributeDict(
561 commit = h.AttributeDict(
562 {'raw_id': raw_id,
562 {'raw_id': raw_id,
563 'message': 'EMPTY or MISSING COMMIT'})
563 'message': 'EMPTY or MISSING COMMIT'})
564 c.commit_changes.append([c_type, commit])
564 c.commit_changes.append([c_type, commit])
565
565
566 # current user review statuses for each version
566 # current user review statuses for each version
567 c.review_versions = {}
567 c.review_versions = {}
568 if self._rhodecode_user.user_id in allowed_reviewers:
568 if self._rhodecode_user.user_id in allowed_reviewers:
569 for co in general_comments:
569 for co in general_comments:
570 if co.author.user_id == self._rhodecode_user.user_id:
570 if co.author.user_id == self._rhodecode_user.user_id:
571 # each comment has a status change
571 # each comment has a status change
572 status = co.status_change
572 status = co.status_change
573 if status:
573 if status:
574 _ver_pr = status[0].comment.pull_request_version_id
574 _ver_pr = status[0].comment.pull_request_version_id
575 c.review_versions[_ver_pr] = status[0]
575 c.review_versions[_ver_pr] = status[0]
576
576
577 return self._get_template_context(c)
577 return self._get_template_context(c)
578
578
579 def assure_not_empty_repo(self):
579 def assure_not_empty_repo(self):
580 _ = self.request.translate
580 _ = self.request.translate
581
581
582 try:
582 try:
583 self.db_repo.scm_instance().get_commit()
583 self.db_repo.scm_instance().get_commit()
584 except EmptyRepositoryError:
584 except EmptyRepositoryError:
585 h.flash(h.literal(_('There are no commits yet')),
585 h.flash(h.literal(_('There are no commits yet')),
586 category='warning')
586 category='warning')
587 raise HTTPFound(
587 raise HTTPFound(
588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
589
589
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator(
592 @HasRepoPermissionAnyDecorator(
593 'repository.read', 'repository.write', 'repository.admin')
593 'repository.read', 'repository.write', 'repository.admin')
594 @view_config(
594 @view_config(
595 route_name='pullrequest_new', request_method='GET',
595 route_name='pullrequest_new', request_method='GET',
596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
597 def pull_request_new(self):
597 def pull_request_new(self):
598 _ = self.request.translate
598 _ = self.request.translate
599 c = self.load_default_context()
599 c = self.load_default_context()
600
600
601 self.assure_not_empty_repo()
601 self.assure_not_empty_repo()
602 source_repo = self.db_repo
602 source_repo = self.db_repo
603
603
604 commit_id = self.request.GET.get('commit')
604 commit_id = self.request.GET.get('commit')
605 branch_ref = self.request.GET.get('branch')
605 branch_ref = self.request.GET.get('branch')
606 bookmark_ref = self.request.GET.get('bookmark')
606 bookmark_ref = self.request.GET.get('bookmark')
607
607
608 try:
608 try:
609 source_repo_data = PullRequestModel().generate_repo_data(
609 source_repo_data = PullRequestModel().generate_repo_data(
610 source_repo, commit_id=commit_id,
610 source_repo, commit_id=commit_id,
611 branch=branch_ref, bookmark=bookmark_ref,
611 branch=branch_ref, bookmark=bookmark_ref,
612 translator=self.request.translate)
612 translator=self.request.translate)
613 except CommitDoesNotExistError as e:
613 except CommitDoesNotExistError as e:
614 log.exception(e)
614 log.exception(e)
615 h.flash(_('Commit does not exist'), 'error')
615 h.flash(_('Commit does not exist'), 'error')
616 raise HTTPFound(
616 raise HTTPFound(
617 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
617 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
618
618
619 default_target_repo = source_repo
619 default_target_repo = source_repo
620
620
621 if source_repo.parent:
621 if source_repo.parent:
622 parent_vcs_obj = source_repo.parent.scm_instance()
622 parent_vcs_obj = source_repo.parent.scm_instance()
623 if parent_vcs_obj and not parent_vcs_obj.is_empty():
623 if parent_vcs_obj and not parent_vcs_obj.is_empty():
624 # change default if we have a parent repo
624 # change default if we have a parent repo
625 default_target_repo = source_repo.parent
625 default_target_repo = source_repo.parent
626
626
627 target_repo_data = PullRequestModel().generate_repo_data(
627 target_repo_data = PullRequestModel().generate_repo_data(
628 default_target_repo, translator=self.request.translate)
628 default_target_repo, translator=self.request.translate)
629
629
630 selected_source_ref = source_repo_data['refs']['selected_ref']
630 selected_source_ref = source_repo_data['refs']['selected_ref']
631 title_source_ref = ''
631 title_source_ref = ''
632 if selected_source_ref:
632 if selected_source_ref:
633 title_source_ref = selected_source_ref.split(':', 2)[1]
633 title_source_ref = selected_source_ref.split(':', 2)[1]
634 c.default_title = PullRequestModel().generate_pullrequest_title(
634 c.default_title = PullRequestModel().generate_pullrequest_title(
635 source=source_repo.repo_name,
635 source=source_repo.repo_name,
636 source_ref=title_source_ref,
636 source_ref=title_source_ref,
637 target=default_target_repo.repo_name
637 target=default_target_repo.repo_name
638 )
638 )
639
639
640 c.default_repo_data = {
640 c.default_repo_data = {
641 'source_repo_name': source_repo.repo_name,
641 'source_repo_name': source_repo.repo_name,
642 'source_refs_json': json.dumps(source_repo_data),
642 'source_refs_json': json.dumps(source_repo_data),
643 'target_repo_name': default_target_repo.repo_name,
643 'target_repo_name': default_target_repo.repo_name,
644 'target_refs_json': json.dumps(target_repo_data),
644 'target_refs_json': json.dumps(target_repo_data),
645 }
645 }
646 c.default_source_ref = selected_source_ref
646 c.default_source_ref = selected_source_ref
647
647
648 return self._get_template_context(c)
648 return self._get_template_context(c)
649
649
650 @LoginRequired()
650 @LoginRequired()
651 @NotAnonymous()
651 @NotAnonymous()
652 @HasRepoPermissionAnyDecorator(
652 @HasRepoPermissionAnyDecorator(
653 'repository.read', 'repository.write', 'repository.admin')
653 'repository.read', 'repository.write', 'repository.admin')
654 @view_config(
654 @view_config(
655 route_name='pullrequest_repo_refs', request_method='GET',
655 route_name='pullrequest_repo_refs', request_method='GET',
656 renderer='json_ext', xhr=True)
656 renderer='json_ext', xhr=True)
657 def pull_request_repo_refs(self):
657 def pull_request_repo_refs(self):
658 self.load_default_context()
658 self.load_default_context()
659 target_repo_name = self.request.matchdict['target_repo_name']
659 target_repo_name = self.request.matchdict['target_repo_name']
660 repo = Repository.get_by_repo_name(target_repo_name)
660 repo = Repository.get_by_repo_name(target_repo_name)
661 if not repo:
661 if not repo:
662 raise HTTPNotFound()
662 raise HTTPNotFound()
663
663
664 target_perm = HasRepoPermissionAny(
664 target_perm = HasRepoPermissionAny(
665 'repository.read', 'repository.write', 'repository.admin')(
665 'repository.read', 'repository.write', 'repository.admin')(
666 target_repo_name)
666 target_repo_name)
667 if not target_perm:
667 if not target_perm:
668 raise HTTPNotFound()
668 raise HTTPNotFound()
669
669
670 return PullRequestModel().generate_repo_data(
670 return PullRequestModel().generate_repo_data(
671 repo, translator=self.request.translate)
671 repo, translator=self.request.translate)
672
672
673 @LoginRequired()
673 @LoginRequired()
674 @NotAnonymous()
674 @NotAnonymous()
675 @HasRepoPermissionAnyDecorator(
675 @HasRepoPermissionAnyDecorator(
676 'repository.read', 'repository.write', 'repository.admin')
676 'repository.read', 'repository.write', 'repository.admin')
677 @view_config(
677 @view_config(
678 route_name='pullrequest_repo_destinations', request_method='GET',
678 route_name='pullrequest_repo_destinations', request_method='GET',
679 renderer='json_ext', xhr=True)
679 renderer='json_ext', xhr=True)
680 def pull_request_repo_destinations(self):
680 def pull_request_repo_destinations(self):
681 _ = self.request.translate
681 _ = self.request.translate
682 filter_query = self.request.GET.get('query')
682 filter_query = self.request.GET.get('query')
683
683
684 query = Repository.query() \
684 query = Repository.query() \
685 .order_by(func.length(Repository.repo_name)) \
685 .order_by(func.length(Repository.repo_name)) \
686 .filter(
686 .filter(
687 or_(Repository.repo_name == self.db_repo.repo_name,
687 or_(Repository.repo_name == self.db_repo.repo_name,
688 Repository.fork_id == self.db_repo.repo_id))
688 Repository.fork_id == self.db_repo.repo_id))
689
689
690 if filter_query:
690 if filter_query:
691 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
691 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
692 query = query.filter(
692 query = query.filter(
693 Repository.repo_name.ilike(ilike_expression))
693 Repository.repo_name.ilike(ilike_expression))
694
694
695 add_parent = False
695 add_parent = False
696 if self.db_repo.parent:
696 if self.db_repo.parent:
697 if filter_query in self.db_repo.parent.repo_name:
697 if filter_query in self.db_repo.parent.repo_name:
698 parent_vcs_obj = self.db_repo.parent.scm_instance()
698 parent_vcs_obj = self.db_repo.parent.scm_instance()
699 if parent_vcs_obj and not parent_vcs_obj.is_empty():
699 if parent_vcs_obj and not parent_vcs_obj.is_empty():
700 add_parent = True
700 add_parent = True
701
701
702 limit = 20 - 1 if add_parent else 20
702 limit = 20 - 1 if add_parent else 20
703 all_repos = query.limit(limit).all()
703 all_repos = query.limit(limit).all()
704 if add_parent:
704 if add_parent:
705 all_repos += [self.db_repo.parent]
705 all_repos += [self.db_repo.parent]
706
706
707 repos = []
707 repos = []
708 for obj in ScmModel().get_repos(all_repos):
708 for obj in ScmModel().get_repos(all_repos):
709 repos.append({
709 repos.append({
710 'id': obj['name'],
710 'id': obj['name'],
711 'text': obj['name'],
711 'text': obj['name'],
712 'type': 'repo',
712 'type': 'repo',
713 'obj': obj['dbrepo']
713 'obj': obj['dbrepo']
714 })
714 })
715
715
716 data = {
716 data = {
717 'more': False,
717 'more': False,
718 'results': [{
718 'results': [{
719 'text': _('Repositories'),
719 'text': _('Repositories'),
720 'children': repos
720 'children': repos
721 }] if repos else []
721 }] if repos else []
722 }
722 }
723 return data
723 return data
724
724
725 @LoginRequired()
725 @LoginRequired()
726 @NotAnonymous()
726 @NotAnonymous()
727 @HasRepoPermissionAnyDecorator(
727 @HasRepoPermissionAnyDecorator(
728 'repository.read', 'repository.write', 'repository.admin')
728 'repository.read', 'repository.write', 'repository.admin')
729 @CSRFRequired()
729 @CSRFRequired()
730 @view_config(
730 @view_config(
731 route_name='pullrequest_create', request_method='POST',
731 route_name='pullrequest_create', request_method='POST',
732 renderer=None)
732 renderer=None)
733 def pull_request_create(self):
733 def pull_request_create(self):
734 _ = self.request.translate
734 _ = self.request.translate
735 self.assure_not_empty_repo()
735 self.assure_not_empty_repo()
736 self.load_default_context()
736 self.load_default_context()
737
737
738 controls = peppercorn.parse(self.request.POST.items())
738 controls = peppercorn.parse(self.request.POST.items())
739
739
740 try:
740 try:
741 form = PullRequestForm(
741 form = PullRequestForm(
742 self.request.translate, self.db_repo.repo_id)()
742 self.request.translate, self.db_repo.repo_id)()
743 _form = form.to_python(controls)
743 _form = form.to_python(controls)
744 except formencode.Invalid as errors:
744 except formencode.Invalid as errors:
745 if errors.error_dict.get('revisions'):
745 if errors.error_dict.get('revisions'):
746 msg = 'Revisions: %s' % errors.error_dict['revisions']
746 msg = 'Revisions: %s' % errors.error_dict['revisions']
747 elif errors.error_dict.get('pullrequest_title'):
747 elif errors.error_dict.get('pullrequest_title'):
748 msg = errors.error_dict.get('pullrequest_title')
748 msg = errors.error_dict.get('pullrequest_title')
749 else:
749 else:
750 msg = _('Error creating pull request: {}').format(errors)
750 msg = _('Error creating pull request: {}').format(errors)
751 log.exception(msg)
751 log.exception(msg)
752 h.flash(msg, 'error')
752 h.flash(msg, 'error')
753
753
754 # would rather just go back to form ...
754 # would rather just go back to form ...
755 raise HTTPFound(
755 raise HTTPFound(
756 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
756 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
757
757
758 source_repo = _form['source_repo']
758 source_repo = _form['source_repo']
759 source_ref = _form['source_ref']
759 source_ref = _form['source_ref']
760 target_repo = _form['target_repo']
760 target_repo = _form['target_repo']
761 target_ref = _form['target_ref']
761 target_ref = _form['target_ref']
762 commit_ids = _form['revisions'][::-1]
762 commit_ids = _form['revisions'][::-1]
763
763
764 # find the ancestor for this pr
764 # find the ancestor for this pr
765 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
765 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
766 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
766 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
767
767
768 # re-check permissions again here
768 # re-check permissions again here
769 # source_repo we must have read permissions
769 # source_repo we must have read permissions
770
770
771 source_perm = HasRepoPermissionAny(
771 source_perm = HasRepoPermissionAny(
772 'repository.read',
772 'repository.read',
773 'repository.write', 'repository.admin')(source_db_repo.repo_name)
773 'repository.write', 'repository.admin')(source_db_repo.repo_name)
774 if not source_perm:
774 if not source_perm:
775 msg = _('Not Enough permissions to source repo `{}`.'.format(
775 msg = _('Not Enough permissions to source repo `{}`.'.format(
776 source_db_repo.repo_name))
776 source_db_repo.repo_name))
777 h.flash(msg, category='error')
777 h.flash(msg, category='error')
778 # copy the args back to redirect
778 # copy the args back to redirect
779 org_query = self.request.GET.mixed()
779 org_query = self.request.GET.mixed()
780 raise HTTPFound(
780 raise HTTPFound(
781 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
781 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
782 _query=org_query))
782 _query=org_query))
783
783
784 # target repo we must have read permissions, and also later on
784 # target repo we must have read permissions, and also later on
785 # we want to check branch permissions here
785 # we want to check branch permissions here
786 target_perm = HasRepoPermissionAny(
786 target_perm = HasRepoPermissionAny(
787 'repository.read',
787 'repository.read',
788 'repository.write', 'repository.admin')(target_db_repo.repo_name)
788 'repository.write', 'repository.admin')(target_db_repo.repo_name)
789 if not target_perm:
789 if not target_perm:
790 msg = _('Not Enough permissions to target repo `{}`.'.format(
790 msg = _('Not Enough permissions to target repo `{}`.'.format(
791 target_db_repo.repo_name))
791 target_db_repo.repo_name))
792 h.flash(msg, category='error')
792 h.flash(msg, category='error')
793 # copy the args back to redirect
793 # copy the args back to redirect
794 org_query = self.request.GET.mixed()
794 org_query = self.request.GET.mixed()
795 raise HTTPFound(
795 raise HTTPFound(
796 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
796 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
797 _query=org_query))
797 _query=org_query))
798
798
799 source_scm = source_db_repo.scm_instance()
799 source_scm = source_db_repo.scm_instance()
800 target_scm = target_db_repo.scm_instance()
800 target_scm = target_db_repo.scm_instance()
801
801
802 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
802 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
803 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
803 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
804
804
805 ancestor = source_scm.get_common_ancestor(
805 ancestor = source_scm.get_common_ancestor(
806 source_commit.raw_id, target_commit.raw_id, target_scm)
806 source_commit.raw_id, target_commit.raw_id, target_scm)
807
807
808 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
808 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
809 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
809 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
810
810
811 pullrequest_title = _form['pullrequest_title']
811 pullrequest_title = _form['pullrequest_title']
812 title_source_ref = source_ref.split(':', 2)[1]
812 title_source_ref = source_ref.split(':', 2)[1]
813 if not pullrequest_title:
813 if not pullrequest_title:
814 pullrequest_title = PullRequestModel().generate_pullrequest_title(
814 pullrequest_title = PullRequestModel().generate_pullrequest_title(
815 source=source_repo,
815 source=source_repo,
816 source_ref=title_source_ref,
816 source_ref=title_source_ref,
817 target=target_repo
817 target=target_repo
818 )
818 )
819
819
820 description = _form['pullrequest_desc']
820 description = _form['pullrequest_desc']
821
821
822 get_default_reviewers_data, validate_default_reviewers = \
822 get_default_reviewers_data, validate_default_reviewers = \
823 PullRequestModel().get_reviewer_functions()
823 PullRequestModel().get_reviewer_functions()
824
824
825 # recalculate reviewers logic, to make sure we can validate this
825 # recalculate reviewers logic, to make sure we can validate this
826 reviewer_rules = get_default_reviewers_data(
826 reviewer_rules = get_default_reviewers_data(
827 self._rhodecode_db_user, source_db_repo,
827 self._rhodecode_db_user, source_db_repo,
828 source_commit, target_db_repo, target_commit)
828 source_commit, target_db_repo, target_commit)
829
829
830 given_reviewers = _form['review_members']
830 given_reviewers = _form['review_members']
831 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
831 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
832
832
833 try:
833 try:
834 pull_request = PullRequestModel().create(
834 pull_request = PullRequestModel().create(
835 self._rhodecode_user.user_id, source_repo, source_ref,
835 self._rhodecode_user.user_id, source_repo, source_ref,
836 target_repo, target_ref, commit_ids, reviewers,
836 target_repo, target_ref, commit_ids, reviewers,
837 pullrequest_title, description, reviewer_rules
837 pullrequest_title, description, reviewer_rules
838 )
838 )
839 Session().commit()
839 Session().commit()
840
840
841 h.flash(_('Successfully opened new pull request'),
841 h.flash(_('Successfully opened new pull request'),
842 category='success')
842 category='success')
843 except Exception:
843 except Exception:
844 msg = _('Error occurred during creation of this pull request.')
844 msg = _('Error occurred during creation of this pull request.')
845 log.exception(msg)
845 log.exception(msg)
846 h.flash(msg, category='error')
846 h.flash(msg, category='error')
847
847
848 # copy the args back to redirect
848 # copy the args back to redirect
849 org_query = self.request.GET.mixed()
849 org_query = self.request.GET.mixed()
850 raise HTTPFound(
850 raise HTTPFound(
851 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
851 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
852 _query=org_query))
852 _query=org_query))
853
853
854 raise HTTPFound(
854 raise HTTPFound(
855 h.route_path('pullrequest_show', repo_name=target_repo,
855 h.route_path('pullrequest_show', repo_name=target_repo,
856 pull_request_id=pull_request.pull_request_id))
856 pull_request_id=pull_request.pull_request_id))
857
857
858 @LoginRequired()
858 @LoginRequired()
859 @NotAnonymous()
859 @NotAnonymous()
860 @HasRepoPermissionAnyDecorator(
860 @HasRepoPermissionAnyDecorator(
861 'repository.read', 'repository.write', 'repository.admin')
861 'repository.read', 'repository.write', 'repository.admin')
862 @CSRFRequired()
862 @CSRFRequired()
863 @view_config(
863 @view_config(
864 route_name='pullrequest_update', request_method='POST',
864 route_name='pullrequest_update', request_method='POST',
865 renderer='json_ext')
865 renderer='json_ext')
866 def pull_request_update(self):
866 def pull_request_update(self):
867 pull_request = PullRequest.get_or_404(
867 pull_request = PullRequest.get_or_404(
868 self.request.matchdict['pull_request_id'])
868 self.request.matchdict['pull_request_id'])
869 _ = self.request.translate
869 _ = self.request.translate
870
870
871 self.load_default_context()
871 self.load_default_context()
872
872
873 if pull_request.is_closed():
873 if pull_request.is_closed():
874 log.debug('update: forbidden because pull request is closed')
874 log.debug('update: forbidden because pull request is closed')
875 msg = _(u'Cannot update closed pull requests.')
875 msg = _(u'Cannot update closed pull requests.')
876 h.flash(msg, category='error')
876 h.flash(msg, category='error')
877 return True
877 return True
878
878
879 # only owner or admin can update it
879 # only owner or admin can update it
880 allowed_to_update = PullRequestModel().check_user_update(
880 allowed_to_update = PullRequestModel().check_user_update(
881 pull_request, self._rhodecode_user)
881 pull_request, self._rhodecode_user)
882 if allowed_to_update:
882 if allowed_to_update:
883 controls = peppercorn.parse(self.request.POST.items())
883 controls = peppercorn.parse(self.request.POST.items())
884
884
885 if 'review_members' in controls:
885 if 'review_members' in controls:
886 self._update_reviewers(
886 self._update_reviewers(
887 pull_request, controls['review_members'],
887 pull_request, controls['review_members'],
888 pull_request.reviewer_data)
888 pull_request.reviewer_data)
889 elif str2bool(self.request.POST.get('update_commits', 'false')):
889 elif str2bool(self.request.POST.get('update_commits', 'false')):
890 self._update_commits(pull_request)
890 self._update_commits(pull_request)
891 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
891 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
892 self._edit_pull_request(pull_request)
892 self._edit_pull_request(pull_request)
893 else:
893 else:
894 raise HTTPBadRequest()
894 raise HTTPBadRequest()
895 return True
895 return True
896 raise HTTPForbidden()
896 raise HTTPForbidden()
897
897
898 def _edit_pull_request(self, pull_request):
898 def _edit_pull_request(self, pull_request):
899 _ = self.request.translate
899 _ = self.request.translate
900 try:
900 try:
901 PullRequestModel().edit(
901 PullRequestModel().edit(
902 pull_request, self.request.POST.get('title'),
902 pull_request, self.request.POST.get('title'),
903 self.request.POST.get('description'), self._rhodecode_user)
903 self.request.POST.get('description'), self._rhodecode_user)
904 except ValueError:
904 except ValueError:
905 msg = _(u'Cannot update closed pull requests.')
905 msg = _(u'Cannot update closed pull requests.')
906 h.flash(msg, category='error')
906 h.flash(msg, category='error')
907 return
907 return
908 else:
908 else:
909 Session().commit()
909 Session().commit()
910
910
911 msg = _(u'Pull request title & description updated.')
911 msg = _(u'Pull request title & description updated.')
912 h.flash(msg, category='success')
912 h.flash(msg, category='success')
913 return
913 return
914
914
915 def _update_commits(self, pull_request):
915 def _update_commits(self, pull_request):
916 _ = self.request.translate
916 _ = self.request.translate
917 resp = PullRequestModel().update_commits(pull_request)
917 resp = PullRequestModel().update_commits(pull_request)
918
918
919 if resp.executed:
919 if resp.executed:
920
920
921 if resp.target_changed and resp.source_changed:
921 if resp.target_changed and resp.source_changed:
922 changed = 'target and source repositories'
922 changed = 'target and source repositories'
923 elif resp.target_changed and not resp.source_changed:
923 elif resp.target_changed and not resp.source_changed:
924 changed = 'target repository'
924 changed = 'target repository'
925 elif not resp.target_changed and resp.source_changed:
925 elif not resp.target_changed and resp.source_changed:
926 changed = 'source repository'
926 changed = 'source repository'
927 else:
927 else:
928 changed = 'nothing'
928 changed = 'nothing'
929
929
930 msg = _(
930 msg = _(
931 u'Pull request updated to "{source_commit_id}" with '
931 u'Pull request updated to "{source_commit_id}" with '
932 u'{count_added} added, {count_removed} removed commits. '
932 u'{count_added} added, {count_removed} removed commits. '
933 u'Source of changes: {change_source}')
933 u'Source of changes: {change_source}')
934 msg = msg.format(
934 msg = msg.format(
935 source_commit_id=pull_request.source_ref_parts.commit_id,
935 source_commit_id=pull_request.source_ref_parts.commit_id,
936 count_added=len(resp.changes.added),
936 count_added=len(resp.changes.added),
937 count_removed=len(resp.changes.removed),
937 count_removed=len(resp.changes.removed),
938 change_source=changed)
938 change_source=changed)
939 h.flash(msg, category='success')
939 h.flash(msg, category='success')
940
940
941 channel = '/repo${}$/pr/{}'.format(
941 channel = '/repo${}$/pr/{}'.format(
942 pull_request.target_repo.repo_name,
942 pull_request.target_repo.repo_name,
943 pull_request.pull_request_id)
943 pull_request.pull_request_id)
944 message = msg + (
944 message = msg + (
945 ' - <a onclick="window.location.reload()">'
945 ' - <a onclick="window.location.reload()">'
946 '<strong>{}</strong></a>'.format(_('Reload page')))
946 '<strong>{}</strong></a>'.format(_('Reload page')))
947 channelstream.post_message(
947 channelstream.post_message(
948 channel, message, self._rhodecode_user.username,
948 channel, message, self._rhodecode_user.username,
949 registry=self.request.registry)
949 registry=self.request.registry)
950 else:
950 else:
951 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
951 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
952 warning_reasons = [
952 warning_reasons = [
953 UpdateFailureReason.NO_CHANGE,
953 UpdateFailureReason.NO_CHANGE,
954 UpdateFailureReason.WRONG_REF_TYPE,
954 UpdateFailureReason.WRONG_REF_TYPE,
955 ]
955 ]
956 category = 'warning' if resp.reason in warning_reasons else 'error'
956 category = 'warning' if resp.reason in warning_reasons else 'error'
957 h.flash(msg, category=category)
957 h.flash(msg, category=category)
958
958
959 @LoginRequired()
959 @LoginRequired()
960 @NotAnonymous()
960 @NotAnonymous()
961 @HasRepoPermissionAnyDecorator(
961 @HasRepoPermissionAnyDecorator(
962 'repository.read', 'repository.write', 'repository.admin')
962 'repository.read', 'repository.write', 'repository.admin')
963 @CSRFRequired()
963 @CSRFRequired()
964 @view_config(
964 @view_config(
965 route_name='pullrequest_merge', request_method='POST',
965 route_name='pullrequest_merge', request_method='POST',
966 renderer='json_ext')
966 renderer='json_ext')
967 def pull_request_merge(self):
967 def pull_request_merge(self):
968 """
968 """
969 Merge will perform a server-side merge of the specified
969 Merge will perform a server-side merge of the specified
970 pull request, if the pull request is approved and mergeable.
970 pull request, if the pull request is approved and mergeable.
971 After successful merging, the pull request is automatically
971 After successful merging, the pull request is automatically
972 closed, with a relevant comment.
972 closed, with a relevant comment.
973 """
973 """
974 pull_request = PullRequest.get_or_404(
974 pull_request = PullRequest.get_or_404(
975 self.request.matchdict['pull_request_id'])
975 self.request.matchdict['pull_request_id'])
976
976
977 self.load_default_context()
977 self.load_default_context()
978 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
978 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
979 translator=self.request.translate)
979 translator=self.request.translate)
980 merge_possible = not check.failed
980 merge_possible = not check.failed
981
981
982 for err_type, error_msg in check.errors:
982 for err_type, error_msg in check.errors:
983 h.flash(error_msg, category=err_type)
983 h.flash(error_msg, category=err_type)
984
984
985 if merge_possible:
985 if merge_possible:
986 log.debug("Pre-conditions checked, trying to merge.")
986 log.debug("Pre-conditions checked, trying to merge.")
987 extras = vcs_operation_context(
987 extras = vcs_operation_context(
988 self.request.environ, repo_name=pull_request.target_repo.repo_name,
988 self.request.environ, repo_name=pull_request.target_repo.repo_name,
989 username=self._rhodecode_db_user.username, action='push',
989 username=self._rhodecode_db_user.username, action='push',
990 scm=pull_request.target_repo.repo_type)
990 scm=pull_request.target_repo.repo_type)
991 self._merge_pull_request(
991 self._merge_pull_request(
992 pull_request, self._rhodecode_db_user, extras)
992 pull_request, self._rhodecode_db_user, extras)
993 else:
993 else:
994 log.debug("Pre-conditions failed, NOT merging.")
994 log.debug("Pre-conditions failed, NOT merging.")
995
995
996 raise HTTPFound(
996 raise HTTPFound(
997 h.route_path('pullrequest_show',
997 h.route_path('pullrequest_show',
998 repo_name=pull_request.target_repo.repo_name,
998 repo_name=pull_request.target_repo.repo_name,
999 pull_request_id=pull_request.pull_request_id))
999 pull_request_id=pull_request.pull_request_id))
1000
1000
1001 def _merge_pull_request(self, pull_request, user, extras):
1001 def _merge_pull_request(self, pull_request, user, extras):
1002 _ = self.request.translate
1002 _ = self.request.translate
1003 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1003 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1004
1004
1005 if merge_resp.executed:
1005 if merge_resp.executed:
1006 log.debug("The merge was successful, closing the pull request.")
1006 log.debug("The merge was successful, closing the pull request.")
1007 PullRequestModel().close_pull_request(
1007 PullRequestModel().close_pull_request(
1008 pull_request.pull_request_id, user)
1008 pull_request.pull_request_id, user)
1009 Session().commit()
1009 Session().commit()
1010 msg = _('Pull request was successfully merged and closed.')
1010 msg = _('Pull request was successfully merged and closed.')
1011 h.flash(msg, category='success')
1011 h.flash(msg, category='success')
1012 else:
1012 else:
1013 log.debug(
1013 log.debug(
1014 "The merge was not successful. Merge response: %s",
1014 "The merge was not successful. Merge response: %s",
1015 merge_resp)
1015 merge_resp)
1016 msg = PullRequestModel().merge_status_message(
1016 msg = PullRequestModel().merge_status_message(
1017 merge_resp.failure_reason)
1017 merge_resp.failure_reason)
1018 h.flash(msg, category='error')
1018 h.flash(msg, category='error')
1019
1019
1020 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1020 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1021 _ = self.request.translate
1021 _ = self.request.translate
1022 get_default_reviewers_data, validate_default_reviewers = \
1022 get_default_reviewers_data, validate_default_reviewers = \
1023 PullRequestModel().get_reviewer_functions()
1023 PullRequestModel().get_reviewer_functions()
1024
1024
1025 try:
1025 try:
1026 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1026 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1027 except ValueError as e:
1027 except ValueError as e:
1028 log.error('Reviewers Validation: {}'.format(e))
1028 log.error('Reviewers Validation: {}'.format(e))
1029 h.flash(e, category='error')
1029 h.flash(e, category='error')
1030 return
1030 return
1031
1031
1032 PullRequestModel().update_reviewers(
1032 PullRequestModel().update_reviewers(
1033 pull_request, reviewers, self._rhodecode_user)
1033 pull_request, reviewers, self._rhodecode_user)
1034 h.flash(_('Pull request reviewers updated.'), category='success')
1034 h.flash(_('Pull request reviewers updated.'), category='success')
1035 Session().commit()
1035 Session().commit()
1036
1036
1037 @LoginRequired()
1037 @LoginRequired()
1038 @NotAnonymous()
1038 @NotAnonymous()
1039 @HasRepoPermissionAnyDecorator(
1039 @HasRepoPermissionAnyDecorator(
1040 'repository.read', 'repository.write', 'repository.admin')
1040 'repository.read', 'repository.write', 'repository.admin')
1041 @CSRFRequired()
1041 @CSRFRequired()
1042 @view_config(
1042 @view_config(
1043 route_name='pullrequest_delete', request_method='POST',
1043 route_name='pullrequest_delete', request_method='POST',
1044 renderer='json_ext')
1044 renderer='json_ext')
1045 def pull_request_delete(self):
1045 def pull_request_delete(self):
1046 _ = self.request.translate
1046 _ = self.request.translate
1047
1047
1048 pull_request = PullRequest.get_or_404(
1048 pull_request = PullRequest.get_or_404(
1049 self.request.matchdict['pull_request_id'])
1049 self.request.matchdict['pull_request_id'])
1050 self.load_default_context()
1050 self.load_default_context()
1051
1051
1052 pr_closed = pull_request.is_closed()
1052 pr_closed = pull_request.is_closed()
1053 allowed_to_delete = PullRequestModel().check_user_delete(
1053 allowed_to_delete = PullRequestModel().check_user_delete(
1054 pull_request, self._rhodecode_user) and not pr_closed
1054 pull_request, self._rhodecode_user) and not pr_closed
1055
1055
1056 # only owner can delete it !
1056 # only owner can delete it !
1057 if allowed_to_delete:
1057 if allowed_to_delete:
1058 PullRequestModel().delete(pull_request, self._rhodecode_user)
1058 PullRequestModel().delete(pull_request, self._rhodecode_user)
1059 Session().commit()
1059 Session().commit()
1060 h.flash(_('Successfully deleted pull request'),
1060 h.flash(_('Successfully deleted pull request'),
1061 category='success')
1061 category='success')
1062 raise HTTPFound(h.route_path('pullrequest_show_all',
1062 raise HTTPFound(h.route_path('pullrequest_show_all',
1063 repo_name=self.db_repo_name))
1063 repo_name=self.db_repo_name))
1064
1064
1065 log.warning('user %s tried to delete pull request without access',
1065 log.warning('user %s tried to delete pull request without access',
1066 self._rhodecode_user)
1066 self._rhodecode_user)
1067 raise HTTPNotFound()
1067 raise HTTPNotFound()
1068
1068
1069 @LoginRequired()
1069 @LoginRequired()
1070 @NotAnonymous()
1070 @NotAnonymous()
1071 @HasRepoPermissionAnyDecorator(
1071 @HasRepoPermissionAnyDecorator(
1072 'repository.read', 'repository.write', 'repository.admin')
1072 'repository.read', 'repository.write', 'repository.admin')
1073 @CSRFRequired()
1073 @CSRFRequired()
1074 @view_config(
1074 @view_config(
1075 route_name='pullrequest_comment_create', request_method='POST',
1075 route_name='pullrequest_comment_create', request_method='POST',
1076 renderer='json_ext')
1076 renderer='json_ext')
1077 def pull_request_comment_create(self):
1077 def pull_request_comment_create(self):
1078 _ = self.request.translate
1078 _ = self.request.translate
1079
1079
1080 pull_request = PullRequest.get_or_404(
1080 pull_request = PullRequest.get_or_404(
1081 self.request.matchdict['pull_request_id'])
1081 self.request.matchdict['pull_request_id'])
1082 pull_request_id = pull_request.pull_request_id
1082 pull_request_id = pull_request.pull_request_id
1083
1083
1084 if pull_request.is_closed():
1084 if pull_request.is_closed():
1085 log.debug('comment: forbidden because pull request is closed')
1085 log.debug('comment: forbidden because pull request is closed')
1086 raise HTTPForbidden()
1086 raise HTTPForbidden()
1087
1087
1088 allowed_to_comment = PullRequestModel().check_user_comment(
1088 allowed_to_comment = PullRequestModel().check_user_comment(
1089 pull_request, self._rhodecode_user)
1089 pull_request, self._rhodecode_user)
1090 if not allowed_to_comment:
1090 if not allowed_to_comment:
1091 log.debug(
1091 log.debug(
1092 'comment: forbidden because pull request is from forbidden repo')
1092 'comment: forbidden because pull request is from forbidden repo')
1093 raise HTTPForbidden()
1093 raise HTTPForbidden()
1094
1094
1095 c = self.load_default_context()
1095 c = self.load_default_context()
1096
1096
1097 status = self.request.POST.get('changeset_status', None)
1097 status = self.request.POST.get('changeset_status', None)
1098 text = self.request.POST.get('text')
1098 text = self.request.POST.get('text')
1099 comment_type = self.request.POST.get('comment_type')
1099 comment_type = self.request.POST.get('comment_type')
1100 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1100 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1101 close_pull_request = self.request.POST.get('close_pull_request')
1101 close_pull_request = self.request.POST.get('close_pull_request')
1102
1102
1103 # the logic here should work like following, if we submit close
1103 # the logic here should work like following, if we submit close
1104 # pr comment, use `close_pull_request_with_comment` function
1104 # pr comment, use `close_pull_request_with_comment` function
1105 # else handle regular comment logic
1105 # else handle regular comment logic
1106
1106
1107 if close_pull_request:
1107 if close_pull_request:
1108 # only owner or admin or person with write permissions
1108 # only owner or admin or person with write permissions
1109 allowed_to_close = PullRequestModel().check_user_update(
1109 allowed_to_close = PullRequestModel().check_user_update(
1110 pull_request, self._rhodecode_user)
1110 pull_request, self._rhodecode_user)
1111 if not allowed_to_close:
1111 if not allowed_to_close:
1112 log.debug('comment: forbidden because not allowed to close '
1112 log.debug('comment: forbidden because not allowed to close '
1113 'pull request %s', pull_request_id)
1113 'pull request %s', pull_request_id)
1114 raise HTTPForbidden()
1114 raise HTTPForbidden()
1115 comment, status = PullRequestModel().close_pull_request_with_comment(
1115 comment, status = PullRequestModel().close_pull_request_with_comment(
1116 pull_request, self._rhodecode_user, self.db_repo, message=text)
1116 pull_request, self._rhodecode_user, self.db_repo, message=text)
1117 Session().flush()
1117 Session().flush()
1118 events.trigger(
1118 events.trigger(
1119 events.PullRequestCommentEvent(pull_request, comment))
1119 events.PullRequestCommentEvent(pull_request, comment))
1120
1120
1121 else:
1121 else:
1122 # regular comment case, could be inline, or one with status.
1122 # regular comment case, could be inline, or one with status.
1123 # for that one we check also permissions
1123 # for that one we check also permissions
1124
1124
1125 allowed_to_change_status = PullRequestModel().check_user_change_status(
1125 allowed_to_change_status = PullRequestModel().check_user_change_status(
1126 pull_request, self._rhodecode_user)
1126 pull_request, self._rhodecode_user)
1127
1127
1128 if status and allowed_to_change_status:
1128 if status and allowed_to_change_status:
1129 message = (_('Status change %(transition_icon)s %(status)s')
1129 message = (_('Status change %(transition_icon)s %(status)s')
1130 % {'transition_icon': '>',
1130 % {'transition_icon': '>',
1131 'status': ChangesetStatus.get_status_lbl(status)})
1131 'status': ChangesetStatus.get_status_lbl(status)})
1132 text = text or message
1132 text = text or message
1133
1133
1134 comment = CommentsModel().create(
1134 comment = CommentsModel().create(
1135 text=text,
1135 text=text,
1136 repo=self.db_repo.repo_id,
1136 repo=self.db_repo.repo_id,
1137 user=self._rhodecode_user.user_id,
1137 user=self._rhodecode_user.user_id,
1138 pull_request=pull_request,
1138 pull_request=pull_request,
1139 f_path=self.request.POST.get('f_path'),
1139 f_path=self.request.POST.get('f_path'),
1140 line_no=self.request.POST.get('line'),
1140 line_no=self.request.POST.get('line'),
1141 status_change=(ChangesetStatus.get_status_lbl(status)
1141 status_change=(ChangesetStatus.get_status_lbl(status)
1142 if status and allowed_to_change_status else None),
1142 if status and allowed_to_change_status else None),
1143 status_change_type=(status
1143 status_change_type=(status
1144 if status and allowed_to_change_status else None),
1144 if status and allowed_to_change_status else None),
1145 comment_type=comment_type,
1145 comment_type=comment_type,
1146 resolves_comment_id=resolves_comment_id
1146 resolves_comment_id=resolves_comment_id
1147 )
1147 )
1148
1148
1149 if allowed_to_change_status:
1149 if allowed_to_change_status:
1150 # calculate old status before we change it
1150 # calculate old status before we change it
1151 old_calculated_status = pull_request.calculated_review_status()
1151 old_calculated_status = pull_request.calculated_review_status()
1152
1152
1153 # get status if set !
1153 # get status if set !
1154 if status:
1154 if status:
1155 ChangesetStatusModel().set_status(
1155 ChangesetStatusModel().set_status(
1156 self.db_repo.repo_id,
1156 self.db_repo.repo_id,
1157 status,
1157 status,
1158 self._rhodecode_user.user_id,
1158 self._rhodecode_user.user_id,
1159 comment,
1159 comment,
1160 pull_request=pull_request
1160 pull_request=pull_request
1161 )
1161 )
1162
1162
1163 Session().flush()
1163 Session().flush()
1164 # this is somehow required to get access to some relationship
1164 # this is somehow required to get access to some relationship
1165 # loaded on comment
1165 # loaded on comment
1166 Session().refresh(comment)
1166 Session().refresh(comment)
1167
1167
1168 events.trigger(
1168 events.trigger(
1169 events.PullRequestCommentEvent(pull_request, comment))
1169 events.PullRequestCommentEvent(pull_request, comment))
1170
1170
1171 # we now calculate the status of pull request, and based on that
1171 # we now calculate the status of pull request, and based on that
1172 # calculation we set the commits status
1172 # calculation we set the commits status
1173 calculated_status = pull_request.calculated_review_status()
1173 calculated_status = pull_request.calculated_review_status()
1174 if old_calculated_status != calculated_status:
1174 if old_calculated_status != calculated_status:
1175 PullRequestModel()._trigger_pull_request_hook(
1175 PullRequestModel()._trigger_pull_request_hook(
1176 pull_request, self._rhodecode_user, 'review_status_change')
1176 pull_request, self._rhodecode_user, 'review_status_change')
1177
1177
1178 Session().commit()
1178 Session().commit()
1179
1179
1180 data = {
1180 data = {
1181 'target_id': h.safeid(h.safe_unicode(
1181 'target_id': h.safeid(h.safe_unicode(
1182 self.request.POST.get('f_path'))),
1182 self.request.POST.get('f_path'))),
1183 }
1183 }
1184 if comment:
1184 if comment:
1185 c.co = comment
1185 c.co = comment
1186 rendered_comment = render(
1186 rendered_comment = render(
1187 'rhodecode:templates/changeset/changeset_comment_block.mako',
1187 'rhodecode:templates/changeset/changeset_comment_block.mako',
1188 self._get_template_context(c), self.request)
1188 self._get_template_context(c), self.request)
1189
1189
1190 data.update(comment.get_dict())
1190 data.update(comment.get_dict())
1191 data.update({'rendered_text': rendered_comment})
1191 data.update({'rendered_text': rendered_comment})
1192
1192
1193 return data
1193 return data
1194
1194
1195 @LoginRequired()
1195 @LoginRequired()
1196 @NotAnonymous()
1196 @NotAnonymous()
1197 @HasRepoPermissionAnyDecorator(
1197 @HasRepoPermissionAnyDecorator(
1198 'repository.read', 'repository.write', 'repository.admin')
1198 'repository.read', 'repository.write', 'repository.admin')
1199 @CSRFRequired()
1199 @CSRFRequired()
1200 @view_config(
1200 @view_config(
1201 route_name='pullrequest_comment_delete', request_method='POST',
1201 route_name='pullrequest_comment_delete', request_method='POST',
1202 renderer='json_ext')
1202 renderer='json_ext')
1203 def pull_request_comment_delete(self):
1203 def pull_request_comment_delete(self):
1204 pull_request = PullRequest.get_or_404(
1204 pull_request = PullRequest.get_or_404(
1205 self.request.matchdict['pull_request_id'])
1205 self.request.matchdict['pull_request_id'])
1206
1206
1207 comment = ChangesetComment.get_or_404(
1207 comment = ChangesetComment.get_or_404(
1208 self.request.matchdict['comment_id'])
1208 self.request.matchdict['comment_id'])
1209 comment_id = comment.comment_id
1209 comment_id = comment.comment_id
1210
1210
1211 if pull_request.is_closed():
1211 if pull_request.is_closed():
1212 log.debug('comment: forbidden because pull request is closed')
1212 log.debug('comment: forbidden because pull request is closed')
1213 raise HTTPForbidden()
1213 raise HTTPForbidden()
1214
1214
1215 if not comment:
1215 if not comment:
1216 log.debug('Comment with id:%s not found, skipping', comment_id)
1216 log.debug('Comment with id:%s not found, skipping', comment_id)
1217 # comment already deleted in another call probably
1217 # comment already deleted in another call probably
1218 return True
1218 return True
1219
1219
1220 if comment.pull_request.is_closed():
1220 if comment.pull_request.is_closed():
1221 # don't allow deleting comments on closed pull request
1221 # don't allow deleting comments on closed pull request
1222 raise HTTPForbidden()
1222 raise HTTPForbidden()
1223
1223
1224 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1224 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1225 super_admin = h.HasPermissionAny('hg.admin')()
1225 super_admin = h.HasPermissionAny('hg.admin')()
1226 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1226 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1227 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1227 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1228 comment_repo_admin = is_repo_admin and is_repo_comment
1228 comment_repo_admin = is_repo_admin and is_repo_comment
1229
1229
1230 if super_admin or comment_owner or comment_repo_admin:
1230 if super_admin or comment_owner or comment_repo_admin:
1231 old_calculated_status = comment.pull_request.calculated_review_status()
1231 old_calculated_status = comment.pull_request.calculated_review_status()
1232 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1232 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1233 Session().commit()
1233 Session().commit()
1234 calculated_status = comment.pull_request.calculated_review_status()
1234 calculated_status = comment.pull_request.calculated_review_status()
1235 if old_calculated_status != calculated_status:
1235 if old_calculated_status != calculated_status:
1236 PullRequestModel()._trigger_pull_request_hook(
1236 PullRequestModel()._trigger_pull_request_hook(
1237 comment.pull_request, self._rhodecode_user, 'review_status_change')
1237 comment.pull_request, self._rhodecode_user, 'review_status_change')
1238 return True
1238 return True
1239 else:
1239 else:
1240 log.warning('No permissions for user %s to delete comment_id: %s',
1240 log.warning('No permissions for user %s to delete comment_id: %s',
1241 self._rhodecode_db_user, comment_id)
1241 self._rhodecode_db_user, comment_id)
1242 raise HTTPNotFound()
1242 raise HTTPNotFound()
@@ -1,1620 +1,1641 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import itertools
27 import itertools
28 import logging
28 import logging
29 import os
29 import os
30 import time
30 import time
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 from rhodecode.lib.vcs import connection
36 from rhodecode.lib.vcs import connection
37 from rhodecode.lib.vcs.utils import author_name, author_email
37 from rhodecode.lib.vcs.utils import author_name, author_email
38 from rhodecode.lib.vcs.conf import settings
38 from rhodecode.lib.vcs.conf import settings
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 RepositoryError)
44 RepositoryError)
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 FILEMODE_DEFAULT = 0100644
50 FILEMODE_DEFAULT = 0100644
51 FILEMODE_EXECUTABLE = 0100755
51 FILEMODE_EXECUTABLE = 0100755
52
52
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 MergeResponse = collections.namedtuple(
54 MergeResponse = collections.namedtuple(
55 'MergeResponse',
55 'MergeResponse',
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57
57
58
58
59 class MergeFailureReason(object):
59 class MergeFailureReason(object):
60 """
60 """
61 Enumeration with all the reasons why the server side merge could fail.
61 Enumeration with all the reasons why the server side merge could fail.
62
62
63 DO NOT change the number of the reasons, as they may be stored in the
63 DO NOT change the number of the reasons, as they may be stored in the
64 database.
64 database.
65
65
66 Changing the name of a reason is acceptable and encouraged to deprecate old
66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 reasons.
67 reasons.
68 """
68 """
69
69
70 # Everything went well.
70 # Everything went well.
71 NONE = 0
71 NONE = 0
72
72
73 # An unexpected exception was raised. Check the logs for more details.
73 # An unexpected exception was raised. Check the logs for more details.
74 UNKNOWN = 1
74 UNKNOWN = 1
75
75
76 # The merge was not successful, there are conflicts.
76 # The merge was not successful, there are conflicts.
77 MERGE_FAILED = 2
77 MERGE_FAILED = 2
78
78
79 # The merge succeeded but we could not push it to the target repository.
79 # The merge succeeded but we could not push it to the target repository.
80 PUSH_FAILED = 3
80 PUSH_FAILED = 3
81
81
82 # The specified target is not a head in the target repository.
82 # The specified target is not a head in the target repository.
83 TARGET_IS_NOT_HEAD = 4
83 TARGET_IS_NOT_HEAD = 4
84
84
85 # The source repository contains more branches than the target. Pushing
85 # The source repository contains more branches than the target. Pushing
86 # the merge will create additional branches in the target.
86 # the merge will create additional branches in the target.
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88
88
89 # The target reference has multiple heads. That does not allow to correctly
89 # The target reference has multiple heads. That does not allow to correctly
90 # identify the target location. This could only happen for mercurial
90 # identify the target location. This could only happen for mercurial
91 # branches.
91 # branches.
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93
93
94 # The target repository is locked
94 # The target repository is locked
95 TARGET_IS_LOCKED = 7
95 TARGET_IS_LOCKED = 7
96
96
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # A involved commit could not be found.
98 # A involved commit could not be found.
99 _DEPRECATED_MISSING_COMMIT = 8
99 _DEPRECATED_MISSING_COMMIT = 8
100
100
101 # The target repo reference is missing.
101 # The target repo reference is missing.
102 MISSING_TARGET_REF = 9
102 MISSING_TARGET_REF = 9
103
103
104 # The source repo reference is missing.
104 # The source repo reference is missing.
105 MISSING_SOURCE_REF = 10
105 MISSING_SOURCE_REF = 10
106
106
107 # The merge was not successful, there are conflicts related to sub
107 # The merge was not successful, there are conflicts related to sub
108 # repositories.
108 # repositories.
109 SUBREPO_MERGE_FAILED = 11
109 SUBREPO_MERGE_FAILED = 11
110
110
111
111
112 class UpdateFailureReason(object):
112 class UpdateFailureReason(object):
113 """
113 """
114 Enumeration with all the reasons why the pull request update could fail.
114 Enumeration with all the reasons why the pull request update could fail.
115
115
116 DO NOT change the number of the reasons, as they may be stored in the
116 DO NOT change the number of the reasons, as they may be stored in the
117 database.
117 database.
118
118
119 Changing the name of a reason is acceptable and encouraged to deprecate old
119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 reasons.
120 reasons.
121 """
121 """
122
122
123 # Everything went well.
123 # Everything went well.
124 NONE = 0
124 NONE = 0
125
125
126 # An unexpected exception was raised. Check the logs for more details.
126 # An unexpected exception was raised. Check the logs for more details.
127 UNKNOWN = 1
127 UNKNOWN = 1
128
128
129 # The pull request is up to date.
129 # The pull request is up to date.
130 NO_CHANGE = 2
130 NO_CHANGE = 2
131
131
132 # The pull request has a reference type that is not supported for update.
132 # The pull request has a reference type that is not supported for update.
133 WRONG_REF_TYPE = 3
133 WRONG_REF_TYPE = 3
134
134
135 # Update failed because the target reference is missing.
135 # Update failed because the target reference is missing.
136 MISSING_TARGET_REF = 4
136 MISSING_TARGET_REF = 4
137
137
138 # Update failed because the source reference is missing.
138 # Update failed because the source reference is missing.
139 MISSING_SOURCE_REF = 5
139 MISSING_SOURCE_REF = 5
140
140
141
141
142 class BaseRepository(object):
142 class BaseRepository(object):
143 """
143 """
144 Base Repository for final backends
144 Base Repository for final backends
145
145
146 .. attribute:: DEFAULT_BRANCH_NAME
146 .. attribute:: DEFAULT_BRANCH_NAME
147
147
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149
149
150 .. attribute:: commit_ids
150 .. attribute:: commit_ids
151
151
152 list of all available commit ids, in ascending order
152 list of all available commit ids, in ascending order
153
153
154 .. attribute:: path
154 .. attribute:: path
155
155
156 absolute path to the repository
156 absolute path to the repository
157
157
158 .. attribute:: bookmarks
158 .. attribute:: bookmarks
159
159
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 there are no bookmarks or the backend implementation does not support
161 there are no bookmarks or the backend implementation does not support
162 bookmarks.
162 bookmarks.
163
163
164 .. attribute:: tags
164 .. attribute:: tags
165
165
166 Mapping from name to :term:`Commit ID` of the tag.
166 Mapping from name to :term:`Commit ID` of the tag.
167
167
168 """
168 """
169
169
170 DEFAULT_BRANCH_NAME = None
170 DEFAULT_BRANCH_NAME = None
171 DEFAULT_CONTACT = u"Unknown"
171 DEFAULT_CONTACT = u"Unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
173 EMPTY_COMMIT_ID = '0' * 40
173 EMPTY_COMMIT_ID = '0' * 40
174
174
175 path = None
175 path = None
176
176
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 """
178 """
179 Initializes repository. Raises RepositoryError if repository could
179 Initializes repository. Raises RepositoryError if repository could
180 not be find at the given ``repo_path`` or directory at ``repo_path``
180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 exists and ``create`` is set to True.
181 exists and ``create`` is set to True.
182
182
183 :param repo_path: local path of the repository
183 :param repo_path: local path of the repository
184 :param config: repository configuration
184 :param config: repository configuration
185 :param create=False: if set to True, would try to create repository.
185 :param create=False: if set to True, would try to create repository.
186 :param src_url=None: if set, should be proper url from which repository
186 :param src_url=None: if set, should be proper url from which repository
187 would be cloned; requires ``create`` parameter to be set to True -
187 would be cloned; requires ``create`` parameter to be set to True -
188 raises RepositoryError if src_url is set and create evaluates to
188 raises RepositoryError if src_url is set and create evaluates to
189 False
189 False
190 """
190 """
191 raise NotImplementedError
191 raise NotImplementedError
192
192
193 def __repr__(self):
193 def __repr__(self):
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195
195
196 def __len__(self):
196 def __len__(self):
197 return self.count()
197 return self.count()
198
198
199 def __eq__(self, other):
199 def __eq__(self, other):
200 same_instance = isinstance(other, self.__class__)
200 same_instance = isinstance(other, self.__class__)
201 return same_instance and other.path == self.path
201 return same_instance and other.path == self.path
202
202
203 def __ne__(self, other):
203 def __ne__(self, other):
204 return not self.__eq__(other)
204 return not self.__eq__(other)
205
205
206 @classmethod
206 @classmethod
207 def get_default_config(cls, default=None):
207 def get_default_config(cls, default=None):
208 config = Config()
208 config = Config()
209 if default and isinstance(default, list):
209 if default and isinstance(default, list):
210 for section, key, val in default:
210 for section, key, val in default:
211 config.set(section, key, val)
211 config.set(section, key, val)
212 return config
212 return config
213
213
214 @LazyProperty
214 @LazyProperty
215 def EMPTY_COMMIT(self):
215 def EMPTY_COMMIT(self):
216 return EmptyCommit(self.EMPTY_COMMIT_ID)
216 return EmptyCommit(self.EMPTY_COMMIT_ID)
217
217
218 @LazyProperty
218 @LazyProperty
219 def alias(self):
219 def alias(self):
220 for k, v in settings.BACKENDS.items():
220 for k, v in settings.BACKENDS.items():
221 if v.split('.')[-1] == str(self.__class__.__name__):
221 if v.split('.')[-1] == str(self.__class__.__name__):
222 return k
222 return k
223
223
224 @LazyProperty
224 @LazyProperty
225 def name(self):
225 def name(self):
226 return safe_unicode(os.path.basename(self.path))
226 return safe_unicode(os.path.basename(self.path))
227
227
228 @LazyProperty
228 @LazyProperty
229 def description(self):
229 def description(self):
230 raise NotImplementedError
230 raise NotImplementedError
231
231
232 def refs(self):
232 def refs(self):
233 """
233 """
234 returns a `dict` with branches, bookmarks, tags, and closed_branches
234 returns a `dict` with branches, bookmarks, tags, and closed_branches
235 for this repository
235 for this repository
236 """
236 """
237 return dict(
237 return dict(
238 branches=self.branches,
238 branches=self.branches,
239 branches_closed=self.branches_closed,
239 branches_closed=self.branches_closed,
240 tags=self.tags,
240 tags=self.tags,
241 bookmarks=self.bookmarks
241 bookmarks=self.bookmarks
242 )
242 )
243
243
244 @LazyProperty
244 @LazyProperty
245 def branches(self):
245 def branches(self):
246 """
246 """
247 A `dict` which maps branch names to commit ids.
247 A `dict` which maps branch names to commit ids.
248 """
248 """
249 raise NotImplementedError
249 raise NotImplementedError
250
250
251 @LazyProperty
251 @LazyProperty
252 def branches_closed(self):
252 def branches_closed(self):
253 """
253 """
254 A `dict` which maps tags names to commit ids.
254 A `dict` which maps tags names to commit ids.
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258 @LazyProperty
258 @LazyProperty
259 def bookmarks(self):
259 def bookmarks(self):
260 """
260 """
261 A `dict` which maps tags names to commit ids.
261 A `dict` which maps tags names to commit ids.
262 """
262 """
263 raise NotImplementedError
263 raise NotImplementedError
264
264
265 @LazyProperty
265 @LazyProperty
266 def tags(self):
266 def tags(self):
267 """
267 """
268 A `dict` which maps tags names to commit ids.
268 A `dict` which maps tags names to commit ids.
269 """
269 """
270 raise NotImplementedError
270 raise NotImplementedError
271
271
272 @LazyProperty
272 @LazyProperty
273 def size(self):
273 def size(self):
274 """
274 """
275 Returns combined size in bytes for all repository files
275 Returns combined size in bytes for all repository files
276 """
276 """
277 tip = self.get_commit()
277 tip = self.get_commit()
278 return tip.size
278 return tip.size
279
279
280 def size_at_commit(self, commit_id):
280 def size_at_commit(self, commit_id):
281 commit = self.get_commit(commit_id)
281 commit = self.get_commit(commit_id)
282 return commit.size
282 return commit.size
283
283
284 def is_empty(self):
284 def is_empty(self):
285 return not bool(self.commit_ids)
285 return not bool(self.commit_ids)
286
286
287 @staticmethod
287 @staticmethod
288 def check_url(url, config):
288 def check_url(url, config):
289 """
289 """
290 Function will check given url and try to verify if it's a valid
290 Function will check given url and try to verify if it's a valid
291 link.
291 link.
292 """
292 """
293 raise NotImplementedError
293 raise NotImplementedError
294
294
295 @staticmethod
295 @staticmethod
296 def is_valid_repository(path):
296 def is_valid_repository(path):
297 """
297 """
298 Check if given `path` contains a valid repository of this backend
298 Check if given `path` contains a valid repository of this backend
299 """
299 """
300 raise NotImplementedError
300 raise NotImplementedError
301
301
302 # ==========================================================================
302 # ==========================================================================
303 # COMMITS
303 # COMMITS
304 # ==========================================================================
304 # ==========================================================================
305
305
306 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
306 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
307 """
307 """
308 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
308 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
309 are both None, most recent commit is returned.
309 are both None, most recent commit is returned.
310
310
311 :param pre_load: Optional. List of commit attributes to load.
311 :param pre_load: Optional. List of commit attributes to load.
312
312
313 :raises ``EmptyRepositoryError``: if there are no commits
313 :raises ``EmptyRepositoryError``: if there are no commits
314 """
314 """
315 raise NotImplementedError
315 raise NotImplementedError
316
316
317 def __iter__(self):
317 def __iter__(self):
318 for commit_id in self.commit_ids:
318 for commit_id in self.commit_ids:
319 yield self.get_commit(commit_id=commit_id)
319 yield self.get_commit(commit_id=commit_id)
320
320
321 def get_commits(
321 def get_commits(
322 self, start_id=None, end_id=None, start_date=None, end_date=None,
322 self, start_id=None, end_id=None, start_date=None, end_date=None,
323 branch_name=None, show_hidden=False, pre_load=None):
323 branch_name=None, show_hidden=False, pre_load=None):
324 """
324 """
325 Returns iterator of `BaseCommit` objects from start to end
325 Returns iterator of `BaseCommit` objects from start to end
326 not inclusive. This should behave just like a list, ie. end is not
326 not inclusive. This should behave just like a list, ie. end is not
327 inclusive.
327 inclusive.
328
328
329 :param start_id: None or str, must be a valid commit id
329 :param start_id: None or str, must be a valid commit id
330 :param end_id: None or str, must be a valid commit id
330 :param end_id: None or str, must be a valid commit id
331 :param start_date:
331 :param start_date:
332 :param end_date:
332 :param end_date:
333 :param branch_name:
333 :param branch_name:
334 :param show_hidden:
334 :param show_hidden:
335 :param pre_load:
335 :param pre_load:
336 """
336 """
337 raise NotImplementedError
337 raise NotImplementedError
338
338
339 def __getitem__(self, key):
339 def __getitem__(self, key):
340 """
340 """
341 Allows index based access to the commit objects of this repository.
341 Allows index based access to the commit objects of this repository.
342 """
342 """
343 pre_load = ["author", "branch", "date", "message", "parents"]
343 pre_load = ["author", "branch", "date", "message", "parents"]
344 if isinstance(key, slice):
344 if isinstance(key, slice):
345 return self._get_range(key, pre_load)
345 return self._get_range(key, pre_load)
346 return self.get_commit(commit_idx=key, pre_load=pre_load)
346 return self.get_commit(commit_idx=key, pre_load=pre_load)
347
347
348 def _get_range(self, slice_obj, pre_load):
348 def _get_range(self, slice_obj, pre_load):
349 for commit_id in self.commit_ids.__getitem__(slice_obj):
349 for commit_id in self.commit_ids.__getitem__(slice_obj):
350 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
350 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
351
351
352 def count(self):
352 def count(self):
353 return len(self.commit_ids)
353 return len(self.commit_ids)
354
354
355 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
355 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
356 """
356 """
357 Creates and returns a tag for the given ``commit_id``.
357 Creates and returns a tag for the given ``commit_id``.
358
358
359 :param name: name for new tag
359 :param name: name for new tag
360 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
360 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
361 :param commit_id: commit id for which new tag would be created
361 :param commit_id: commit id for which new tag would be created
362 :param message: message of the tag's commit
362 :param message: message of the tag's commit
363 :param date: date of tag's commit
363 :param date: date of tag's commit
364
364
365 :raises TagAlreadyExistError: if tag with same name already exists
365 :raises TagAlreadyExistError: if tag with same name already exists
366 """
366 """
367 raise NotImplementedError
367 raise NotImplementedError
368
368
369 def remove_tag(self, name, user, message=None, date=None):
369 def remove_tag(self, name, user, message=None, date=None):
370 """
370 """
371 Removes tag with the given ``name``.
371 Removes tag with the given ``name``.
372
372
373 :param name: name of the tag to be removed
373 :param name: name of the tag to be removed
374 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
374 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
375 :param message: message of the tag's removal commit
375 :param message: message of the tag's removal commit
376 :param date: date of tag's removal commit
376 :param date: date of tag's removal commit
377
377
378 :raises TagDoesNotExistError: if tag with given name does not exists
378 :raises TagDoesNotExistError: if tag with given name does not exists
379 """
379 """
380 raise NotImplementedError
380 raise NotImplementedError
381
381
382 def get_diff(
382 def get_diff(
383 self, commit1, commit2, path=None, ignore_whitespace=False,
383 self, commit1, commit2, path=None, ignore_whitespace=False,
384 context=3, path1=None):
384 context=3, path1=None):
385 """
385 """
386 Returns (git like) *diff*, as plain text. Shows changes introduced by
386 Returns (git like) *diff*, as plain text. Shows changes introduced by
387 `commit2` since `commit1`.
387 `commit2` since `commit1`.
388
388
389 :param commit1: Entry point from which diff is shown. Can be
389 :param commit1: Entry point from which diff is shown. Can be
390 ``self.EMPTY_COMMIT`` - in this case, patch showing all
390 ``self.EMPTY_COMMIT`` - in this case, patch showing all
391 the changes since empty state of the repository until `commit2`
391 the changes since empty state of the repository until `commit2`
392 :param commit2: Until which commit changes should be shown.
392 :param commit2: Until which commit changes should be shown.
393 :param path: Can be set to a path of a file to create a diff of that
393 :param path: Can be set to a path of a file to create a diff of that
394 file. If `path1` is also set, this value is only associated to
394 file. If `path1` is also set, this value is only associated to
395 `commit2`.
395 `commit2`.
396 :param ignore_whitespace: If set to ``True``, would not show whitespace
396 :param ignore_whitespace: If set to ``True``, would not show whitespace
397 changes. Defaults to ``False``.
397 changes. Defaults to ``False``.
398 :param context: How many lines before/after changed lines should be
398 :param context: How many lines before/after changed lines should be
399 shown. Defaults to ``3``.
399 shown. Defaults to ``3``.
400 :param path1: Can be set to a path to associate with `commit1`. This
400 :param path1: Can be set to a path to associate with `commit1`. This
401 parameter works only for backends which support diff generation for
401 parameter works only for backends which support diff generation for
402 different paths. Other backends will raise a `ValueError` if `path1`
402 different paths. Other backends will raise a `ValueError` if `path1`
403 is set and has a different value than `path`.
403 is set and has a different value than `path`.
404 :param file_path: filter this diff by given path pattern
404 :param file_path: filter this diff by given path pattern
405 """
405 """
406 raise NotImplementedError
406 raise NotImplementedError
407
407
408 def strip(self, commit_id, branch=None):
408 def strip(self, commit_id, branch=None):
409 """
409 """
410 Strip given commit_id from the repository
410 Strip given commit_id from the repository
411 """
411 """
412 raise NotImplementedError
412 raise NotImplementedError
413
413
414 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
414 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
415 """
415 """
416 Return a latest common ancestor commit if one exists for this repo
416 Return a latest common ancestor commit if one exists for this repo
417 `commit_id1` vs `commit_id2` from `repo2`.
417 `commit_id1` vs `commit_id2` from `repo2`.
418
418
419 :param commit_id1: Commit it from this repository to use as a
419 :param commit_id1: Commit it from this repository to use as a
420 target for the comparison.
420 target for the comparison.
421 :param commit_id2: Source commit id to use for comparison.
421 :param commit_id2: Source commit id to use for comparison.
422 :param repo2: Source repository to use for comparison.
422 :param repo2: Source repository to use for comparison.
423 """
423 """
424 raise NotImplementedError
424 raise NotImplementedError
425
425
426 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
426 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
427 """
427 """
428 Compare this repository's revision `commit_id1` with `commit_id2`.
428 Compare this repository's revision `commit_id1` with `commit_id2`.
429
429
430 Returns a tuple(commits, ancestor) that would be merged from
430 Returns a tuple(commits, ancestor) that would be merged from
431 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
431 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
432 will be returned as ancestor.
432 will be returned as ancestor.
433
433
434 :param commit_id1: Commit it from this repository to use as a
434 :param commit_id1: Commit it from this repository to use as a
435 target for the comparison.
435 target for the comparison.
436 :param commit_id2: Source commit id to use for comparison.
436 :param commit_id2: Source commit id to use for comparison.
437 :param repo2: Source repository to use for comparison.
437 :param repo2: Source repository to use for comparison.
438 :param merge: If set to ``True`` will do a merge compare which also
438 :param merge: If set to ``True`` will do a merge compare which also
439 returns the common ancestor.
439 returns the common ancestor.
440 :param pre_load: Optional. List of commit attributes to load.
440 :param pre_load: Optional. List of commit attributes to load.
441 """
441 """
442 raise NotImplementedError
442 raise NotImplementedError
443
443
444 def merge(self, target_ref, source_repo, source_ref, workspace_id,
444 def merge(self, target_ref, source_repo, source_ref, workspace_id,
445 user_name='', user_email='', message='', dry_run=False,
445 user_name='', user_email='', message='', dry_run=False,
446 use_rebase=False, close_branch=False):
446 use_rebase=False, close_branch=False):
447 """
447 """
448 Merge the revisions specified in `source_ref` from `source_repo`
448 Merge the revisions specified in `source_ref` from `source_repo`
449 onto the `target_ref` of this repository.
449 onto the `target_ref` of this repository.
450
450
451 `source_ref` and `target_ref` are named tupls with the following
451 `source_ref` and `target_ref` are named tupls with the following
452 fields `type`, `name` and `commit_id`.
452 fields `type`, `name` and `commit_id`.
453
453
454 Returns a MergeResponse named tuple with the following fields
454 Returns a MergeResponse named tuple with the following fields
455 'possible', 'executed', 'source_commit', 'target_commit',
455 'possible', 'executed', 'source_commit', 'target_commit',
456 'merge_commit'.
456 'merge_commit'.
457
457
458 :param target_ref: `target_ref` points to the commit on top of which
458 :param target_ref: `target_ref` points to the commit on top of which
459 the `source_ref` should be merged.
459 the `source_ref` should be merged.
460 :param source_repo: The repository that contains the commits to be
460 :param source_repo: The repository that contains the commits to be
461 merged.
461 merged.
462 :param source_ref: `source_ref` points to the topmost commit from
462 :param source_ref: `source_ref` points to the topmost commit from
463 the `source_repo` which should be merged.
463 the `source_repo` which should be merged.
464 :param workspace_id: `workspace_id` unique identifier.
464 :param workspace_id: `workspace_id` unique identifier.
465 :param user_name: Merge commit `user_name`.
465 :param user_name: Merge commit `user_name`.
466 :param user_email: Merge commit `user_email`.
466 :param user_email: Merge commit `user_email`.
467 :param message: Merge commit `message`.
467 :param message: Merge commit `message`.
468 :param dry_run: If `True` the merge will not take place.
468 :param dry_run: If `True` the merge will not take place.
469 :param use_rebase: If `True` commits from the source will be rebased
469 :param use_rebase: If `True` commits from the source will be rebased
470 on top of the target instead of being merged.
470 on top of the target instead of being merged.
471 :param close_branch: If `True` branch will be close before merging it
471 :param close_branch: If `True` branch will be close before merging it
472 """
472 """
473 if dry_run:
473 if dry_run:
474 message = message or 'dry_run_merge_message'
474 message = message or 'dry_run_merge_message'
475 user_email = user_email or 'dry-run-merge@rhodecode.com'
475 user_email = user_email or 'dry-run-merge@rhodecode.com'
476 user_name = user_name or 'Dry-Run User'
476 user_name = user_name or 'Dry-Run User'
477 else:
477 else:
478 if not user_name:
478 if not user_name:
479 raise ValueError('user_name cannot be empty')
479 raise ValueError('user_name cannot be empty')
480 if not user_email:
480 if not user_email:
481 raise ValueError('user_email cannot be empty')
481 raise ValueError('user_email cannot be empty')
482 if not message:
482 if not message:
483 raise ValueError('message cannot be empty')
483 raise ValueError('message cannot be empty')
484
484
485 shadow_repository_path = self._maybe_prepare_merge_workspace(
485 shadow_repository_path = self._maybe_prepare_merge_workspace(
486 workspace_id, target_ref, source_ref)
486 workspace_id, target_ref, source_ref)
487
487
488 try:
488 try:
489 return self._merge_repo(
489 return self._merge_repo(
490 shadow_repository_path, target_ref, source_repo,
490 shadow_repository_path, target_ref, source_repo,
491 source_ref, message, user_name, user_email, dry_run=dry_run,
491 source_ref, message, user_name, user_email, dry_run=dry_run,
492 use_rebase=use_rebase, close_branch=close_branch)
492 use_rebase=use_rebase, close_branch=close_branch)
493 except RepositoryError:
493 except RepositoryError:
494 log.exception(
494 log.exception(
495 'Unexpected failure when running merge, dry-run=%s',
495 'Unexpected failure when running merge, dry-run=%s',
496 dry_run)
496 dry_run)
497 return MergeResponse(
497 return MergeResponse(
498 False, False, None, MergeFailureReason.UNKNOWN)
498 False, False, None, MergeFailureReason.UNKNOWN)
499
499
500 def _merge_repo(self, shadow_repository_path, target_ref,
500 def _merge_repo(self, shadow_repository_path, target_ref,
501 source_repo, source_ref, merge_message,
501 source_repo, source_ref, merge_message,
502 merger_name, merger_email, dry_run=False,
502 merger_name, merger_email, dry_run=False,
503 use_rebase=False, close_branch=False):
503 use_rebase=False, close_branch=False):
504 """Internal implementation of merge."""
504 """Internal implementation of merge."""
505 raise NotImplementedError
505 raise NotImplementedError
506
506
507 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
507 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
508 """
508 """
509 Create the merge workspace.
509 Create the merge workspace.
510
510
511 :param workspace_id: `workspace_id` unique identifier.
511 :param workspace_id: `workspace_id` unique identifier.
512 """
512 """
513 raise NotImplementedError
513 raise NotImplementedError
514
514
515 def cleanup_merge_workspace(self, workspace_id):
515 def cleanup_merge_workspace(self, workspace_id):
516 """
516 """
517 Remove merge workspace.
517 Remove merge workspace.
518
518
519 This function MUST not fail in case there is no workspace associated to
519 This function MUST not fail in case there is no workspace associated to
520 the given `workspace_id`.
520 the given `workspace_id`.
521
521
522 :param workspace_id: `workspace_id` unique identifier.
522 :param workspace_id: `workspace_id` unique identifier.
523 """
523 """
524 raise NotImplementedError
524 raise NotImplementedError
525
525
526 # ========== #
526 # ========== #
527 # COMMIT API #
527 # COMMIT API #
528 # ========== #
528 # ========== #
529
529
530 @LazyProperty
530 @LazyProperty
531 def in_memory_commit(self):
531 def in_memory_commit(self):
532 """
532 """
533 Returns :class:`InMemoryCommit` object for this repository.
533 Returns :class:`InMemoryCommit` object for this repository.
534 """
534 """
535 raise NotImplementedError
535 raise NotImplementedError
536
536
537 # ======================== #
537 # ======================== #
538 # UTILITIES FOR SUBCLASSES #
538 # UTILITIES FOR SUBCLASSES #
539 # ======================== #
539 # ======================== #
540
540
541 def _validate_diff_commits(self, commit1, commit2):
541 def _validate_diff_commits(self, commit1, commit2):
542 """
542 """
543 Validates that the given commits are related to this repository.
543 Validates that the given commits are related to this repository.
544
544
545 Intended as a utility for sub classes to have a consistent validation
545 Intended as a utility for sub classes to have a consistent validation
546 of input parameters in methods like :meth:`get_diff`.
546 of input parameters in methods like :meth:`get_diff`.
547 """
547 """
548 self._validate_commit(commit1)
548 self._validate_commit(commit1)
549 self._validate_commit(commit2)
549 self._validate_commit(commit2)
550 if (isinstance(commit1, EmptyCommit) and
550 if (isinstance(commit1, EmptyCommit) and
551 isinstance(commit2, EmptyCommit)):
551 isinstance(commit2, EmptyCommit)):
552 raise ValueError("Cannot compare two empty commits")
552 raise ValueError("Cannot compare two empty commits")
553
553
554 def _validate_commit(self, commit):
554 def _validate_commit(self, commit):
555 if not isinstance(commit, BaseCommit):
555 if not isinstance(commit, BaseCommit):
556 raise TypeError(
556 raise TypeError(
557 "%s is not of type BaseCommit" % repr(commit))
557 "%s is not of type BaseCommit" % repr(commit))
558 if commit.repository != self and not isinstance(commit, EmptyCommit):
558 if commit.repository != self and not isinstance(commit, EmptyCommit):
559 raise ValueError(
559 raise ValueError(
560 "Commit %s must be a valid commit from this repository %s, "
560 "Commit %s must be a valid commit from this repository %s, "
561 "related to this repository instead %s." %
561 "related to this repository instead %s." %
562 (commit, self, commit.repository))
562 (commit, self, commit.repository))
563
563
564 def _validate_commit_id(self, commit_id):
564 def _validate_commit_id(self, commit_id):
565 if not isinstance(commit_id, basestring):
565 if not isinstance(commit_id, basestring):
566 raise TypeError("commit_id must be a string value")
566 raise TypeError("commit_id must be a string value")
567
567
568 def _validate_commit_idx(self, commit_idx):
568 def _validate_commit_idx(self, commit_idx):
569 if not isinstance(commit_idx, (int, long)):
569 if not isinstance(commit_idx, (int, long)):
570 raise TypeError("commit_idx must be a numeric value")
570 raise TypeError("commit_idx must be a numeric value")
571
571
572 def _validate_branch_name(self, branch_name):
572 def _validate_branch_name(self, branch_name):
573 if branch_name and branch_name not in self.branches_all:
573 if branch_name and branch_name not in self.branches_all:
574 msg = ("Branch %s not found in %s" % (branch_name, self))
574 msg = ("Branch %s not found in %s" % (branch_name, self))
575 raise BranchDoesNotExistError(msg)
575 raise BranchDoesNotExistError(msg)
576
576
577 #
577 #
578 # Supporting deprecated API parts
578 # Supporting deprecated API parts
579 # TODO: johbo: consider to move this into a mixin
579 # TODO: johbo: consider to move this into a mixin
580 #
580 #
581
581
582 @property
582 @property
583 def EMPTY_CHANGESET(self):
583 def EMPTY_CHANGESET(self):
584 warnings.warn(
584 warnings.warn(
585 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
585 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
586 return self.EMPTY_COMMIT_ID
586 return self.EMPTY_COMMIT_ID
587
587
588 @property
588 @property
589 def revisions(self):
589 def revisions(self):
590 warnings.warn("Use commits attribute instead", DeprecationWarning)
590 warnings.warn("Use commits attribute instead", DeprecationWarning)
591 return self.commit_ids
591 return self.commit_ids
592
592
593 @revisions.setter
593 @revisions.setter
594 def revisions(self, value):
594 def revisions(self, value):
595 warnings.warn("Use commits attribute instead", DeprecationWarning)
595 warnings.warn("Use commits attribute instead", DeprecationWarning)
596 self.commit_ids = value
596 self.commit_ids = value
597
597
598 def get_changeset(self, revision=None, pre_load=None):
598 def get_changeset(self, revision=None, pre_load=None):
599 warnings.warn("Use get_commit instead", DeprecationWarning)
599 warnings.warn("Use get_commit instead", DeprecationWarning)
600 commit_id = None
600 commit_id = None
601 commit_idx = None
601 commit_idx = None
602 if isinstance(revision, basestring):
602 if isinstance(revision, basestring):
603 commit_id = revision
603 commit_id = revision
604 else:
604 else:
605 commit_idx = revision
605 commit_idx = revision
606 return self.get_commit(
606 return self.get_commit(
607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608
608
609 def get_changesets(
609 def get_changesets(
610 self, start=None, end=None, start_date=None, end_date=None,
610 self, start=None, end=None, start_date=None, end_date=None,
611 branch_name=None, pre_load=None):
611 branch_name=None, pre_load=None):
612 warnings.warn("Use get_commits instead", DeprecationWarning)
612 warnings.warn("Use get_commits instead", DeprecationWarning)
613 start_id = self._revision_to_commit(start)
613 start_id = self._revision_to_commit(start)
614 end_id = self._revision_to_commit(end)
614 end_id = self._revision_to_commit(end)
615 return self.get_commits(
615 return self.get_commits(
616 start_id=start_id, end_id=end_id, start_date=start_date,
616 start_id=start_id, end_id=end_id, start_date=start_date,
617 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
617 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
618
618
619 def _revision_to_commit(self, revision):
619 def _revision_to_commit(self, revision):
620 """
620 """
621 Translates a revision to a commit_id
621 Translates a revision to a commit_id
622
622
623 Helps to support the old changeset based API which allows to use
623 Helps to support the old changeset based API which allows to use
624 commit ids and commit indices interchangeable.
624 commit ids and commit indices interchangeable.
625 """
625 """
626 if revision is None:
626 if revision is None:
627 return revision
627 return revision
628
628
629 if isinstance(revision, basestring):
629 if isinstance(revision, basestring):
630 commit_id = revision
630 commit_id = revision
631 else:
631 else:
632 commit_id = self.commit_ids[revision]
632 commit_id = self.commit_ids[revision]
633 return commit_id
633 return commit_id
634
634
635 @property
635 @property
636 def in_memory_changeset(self):
636 def in_memory_changeset(self):
637 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
637 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
638 return self.in_memory_commit
638 return self.in_memory_commit
639
639
640 #
641 def get_path_permissions(self, username):
642 """
643
644 Returns a path permission checker or None if not supported
645
646 :param username: session user name
647 :return: an instance of BasePathPermissionChecker or None
648 """
649 return None
650
640
651
641 class BaseCommit(object):
652 class BaseCommit(object):
642 """
653 """
643 Each backend should implement it's commit representation.
654 Each backend should implement it's commit representation.
644
655
645 **Attributes**
656 **Attributes**
646
657
647 ``repository``
658 ``repository``
648 repository object within which commit exists
659 repository object within which commit exists
649
660
650 ``id``
661 ``id``
651 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
662 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
652 just ``tip``.
663 just ``tip``.
653
664
654 ``raw_id``
665 ``raw_id``
655 raw commit representation (i.e. full 40 length sha for git
666 raw commit representation (i.e. full 40 length sha for git
656 backend)
667 backend)
657
668
658 ``short_id``
669 ``short_id``
659 shortened (if apply) version of ``raw_id``; it would be simple
670 shortened (if apply) version of ``raw_id``; it would be simple
660 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
671 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
661 as ``raw_id`` for subversion
672 as ``raw_id`` for subversion
662
673
663 ``idx``
674 ``idx``
664 commit index
675 commit index
665
676
666 ``files``
677 ``files``
667 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
678 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
668
679
669 ``dirs``
680 ``dirs``
670 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
681 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
671
682
672 ``nodes``
683 ``nodes``
673 combined list of ``Node`` objects
684 combined list of ``Node`` objects
674
685
675 ``author``
686 ``author``
676 author of the commit, as unicode
687 author of the commit, as unicode
677
688
678 ``message``
689 ``message``
679 message of the commit, as unicode
690 message of the commit, as unicode
680
691
681 ``parents``
692 ``parents``
682 list of parent commits
693 list of parent commits
683
694
684 """
695 """
685
696
686 branch = None
697 branch = None
687 """
698 """
688 Depending on the backend this should be set to the branch name of the
699 Depending on the backend this should be set to the branch name of the
689 commit. Backends not supporting branches on commits should leave this
700 commit. Backends not supporting branches on commits should leave this
690 value as ``None``.
701 value as ``None``.
691 """
702 """
692
703
693 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
704 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
694 """
705 """
695 This template is used to generate a default prefix for repository archives
706 This template is used to generate a default prefix for repository archives
696 if no prefix has been specified.
707 if no prefix has been specified.
697 """
708 """
698
709
699 def __str__(self):
710 def __str__(self):
700 return '<%s at %s:%s>' % (
711 return '<%s at %s:%s>' % (
701 self.__class__.__name__, self.idx, self.short_id)
712 self.__class__.__name__, self.idx, self.short_id)
702
713
703 def __repr__(self):
714 def __repr__(self):
704 return self.__str__()
715 return self.__str__()
705
716
706 def __unicode__(self):
717 def __unicode__(self):
707 return u'%s:%s' % (self.idx, self.short_id)
718 return u'%s:%s' % (self.idx, self.short_id)
708
719
709 def __eq__(self, other):
720 def __eq__(self, other):
710 same_instance = isinstance(other, self.__class__)
721 same_instance = isinstance(other, self.__class__)
711 return same_instance and self.raw_id == other.raw_id
722 return same_instance and self.raw_id == other.raw_id
712
723
713 def __json__(self):
724 def __json__(self):
714 parents = []
725 parents = []
715 try:
726 try:
716 for parent in self.parents:
727 for parent in self.parents:
717 parents.append({'raw_id': parent.raw_id})
728 parents.append({'raw_id': parent.raw_id})
718 except NotImplementedError:
729 except NotImplementedError:
719 # empty commit doesn't have parents implemented
730 # empty commit doesn't have parents implemented
720 pass
731 pass
721
732
722 return {
733 return {
723 'short_id': self.short_id,
734 'short_id': self.short_id,
724 'raw_id': self.raw_id,
735 'raw_id': self.raw_id,
725 'revision': self.idx,
736 'revision': self.idx,
726 'message': self.message,
737 'message': self.message,
727 'date': self.date,
738 'date': self.date,
728 'author': self.author,
739 'author': self.author,
729 'parents': parents,
740 'parents': parents,
730 'branch': self.branch
741 'branch': self.branch
731 }
742 }
732
743
733 def _get_refs(self):
744 def _get_refs(self):
734 return {
745 return {
735 'branches': [self.branch],
746 'branches': [self.branch],
736 'bookmarks': getattr(self, 'bookmarks', []),
747 'bookmarks': getattr(self, 'bookmarks', []),
737 'tags': self.tags
748 'tags': self.tags
738 }
749 }
739
750
740 @LazyProperty
751 @LazyProperty
741 def last(self):
752 def last(self):
742 """
753 """
743 ``True`` if this is last commit in repository, ``False``
754 ``True`` if this is last commit in repository, ``False``
744 otherwise; trying to access this attribute while there is no
755 otherwise; trying to access this attribute while there is no
745 commits would raise `EmptyRepositoryError`
756 commits would raise `EmptyRepositoryError`
746 """
757 """
747 if self.repository is None:
758 if self.repository is None:
748 raise CommitError("Cannot check if it's most recent commit")
759 raise CommitError("Cannot check if it's most recent commit")
749 return self.raw_id == self.repository.commit_ids[-1]
760 return self.raw_id == self.repository.commit_ids[-1]
750
761
751 @LazyProperty
762 @LazyProperty
752 def parents(self):
763 def parents(self):
753 """
764 """
754 Returns list of parent commits.
765 Returns list of parent commits.
755 """
766 """
756 raise NotImplementedError
767 raise NotImplementedError
757
768
758 @property
769 @property
759 def merge(self):
770 def merge(self):
760 """
771 """
761 Returns boolean if commit is a merge.
772 Returns boolean if commit is a merge.
762 """
773 """
763 return len(self.parents) > 1
774 return len(self.parents) > 1
764
775
765 @LazyProperty
776 @LazyProperty
766 def children(self):
777 def children(self):
767 """
778 """
768 Returns list of child commits.
779 Returns list of child commits.
769 """
780 """
770 raise NotImplementedError
781 raise NotImplementedError
771
782
772 @LazyProperty
783 @LazyProperty
773 def id(self):
784 def id(self):
774 """
785 """
775 Returns string identifying this commit.
786 Returns string identifying this commit.
776 """
787 """
777 raise NotImplementedError
788 raise NotImplementedError
778
789
779 @LazyProperty
790 @LazyProperty
780 def raw_id(self):
791 def raw_id(self):
781 """
792 """
782 Returns raw string identifying this commit.
793 Returns raw string identifying this commit.
783 """
794 """
784 raise NotImplementedError
795 raise NotImplementedError
785
796
786 @LazyProperty
797 @LazyProperty
787 def short_id(self):
798 def short_id(self):
788 """
799 """
789 Returns shortened version of ``raw_id`` attribute, as string,
800 Returns shortened version of ``raw_id`` attribute, as string,
790 identifying this commit, useful for presentation to users.
801 identifying this commit, useful for presentation to users.
791 """
802 """
792 raise NotImplementedError
803 raise NotImplementedError
793
804
794 @LazyProperty
805 @LazyProperty
795 def idx(self):
806 def idx(self):
796 """
807 """
797 Returns integer identifying this commit.
808 Returns integer identifying this commit.
798 """
809 """
799 raise NotImplementedError
810 raise NotImplementedError
800
811
801 @LazyProperty
812 @LazyProperty
802 def committer(self):
813 def committer(self):
803 """
814 """
804 Returns committer for this commit
815 Returns committer for this commit
805 """
816 """
806 raise NotImplementedError
817 raise NotImplementedError
807
818
808 @LazyProperty
819 @LazyProperty
809 def committer_name(self):
820 def committer_name(self):
810 """
821 """
811 Returns committer name for this commit
822 Returns committer name for this commit
812 """
823 """
813
824
814 return author_name(self.committer)
825 return author_name(self.committer)
815
826
816 @LazyProperty
827 @LazyProperty
817 def committer_email(self):
828 def committer_email(self):
818 """
829 """
819 Returns committer email address for this commit
830 Returns committer email address for this commit
820 """
831 """
821
832
822 return author_email(self.committer)
833 return author_email(self.committer)
823
834
824 @LazyProperty
835 @LazyProperty
825 def author(self):
836 def author(self):
826 """
837 """
827 Returns author for this commit
838 Returns author for this commit
828 """
839 """
829
840
830 raise NotImplementedError
841 raise NotImplementedError
831
842
832 @LazyProperty
843 @LazyProperty
833 def author_name(self):
844 def author_name(self):
834 """
845 """
835 Returns author name for this commit
846 Returns author name for this commit
836 """
847 """
837
848
838 return author_name(self.author)
849 return author_name(self.author)
839
850
840 @LazyProperty
851 @LazyProperty
841 def author_email(self):
852 def author_email(self):
842 """
853 """
843 Returns author email address for this commit
854 Returns author email address for this commit
844 """
855 """
845
856
846 return author_email(self.author)
857 return author_email(self.author)
847
858
848 def get_file_mode(self, path):
859 def get_file_mode(self, path):
849 """
860 """
850 Returns stat mode of the file at `path`.
861 Returns stat mode of the file at `path`.
851 """
862 """
852 raise NotImplementedError
863 raise NotImplementedError
853
864
854 def is_link(self, path):
865 def is_link(self, path):
855 """
866 """
856 Returns ``True`` if given `path` is a symlink
867 Returns ``True`` if given `path` is a symlink
857 """
868 """
858 raise NotImplementedError
869 raise NotImplementedError
859
870
860 def get_file_content(self, path):
871 def get_file_content(self, path):
861 """
872 """
862 Returns content of the file at the given `path`.
873 Returns content of the file at the given `path`.
863 """
874 """
864 raise NotImplementedError
875 raise NotImplementedError
865
876
866 def get_file_size(self, path):
877 def get_file_size(self, path):
867 """
878 """
868 Returns size of the file at the given `path`.
879 Returns size of the file at the given `path`.
869 """
880 """
870 raise NotImplementedError
881 raise NotImplementedError
871
882
872 def get_file_commit(self, path, pre_load=None):
883 def get_file_commit(self, path, pre_load=None):
873 """
884 """
874 Returns last commit of the file at the given `path`.
885 Returns last commit of the file at the given `path`.
875
886
876 :param pre_load: Optional. List of commit attributes to load.
887 :param pre_load: Optional. List of commit attributes to load.
877 """
888 """
878 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
889 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
879 if not commits:
890 if not commits:
880 raise RepositoryError(
891 raise RepositoryError(
881 'Failed to fetch history for path {}. '
892 'Failed to fetch history for path {}. '
882 'Please check if such path exists in your repository'.format(
893 'Please check if such path exists in your repository'.format(
883 path))
894 path))
884 return commits[0]
895 return commits[0]
885
896
886 def get_file_history(self, path, limit=None, pre_load=None):
897 def get_file_history(self, path, limit=None, pre_load=None):
887 """
898 """
888 Returns history of file as reversed list of :class:`BaseCommit`
899 Returns history of file as reversed list of :class:`BaseCommit`
889 objects for which file at given `path` has been modified.
900 objects for which file at given `path` has been modified.
890
901
891 :param limit: Optional. Allows to limit the size of the returned
902 :param limit: Optional. Allows to limit the size of the returned
892 history. This is intended as a hint to the underlying backend, so
903 history. This is intended as a hint to the underlying backend, so
893 that it can apply optimizations depending on the limit.
904 that it can apply optimizations depending on the limit.
894 :param pre_load: Optional. List of commit attributes to load.
905 :param pre_load: Optional. List of commit attributes to load.
895 """
906 """
896 raise NotImplementedError
907 raise NotImplementedError
897
908
898 def get_file_annotate(self, path, pre_load=None):
909 def get_file_annotate(self, path, pre_load=None):
899 """
910 """
900 Returns a generator of four element tuples with
911 Returns a generator of four element tuples with
901 lineno, sha, commit lazy loader and line
912 lineno, sha, commit lazy loader and line
902
913
903 :param pre_load: Optional. List of commit attributes to load.
914 :param pre_load: Optional. List of commit attributes to load.
904 """
915 """
905 raise NotImplementedError
916 raise NotImplementedError
906
917
907 def get_nodes(self, path):
918 def get_nodes(self, path):
908 """
919 """
909 Returns combined ``DirNode`` and ``FileNode`` objects list representing
920 Returns combined ``DirNode`` and ``FileNode`` objects list representing
910 state of commit at the given ``path``.
921 state of commit at the given ``path``.
911
922
912 :raises ``CommitError``: if node at the given ``path`` is not
923 :raises ``CommitError``: if node at the given ``path`` is not
913 instance of ``DirNode``
924 instance of ``DirNode``
914 """
925 """
915 raise NotImplementedError
926 raise NotImplementedError
916
927
917 def get_node(self, path):
928 def get_node(self, path):
918 """
929 """
919 Returns ``Node`` object from the given ``path``.
930 Returns ``Node`` object from the given ``path``.
920
931
921 :raises ``NodeDoesNotExistError``: if there is no node at the given
932 :raises ``NodeDoesNotExistError``: if there is no node at the given
922 ``path``
933 ``path``
923 """
934 """
924 raise NotImplementedError
935 raise NotImplementedError
925
936
926 def get_largefile_node(self, path):
937 def get_largefile_node(self, path):
927 """
938 """
928 Returns the path to largefile from Mercurial/Git-lfs storage.
939 Returns the path to largefile from Mercurial/Git-lfs storage.
929 or None if it's not a largefile node
940 or None if it's not a largefile node
930 """
941 """
931 return None
942 return None
932
943
933 def archive_repo(self, file_path, kind='tgz', subrepos=None,
944 def archive_repo(self, file_path, kind='tgz', subrepos=None,
934 prefix=None, write_metadata=False, mtime=None):
945 prefix=None, write_metadata=False, mtime=None):
935 """
946 """
936 Creates an archive containing the contents of the repository.
947 Creates an archive containing the contents of the repository.
937
948
938 :param file_path: path to the file which to create the archive.
949 :param file_path: path to the file which to create the archive.
939 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
950 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
940 :param prefix: name of root directory in archive.
951 :param prefix: name of root directory in archive.
941 Default is repository name and commit's short_id joined with dash:
952 Default is repository name and commit's short_id joined with dash:
942 ``"{repo_name}-{short_id}"``.
953 ``"{repo_name}-{short_id}"``.
943 :param write_metadata: write a metadata file into archive.
954 :param write_metadata: write a metadata file into archive.
944 :param mtime: custom modification time for archive creation, defaults
955 :param mtime: custom modification time for archive creation, defaults
945 to time.time() if not given.
956 to time.time() if not given.
946
957
947 :raise VCSError: If prefix has a problem.
958 :raise VCSError: If prefix has a problem.
948 """
959 """
949 allowed_kinds = settings.ARCHIVE_SPECS.keys()
960 allowed_kinds = settings.ARCHIVE_SPECS.keys()
950 if kind not in allowed_kinds:
961 if kind not in allowed_kinds:
951 raise ImproperArchiveTypeError(
962 raise ImproperArchiveTypeError(
952 'Archive kind (%s) not supported use one of %s' %
963 'Archive kind (%s) not supported use one of %s' %
953 (kind, allowed_kinds))
964 (kind, allowed_kinds))
954
965
955 prefix = self._validate_archive_prefix(prefix)
966 prefix = self._validate_archive_prefix(prefix)
956
967
957 mtime = mtime or time.mktime(self.date.timetuple())
968 mtime = mtime or time.mktime(self.date.timetuple())
958
969
959 file_info = []
970 file_info = []
960 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
971 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
961 for _r, _d, files in cur_rev.walk('/'):
972 for _r, _d, files in cur_rev.walk('/'):
962 for f in files:
973 for f in files:
963 f_path = os.path.join(prefix, f.path)
974 f_path = os.path.join(prefix, f.path)
964 file_info.append(
975 file_info.append(
965 (f_path, f.mode, f.is_link(), f.raw_bytes))
976 (f_path, f.mode, f.is_link(), f.raw_bytes))
966
977
967 if write_metadata:
978 if write_metadata:
968 metadata = [
979 metadata = [
969 ('repo_name', self.repository.name),
980 ('repo_name', self.repository.name),
970 ('rev', self.raw_id),
981 ('rev', self.raw_id),
971 ('create_time', mtime),
982 ('create_time', mtime),
972 ('branch', self.branch),
983 ('branch', self.branch),
973 ('tags', ','.join(self.tags)),
984 ('tags', ','.join(self.tags)),
974 ]
985 ]
975 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
986 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
976 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
987 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
977
988
978 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
989 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
979
990
980 def _validate_archive_prefix(self, prefix):
991 def _validate_archive_prefix(self, prefix):
981 if prefix is None:
992 if prefix is None:
982 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
993 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
983 repo_name=safe_str(self.repository.name),
994 repo_name=safe_str(self.repository.name),
984 short_id=self.short_id)
995 short_id=self.short_id)
985 elif not isinstance(prefix, str):
996 elif not isinstance(prefix, str):
986 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
997 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
987 elif prefix.startswith('/'):
998 elif prefix.startswith('/'):
988 raise VCSError("Prefix cannot start with leading slash")
999 raise VCSError("Prefix cannot start with leading slash")
989 elif prefix.strip() == '':
1000 elif prefix.strip() == '':
990 raise VCSError("Prefix cannot be empty")
1001 raise VCSError("Prefix cannot be empty")
991 return prefix
1002 return prefix
992
1003
993 @LazyProperty
1004 @LazyProperty
994 def root(self):
1005 def root(self):
995 """
1006 """
996 Returns ``RootNode`` object for this commit.
1007 Returns ``RootNode`` object for this commit.
997 """
1008 """
998 return self.get_node('')
1009 return self.get_node('')
999
1010
1000 def next(self, branch=None):
1011 def next(self, branch=None):
1001 """
1012 """
1002 Returns next commit from current, if branch is gives it will return
1013 Returns next commit from current, if branch is gives it will return
1003 next commit belonging to this branch
1014 next commit belonging to this branch
1004
1015
1005 :param branch: show commits within the given named branch
1016 :param branch: show commits within the given named branch
1006 """
1017 """
1007 indexes = xrange(self.idx + 1, self.repository.count())
1018 indexes = xrange(self.idx + 1, self.repository.count())
1008 return self._find_next(indexes, branch)
1019 return self._find_next(indexes, branch)
1009
1020
1010 def prev(self, branch=None):
1021 def prev(self, branch=None):
1011 """
1022 """
1012 Returns previous commit from current, if branch is gives it will
1023 Returns previous commit from current, if branch is gives it will
1013 return previous commit belonging to this branch
1024 return previous commit belonging to this branch
1014
1025
1015 :param branch: show commit within the given named branch
1026 :param branch: show commit within the given named branch
1016 """
1027 """
1017 indexes = xrange(self.idx - 1, -1, -1)
1028 indexes = xrange(self.idx - 1, -1, -1)
1018 return self._find_next(indexes, branch)
1029 return self._find_next(indexes, branch)
1019
1030
1020 def _find_next(self, indexes, branch=None):
1031 def _find_next(self, indexes, branch=None):
1021 if branch and self.branch != branch:
1032 if branch and self.branch != branch:
1022 raise VCSError('Branch option used on commit not belonging '
1033 raise VCSError('Branch option used on commit not belonging '
1023 'to that branch')
1034 'to that branch')
1024
1035
1025 for next_idx in indexes:
1036 for next_idx in indexes:
1026 commit = self.repository.get_commit(commit_idx=next_idx)
1037 commit = self.repository.get_commit(commit_idx=next_idx)
1027 if branch and branch != commit.branch:
1038 if branch and branch != commit.branch:
1028 continue
1039 continue
1029 return commit
1040 return commit
1030 raise CommitDoesNotExistError
1041 raise CommitDoesNotExistError
1031
1042
1032 def diff(self, ignore_whitespace=True, context=3):
1043 def diff(self, ignore_whitespace=True, context=3):
1033 """
1044 """
1034 Returns a `Diff` object representing the change made by this commit.
1045 Returns a `Diff` object representing the change made by this commit.
1035 """
1046 """
1036 parent = (
1047 parent = (
1037 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1048 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1038 diff = self.repository.get_diff(
1049 diff = self.repository.get_diff(
1039 parent, self,
1050 parent, self,
1040 ignore_whitespace=ignore_whitespace,
1051 ignore_whitespace=ignore_whitespace,
1041 context=context)
1052 context=context)
1042 return diff
1053 return diff
1043
1054
1044 @LazyProperty
1055 @LazyProperty
1045 def added(self):
1056 def added(self):
1046 """
1057 """
1047 Returns list of added ``FileNode`` objects.
1058 Returns list of added ``FileNode`` objects.
1048 """
1059 """
1049 raise NotImplementedError
1060 raise NotImplementedError
1050
1061
1051 @LazyProperty
1062 @LazyProperty
1052 def changed(self):
1063 def changed(self):
1053 """
1064 """
1054 Returns list of modified ``FileNode`` objects.
1065 Returns list of modified ``FileNode`` objects.
1055 """
1066 """
1056 raise NotImplementedError
1067 raise NotImplementedError
1057
1068
1058 @LazyProperty
1069 @LazyProperty
1059 def removed(self):
1070 def removed(self):
1060 """
1071 """
1061 Returns list of removed ``FileNode`` objects.
1072 Returns list of removed ``FileNode`` objects.
1062 """
1073 """
1063 raise NotImplementedError
1074 raise NotImplementedError
1064
1075
1065 @LazyProperty
1076 @LazyProperty
1066 def size(self):
1077 def size(self):
1067 """
1078 """
1068 Returns total number of bytes from contents of all filenodes.
1079 Returns total number of bytes from contents of all filenodes.
1069 """
1080 """
1070 return sum((node.size for node in self.get_filenodes_generator()))
1081 return sum((node.size for node in self.get_filenodes_generator()))
1071
1082
1072 def walk(self, topurl=''):
1083 def walk(self, topurl=''):
1073 """
1084 """
1074 Similar to os.walk method. Insted of filesystem it walks through
1085 Similar to os.walk method. Insted of filesystem it walks through
1075 commit starting at given ``topurl``. Returns generator of tuples
1086 commit starting at given ``topurl``. Returns generator of tuples
1076 (topnode, dirnodes, filenodes).
1087 (topnode, dirnodes, filenodes).
1077 """
1088 """
1078 topnode = self.get_node(topurl)
1089 topnode = self.get_node(topurl)
1079 if not topnode.is_dir():
1090 if not topnode.is_dir():
1080 return
1091 return
1081 yield (topnode, topnode.dirs, topnode.files)
1092 yield (topnode, topnode.dirs, topnode.files)
1082 for dirnode in topnode.dirs:
1093 for dirnode in topnode.dirs:
1083 for tup in self.walk(dirnode.path):
1094 for tup in self.walk(dirnode.path):
1084 yield tup
1095 yield tup
1085
1096
1086 def get_filenodes_generator(self):
1097 def get_filenodes_generator(self):
1087 """
1098 """
1088 Returns generator that yields *all* file nodes.
1099 Returns generator that yields *all* file nodes.
1089 """
1100 """
1090 for topnode, dirs, files in self.walk():
1101 for topnode, dirs, files in self.walk():
1091 for node in files:
1102 for node in files:
1092 yield node
1103 yield node
1093
1104
1094 #
1105 #
1095 # Utilities for sub classes to support consistent behavior
1106 # Utilities for sub classes to support consistent behavior
1096 #
1107 #
1097
1108
1098 def no_node_at_path(self, path):
1109 def no_node_at_path(self, path):
1099 return NodeDoesNotExistError(
1110 return NodeDoesNotExistError(
1100 u"There is no file nor directory at the given path: "
1111 u"There is no file nor directory at the given path: "
1101 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1112 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1102
1113
1103 def _fix_path(self, path):
1114 def _fix_path(self, path):
1104 """
1115 """
1105 Paths are stored without trailing slash so we need to get rid off it if
1116 Paths are stored without trailing slash so we need to get rid off it if
1106 needed.
1117 needed.
1107 """
1118 """
1108 return path.rstrip('/')
1119 return path.rstrip('/')
1109
1120
1110 #
1121 #
1111 # Deprecated API based on changesets
1122 # Deprecated API based on changesets
1112 #
1123 #
1113
1124
1114 @property
1125 @property
1115 def revision(self):
1126 def revision(self):
1116 warnings.warn("Use idx instead", DeprecationWarning)
1127 warnings.warn("Use idx instead", DeprecationWarning)
1117 return self.idx
1128 return self.idx
1118
1129
1119 @revision.setter
1130 @revision.setter
1120 def revision(self, value):
1131 def revision(self, value):
1121 warnings.warn("Use idx instead", DeprecationWarning)
1132 warnings.warn("Use idx instead", DeprecationWarning)
1122 self.idx = value
1133 self.idx = value
1123
1134
1124 def get_file_changeset(self, path):
1135 def get_file_changeset(self, path):
1125 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1136 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1126 return self.get_file_commit(path)
1137 return self.get_file_commit(path)
1127
1138
1128
1139
1129 class BaseChangesetClass(type):
1140 class BaseChangesetClass(type):
1130
1141
1131 def __instancecheck__(self, instance):
1142 def __instancecheck__(self, instance):
1132 return isinstance(instance, BaseCommit)
1143 return isinstance(instance, BaseCommit)
1133
1144
1134
1145
1135 class BaseChangeset(BaseCommit):
1146 class BaseChangeset(BaseCommit):
1136
1147
1137 __metaclass__ = BaseChangesetClass
1148 __metaclass__ = BaseChangesetClass
1138
1149
1139 def __new__(cls, *args, **kwargs):
1150 def __new__(cls, *args, **kwargs):
1140 warnings.warn(
1151 warnings.warn(
1141 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1152 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1142 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1153 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1143
1154
1144
1155
1145 class BaseInMemoryCommit(object):
1156 class BaseInMemoryCommit(object):
1146 """
1157 """
1147 Represents differences between repository's state (most recent head) and
1158 Represents differences between repository's state (most recent head) and
1148 changes made *in place*.
1159 changes made *in place*.
1149
1160
1150 **Attributes**
1161 **Attributes**
1151
1162
1152 ``repository``
1163 ``repository``
1153 repository object for this in-memory-commit
1164 repository object for this in-memory-commit
1154
1165
1155 ``added``
1166 ``added``
1156 list of ``FileNode`` objects marked as *added*
1167 list of ``FileNode`` objects marked as *added*
1157
1168
1158 ``changed``
1169 ``changed``
1159 list of ``FileNode`` objects marked as *changed*
1170 list of ``FileNode`` objects marked as *changed*
1160
1171
1161 ``removed``
1172 ``removed``
1162 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1173 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1163 *removed*
1174 *removed*
1164
1175
1165 ``parents``
1176 ``parents``
1166 list of :class:`BaseCommit` instances representing parents of
1177 list of :class:`BaseCommit` instances representing parents of
1167 in-memory commit. Should always be 2-element sequence.
1178 in-memory commit. Should always be 2-element sequence.
1168
1179
1169 """
1180 """
1170
1181
1171 def __init__(self, repository):
1182 def __init__(self, repository):
1172 self.repository = repository
1183 self.repository = repository
1173 self.added = []
1184 self.added = []
1174 self.changed = []
1185 self.changed = []
1175 self.removed = []
1186 self.removed = []
1176 self.parents = []
1187 self.parents = []
1177
1188
1178 def add(self, *filenodes):
1189 def add(self, *filenodes):
1179 """
1190 """
1180 Marks given ``FileNode`` objects as *to be committed*.
1191 Marks given ``FileNode`` objects as *to be committed*.
1181
1192
1182 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1193 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1183 latest commit
1194 latest commit
1184 :raises ``NodeAlreadyAddedError``: if node with same path is already
1195 :raises ``NodeAlreadyAddedError``: if node with same path is already
1185 marked as *added*
1196 marked as *added*
1186 """
1197 """
1187 # Check if not already marked as *added* first
1198 # Check if not already marked as *added* first
1188 for node in filenodes:
1199 for node in filenodes:
1189 if node.path in (n.path for n in self.added):
1200 if node.path in (n.path for n in self.added):
1190 raise NodeAlreadyAddedError(
1201 raise NodeAlreadyAddedError(
1191 "Such FileNode %s is already marked for addition"
1202 "Such FileNode %s is already marked for addition"
1192 % node.path)
1203 % node.path)
1193 for node in filenodes:
1204 for node in filenodes:
1194 self.added.append(node)
1205 self.added.append(node)
1195
1206
1196 def change(self, *filenodes):
1207 def change(self, *filenodes):
1197 """
1208 """
1198 Marks given ``FileNode`` objects to be *changed* in next commit.
1209 Marks given ``FileNode`` objects to be *changed* in next commit.
1199
1210
1200 :raises ``EmptyRepositoryError``: if there are no commits yet
1211 :raises ``EmptyRepositoryError``: if there are no commits yet
1201 :raises ``NodeAlreadyExistsError``: if node with same path is already
1212 :raises ``NodeAlreadyExistsError``: if node with same path is already
1202 marked to be *changed*
1213 marked to be *changed*
1203 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1214 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1204 marked to be *removed*
1215 marked to be *removed*
1205 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1216 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1206 commit
1217 commit
1207 :raises ``NodeNotChangedError``: if node hasn't really be changed
1218 :raises ``NodeNotChangedError``: if node hasn't really be changed
1208 """
1219 """
1209 for node in filenodes:
1220 for node in filenodes:
1210 if node.path in (n.path for n in self.removed):
1221 if node.path in (n.path for n in self.removed):
1211 raise NodeAlreadyRemovedError(
1222 raise NodeAlreadyRemovedError(
1212 "Node at %s is already marked as removed" % node.path)
1223 "Node at %s is already marked as removed" % node.path)
1213 try:
1224 try:
1214 self.repository.get_commit()
1225 self.repository.get_commit()
1215 except EmptyRepositoryError:
1226 except EmptyRepositoryError:
1216 raise EmptyRepositoryError(
1227 raise EmptyRepositoryError(
1217 "Nothing to change - try to *add* new nodes rather than "
1228 "Nothing to change - try to *add* new nodes rather than "
1218 "changing them")
1229 "changing them")
1219 for node in filenodes:
1230 for node in filenodes:
1220 if node.path in (n.path for n in self.changed):
1231 if node.path in (n.path for n in self.changed):
1221 raise NodeAlreadyChangedError(
1232 raise NodeAlreadyChangedError(
1222 "Node at '%s' is already marked as changed" % node.path)
1233 "Node at '%s' is already marked as changed" % node.path)
1223 self.changed.append(node)
1234 self.changed.append(node)
1224
1235
1225 def remove(self, *filenodes):
1236 def remove(self, *filenodes):
1226 """
1237 """
1227 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1238 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1228 *removed* in next commit.
1239 *removed* in next commit.
1229
1240
1230 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1241 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1231 be *removed*
1242 be *removed*
1232 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1243 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1233 be *changed*
1244 be *changed*
1234 """
1245 """
1235 for node in filenodes:
1246 for node in filenodes:
1236 if node.path in (n.path for n in self.removed):
1247 if node.path in (n.path for n in self.removed):
1237 raise NodeAlreadyRemovedError(
1248 raise NodeAlreadyRemovedError(
1238 "Node is already marked to for removal at %s" % node.path)
1249 "Node is already marked to for removal at %s" % node.path)
1239 if node.path in (n.path for n in self.changed):
1250 if node.path in (n.path for n in self.changed):
1240 raise NodeAlreadyChangedError(
1251 raise NodeAlreadyChangedError(
1241 "Node is already marked to be changed at %s" % node.path)
1252 "Node is already marked to be changed at %s" % node.path)
1242 # We only mark node as *removed* - real removal is done by
1253 # We only mark node as *removed* - real removal is done by
1243 # commit method
1254 # commit method
1244 self.removed.append(node)
1255 self.removed.append(node)
1245
1256
1246 def reset(self):
1257 def reset(self):
1247 """
1258 """
1248 Resets this instance to initial state (cleans ``added``, ``changed``
1259 Resets this instance to initial state (cleans ``added``, ``changed``
1249 and ``removed`` lists).
1260 and ``removed`` lists).
1250 """
1261 """
1251 self.added = []
1262 self.added = []
1252 self.changed = []
1263 self.changed = []
1253 self.removed = []
1264 self.removed = []
1254 self.parents = []
1265 self.parents = []
1255
1266
1256 def get_ipaths(self):
1267 def get_ipaths(self):
1257 """
1268 """
1258 Returns generator of paths from nodes marked as added, changed or
1269 Returns generator of paths from nodes marked as added, changed or
1259 removed.
1270 removed.
1260 """
1271 """
1261 for node in itertools.chain(self.added, self.changed, self.removed):
1272 for node in itertools.chain(self.added, self.changed, self.removed):
1262 yield node.path
1273 yield node.path
1263
1274
1264 def get_paths(self):
1275 def get_paths(self):
1265 """
1276 """
1266 Returns list of paths from nodes marked as added, changed or removed.
1277 Returns list of paths from nodes marked as added, changed or removed.
1267 """
1278 """
1268 return list(self.get_ipaths())
1279 return list(self.get_ipaths())
1269
1280
1270 def check_integrity(self, parents=None):
1281 def check_integrity(self, parents=None):
1271 """
1282 """
1272 Checks in-memory commit's integrity. Also, sets parents if not
1283 Checks in-memory commit's integrity. Also, sets parents if not
1273 already set.
1284 already set.
1274
1285
1275 :raises CommitError: if any error occurs (i.e.
1286 :raises CommitError: if any error occurs (i.e.
1276 ``NodeDoesNotExistError``).
1287 ``NodeDoesNotExistError``).
1277 """
1288 """
1278 if not self.parents:
1289 if not self.parents:
1279 parents = parents or []
1290 parents = parents or []
1280 if len(parents) == 0:
1291 if len(parents) == 0:
1281 try:
1292 try:
1282 parents = [self.repository.get_commit(), None]
1293 parents = [self.repository.get_commit(), None]
1283 except EmptyRepositoryError:
1294 except EmptyRepositoryError:
1284 parents = [None, None]
1295 parents = [None, None]
1285 elif len(parents) == 1:
1296 elif len(parents) == 1:
1286 parents += [None]
1297 parents += [None]
1287 self.parents = parents
1298 self.parents = parents
1288
1299
1289 # Local parents, only if not None
1300 # Local parents, only if not None
1290 parents = [p for p in self.parents if p]
1301 parents = [p for p in self.parents if p]
1291
1302
1292 # Check nodes marked as added
1303 # Check nodes marked as added
1293 for p in parents:
1304 for p in parents:
1294 for node in self.added:
1305 for node in self.added:
1295 try:
1306 try:
1296 p.get_node(node.path)
1307 p.get_node(node.path)
1297 except NodeDoesNotExistError:
1308 except NodeDoesNotExistError:
1298 pass
1309 pass
1299 else:
1310 else:
1300 raise NodeAlreadyExistsError(
1311 raise NodeAlreadyExistsError(
1301 "Node `%s` already exists at %s" % (node.path, p))
1312 "Node `%s` already exists at %s" % (node.path, p))
1302
1313
1303 # Check nodes marked as changed
1314 # Check nodes marked as changed
1304 missing = set(self.changed)
1315 missing = set(self.changed)
1305 not_changed = set(self.changed)
1316 not_changed = set(self.changed)
1306 if self.changed and not parents:
1317 if self.changed and not parents:
1307 raise NodeDoesNotExistError(str(self.changed[0].path))
1318 raise NodeDoesNotExistError(str(self.changed[0].path))
1308 for p in parents:
1319 for p in parents:
1309 for node in self.changed:
1320 for node in self.changed:
1310 try:
1321 try:
1311 old = p.get_node(node.path)
1322 old = p.get_node(node.path)
1312 missing.remove(node)
1323 missing.remove(node)
1313 # if content actually changed, remove node from not_changed
1324 # if content actually changed, remove node from not_changed
1314 if old.content != node.content:
1325 if old.content != node.content:
1315 not_changed.remove(node)
1326 not_changed.remove(node)
1316 except NodeDoesNotExistError:
1327 except NodeDoesNotExistError:
1317 pass
1328 pass
1318 if self.changed and missing:
1329 if self.changed and missing:
1319 raise NodeDoesNotExistError(
1330 raise NodeDoesNotExistError(
1320 "Node `%s` marked as modified but missing in parents: %s"
1331 "Node `%s` marked as modified but missing in parents: %s"
1321 % (node.path, parents))
1332 % (node.path, parents))
1322
1333
1323 if self.changed and not_changed:
1334 if self.changed and not_changed:
1324 raise NodeNotChangedError(
1335 raise NodeNotChangedError(
1325 "Node `%s` wasn't actually changed (parents: %s)"
1336 "Node `%s` wasn't actually changed (parents: %s)"
1326 % (not_changed.pop().path, parents))
1337 % (not_changed.pop().path, parents))
1327
1338
1328 # Check nodes marked as removed
1339 # Check nodes marked as removed
1329 if self.removed and not parents:
1340 if self.removed and not parents:
1330 raise NodeDoesNotExistError(
1341 raise NodeDoesNotExistError(
1331 "Cannot remove node at %s as there "
1342 "Cannot remove node at %s as there "
1332 "were no parents specified" % self.removed[0].path)
1343 "were no parents specified" % self.removed[0].path)
1333 really_removed = set()
1344 really_removed = set()
1334 for p in parents:
1345 for p in parents:
1335 for node in self.removed:
1346 for node in self.removed:
1336 try:
1347 try:
1337 p.get_node(node.path)
1348 p.get_node(node.path)
1338 really_removed.add(node)
1349 really_removed.add(node)
1339 except CommitError:
1350 except CommitError:
1340 pass
1351 pass
1341 not_removed = set(self.removed) - really_removed
1352 not_removed = set(self.removed) - really_removed
1342 if not_removed:
1353 if not_removed:
1343 # TODO: johbo: This code branch does not seem to be covered
1354 # TODO: johbo: This code branch does not seem to be covered
1344 raise NodeDoesNotExistError(
1355 raise NodeDoesNotExistError(
1345 "Cannot remove node at %s from "
1356 "Cannot remove node at %s from "
1346 "following parents: %s" % (not_removed, parents))
1357 "following parents: %s" % (not_removed, parents))
1347
1358
1348 def commit(
1359 def commit(
1349 self, message, author, parents=None, branch=None, date=None,
1360 self, message, author, parents=None, branch=None, date=None,
1350 **kwargs):
1361 **kwargs):
1351 """
1362 """
1352 Performs in-memory commit (doesn't check workdir in any way) and
1363 Performs in-memory commit (doesn't check workdir in any way) and
1353 returns newly created :class:`BaseCommit`. Updates repository's
1364 returns newly created :class:`BaseCommit`. Updates repository's
1354 attribute `commits`.
1365 attribute `commits`.
1355
1366
1356 .. note::
1367 .. note::
1357
1368
1358 While overriding this method each backend's should call
1369 While overriding this method each backend's should call
1359 ``self.check_integrity(parents)`` in the first place.
1370 ``self.check_integrity(parents)`` in the first place.
1360
1371
1361 :param message: message of the commit
1372 :param message: message of the commit
1362 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1373 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1363 :param parents: single parent or sequence of parents from which commit
1374 :param parents: single parent or sequence of parents from which commit
1364 would be derived
1375 would be derived
1365 :param date: ``datetime.datetime`` instance. Defaults to
1376 :param date: ``datetime.datetime`` instance. Defaults to
1366 ``datetime.datetime.now()``.
1377 ``datetime.datetime.now()``.
1367 :param branch: branch name, as string. If none given, default backend's
1378 :param branch: branch name, as string. If none given, default backend's
1368 branch would be used.
1379 branch would be used.
1369
1380
1370 :raises ``CommitError``: if any error occurs while committing
1381 :raises ``CommitError``: if any error occurs while committing
1371 """
1382 """
1372 raise NotImplementedError
1383 raise NotImplementedError
1373
1384
1374
1385
1375 class BaseInMemoryChangesetClass(type):
1386 class BaseInMemoryChangesetClass(type):
1376
1387
1377 def __instancecheck__(self, instance):
1388 def __instancecheck__(self, instance):
1378 return isinstance(instance, BaseInMemoryCommit)
1389 return isinstance(instance, BaseInMemoryCommit)
1379
1390
1380
1391
1381 class BaseInMemoryChangeset(BaseInMemoryCommit):
1392 class BaseInMemoryChangeset(BaseInMemoryCommit):
1382
1393
1383 __metaclass__ = BaseInMemoryChangesetClass
1394 __metaclass__ = BaseInMemoryChangesetClass
1384
1395
1385 def __new__(cls, *args, **kwargs):
1396 def __new__(cls, *args, **kwargs):
1386 warnings.warn(
1397 warnings.warn(
1387 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1398 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1388 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1399 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1389
1400
1390
1401
1391 class EmptyCommit(BaseCommit):
1402 class EmptyCommit(BaseCommit):
1392 """
1403 """
1393 An dummy empty commit. It's possible to pass hash when creating
1404 An dummy empty commit. It's possible to pass hash when creating
1394 an EmptyCommit
1405 an EmptyCommit
1395 """
1406 """
1396
1407
1397 def __init__(
1408 def __init__(
1398 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1409 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1399 message='', author='', date=None):
1410 message='', author='', date=None):
1400 self._empty_commit_id = commit_id
1411 self._empty_commit_id = commit_id
1401 # TODO: johbo: Solve idx parameter, default value does not make
1412 # TODO: johbo: Solve idx parameter, default value does not make
1402 # too much sense
1413 # too much sense
1403 self.idx = idx
1414 self.idx = idx
1404 self.message = message
1415 self.message = message
1405 self.author = author
1416 self.author = author
1406 self.date = date or datetime.datetime.fromtimestamp(0)
1417 self.date = date or datetime.datetime.fromtimestamp(0)
1407 self.repository = repo
1418 self.repository = repo
1408 self.alias = alias
1419 self.alias = alias
1409
1420
1410 @LazyProperty
1421 @LazyProperty
1411 def raw_id(self):
1422 def raw_id(self):
1412 """
1423 """
1413 Returns raw string identifying this commit, useful for web
1424 Returns raw string identifying this commit, useful for web
1414 representation.
1425 representation.
1415 """
1426 """
1416
1427
1417 return self._empty_commit_id
1428 return self._empty_commit_id
1418
1429
1419 @LazyProperty
1430 @LazyProperty
1420 def branch(self):
1431 def branch(self):
1421 if self.alias:
1432 if self.alias:
1422 from rhodecode.lib.vcs.backends import get_backend
1433 from rhodecode.lib.vcs.backends import get_backend
1423 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1434 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1424
1435
1425 @LazyProperty
1436 @LazyProperty
1426 def short_id(self):
1437 def short_id(self):
1427 return self.raw_id[:12]
1438 return self.raw_id[:12]
1428
1439
1429 @LazyProperty
1440 @LazyProperty
1430 def id(self):
1441 def id(self):
1431 return self.raw_id
1442 return self.raw_id
1432
1443
1433 def get_file_commit(self, path):
1444 def get_file_commit(self, path):
1434 return self
1445 return self
1435
1446
1436 def get_file_content(self, path):
1447 def get_file_content(self, path):
1437 return u''
1448 return u''
1438
1449
1439 def get_file_size(self, path):
1450 def get_file_size(self, path):
1440 return 0
1451 return 0
1441
1452
1442
1453
1443 class EmptyChangesetClass(type):
1454 class EmptyChangesetClass(type):
1444
1455
1445 def __instancecheck__(self, instance):
1456 def __instancecheck__(self, instance):
1446 return isinstance(instance, EmptyCommit)
1457 return isinstance(instance, EmptyCommit)
1447
1458
1448
1459
1449 class EmptyChangeset(EmptyCommit):
1460 class EmptyChangeset(EmptyCommit):
1450
1461
1451 __metaclass__ = EmptyChangesetClass
1462 __metaclass__ = EmptyChangesetClass
1452
1463
1453 def __new__(cls, *args, **kwargs):
1464 def __new__(cls, *args, **kwargs):
1454 warnings.warn(
1465 warnings.warn(
1455 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1466 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1456 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1467 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1457
1468
1458 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1469 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1459 alias=None, revision=-1, message='', author='', date=None):
1470 alias=None, revision=-1, message='', author='', date=None):
1460 if requested_revision is not None:
1471 if requested_revision is not None:
1461 warnings.warn(
1472 warnings.warn(
1462 "Parameter requested_revision not supported anymore",
1473 "Parameter requested_revision not supported anymore",
1463 DeprecationWarning)
1474 DeprecationWarning)
1464 super(EmptyChangeset, self).__init__(
1475 super(EmptyChangeset, self).__init__(
1465 commit_id=cs, repo=repo, alias=alias, idx=revision,
1476 commit_id=cs, repo=repo, alias=alias, idx=revision,
1466 message=message, author=author, date=date)
1477 message=message, author=author, date=date)
1467
1478
1468 @property
1479 @property
1469 def revision(self):
1480 def revision(self):
1470 warnings.warn("Use idx instead", DeprecationWarning)
1481 warnings.warn("Use idx instead", DeprecationWarning)
1471 return self.idx
1482 return self.idx
1472
1483
1473 @revision.setter
1484 @revision.setter
1474 def revision(self, value):
1485 def revision(self, value):
1475 warnings.warn("Use idx instead", DeprecationWarning)
1486 warnings.warn("Use idx instead", DeprecationWarning)
1476 self.idx = value
1487 self.idx = value
1477
1488
1478
1489
1479 class EmptyRepository(BaseRepository):
1490 class EmptyRepository(BaseRepository):
1480 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1491 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1481 pass
1492 pass
1482
1493
1483 def get_diff(self, *args, **kwargs):
1494 def get_diff(self, *args, **kwargs):
1484 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1495 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1485 return GitDiff('')
1496 return GitDiff('')
1486
1497
1487
1498
1488 class CollectionGenerator(object):
1499 class CollectionGenerator(object):
1489
1500
1490 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1501 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1491 self.repo = repo
1502 self.repo = repo
1492 self.commit_ids = commit_ids
1503 self.commit_ids = commit_ids
1493 # TODO: (oliver) this isn't currently hooked up
1504 # TODO: (oliver) this isn't currently hooked up
1494 self.collection_size = None
1505 self.collection_size = None
1495 self.pre_load = pre_load
1506 self.pre_load = pre_load
1496
1507
1497 def __len__(self):
1508 def __len__(self):
1498 if self.collection_size is not None:
1509 if self.collection_size is not None:
1499 return self.collection_size
1510 return self.collection_size
1500 return self.commit_ids.__len__()
1511 return self.commit_ids.__len__()
1501
1512
1502 def __iter__(self):
1513 def __iter__(self):
1503 for commit_id in self.commit_ids:
1514 for commit_id in self.commit_ids:
1504 # TODO: johbo: Mercurial passes in commit indices or commit ids
1515 # TODO: johbo: Mercurial passes in commit indices or commit ids
1505 yield self._commit_factory(commit_id)
1516 yield self._commit_factory(commit_id)
1506
1517
1507 def _commit_factory(self, commit_id):
1518 def _commit_factory(self, commit_id):
1508 """
1519 """
1509 Allows backends to override the way commits are generated.
1520 Allows backends to override the way commits are generated.
1510 """
1521 """
1511 return self.repo.get_commit(commit_id=commit_id,
1522 return self.repo.get_commit(commit_id=commit_id,
1512 pre_load=self.pre_load)
1523 pre_load=self.pre_load)
1513
1524
1514 def __getslice__(self, i, j):
1525 def __getslice__(self, i, j):
1515 """
1526 """
1516 Returns an iterator of sliced repository
1527 Returns an iterator of sliced repository
1517 """
1528 """
1518 commit_ids = self.commit_ids[i:j]
1529 commit_ids = self.commit_ids[i:j]
1519 return self.__class__(
1530 return self.__class__(
1520 self.repo, commit_ids, pre_load=self.pre_load)
1531 self.repo, commit_ids, pre_load=self.pre_load)
1521
1532
1522 def __repr__(self):
1533 def __repr__(self):
1523 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1534 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1524
1535
1525
1536
1526 class Config(object):
1537 class Config(object):
1527 """
1538 """
1528 Represents the configuration for a repository.
1539 Represents the configuration for a repository.
1529
1540
1530 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1541 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1531 standard library. It implements only the needed subset.
1542 standard library. It implements only the needed subset.
1532 """
1543 """
1533
1544
1534 def __init__(self):
1545 def __init__(self):
1535 self._values = {}
1546 self._values = {}
1536
1547
1537 def copy(self):
1548 def copy(self):
1538 clone = Config()
1549 clone = Config()
1539 for section, values in self._values.items():
1550 for section, values in self._values.items():
1540 clone._values[section] = values.copy()
1551 clone._values[section] = values.copy()
1541 return clone
1552 return clone
1542
1553
1543 def __repr__(self):
1554 def __repr__(self):
1544 return '<Config(%s sections) at %s>' % (
1555 return '<Config(%s sections) at %s>' % (
1545 len(self._values), hex(id(self)))
1556 len(self._values), hex(id(self)))
1546
1557
1547 def items(self, section):
1558 def items(self, section):
1548 return self._values.get(section, {}).iteritems()
1559 return self._values.get(section, {}).iteritems()
1549
1560
1550 def get(self, section, option):
1561 def get(self, section, option):
1551 return self._values.get(section, {}).get(option)
1562 return self._values.get(section, {}).get(option)
1552
1563
1553 def set(self, section, option, value):
1564 def set(self, section, option, value):
1554 section_values = self._values.setdefault(section, {})
1565 section_values = self._values.setdefault(section, {})
1555 section_values[option] = value
1566 section_values[option] = value
1556
1567
1557 def clear_section(self, section):
1568 def clear_section(self, section):
1558 self._values[section] = {}
1569 self._values[section] = {}
1559
1570
1560 def serialize(self):
1571 def serialize(self):
1561 """
1572 """
1562 Creates a list of three tuples (section, key, value) representing
1573 Creates a list of three tuples (section, key, value) representing
1563 this config object.
1574 this config object.
1564 """
1575 """
1565 items = []
1576 items = []
1566 for section in self._values:
1577 for section in self._values:
1567 for option, value in self._values[section].items():
1578 for option, value in self._values[section].items():
1568 items.append(
1579 items.append(
1569 (safe_str(section), safe_str(option), safe_str(value)))
1580 (safe_str(section), safe_str(option), safe_str(value)))
1570 return items
1581 return items
1571
1582
1572
1583
1573 class Diff(object):
1584 class Diff(object):
1574 """
1585 """
1575 Represents a diff result from a repository backend.
1586 Represents a diff result from a repository backend.
1576
1587
1577 Subclasses have to provide a backend specific value for
1588 Subclasses have to provide a backend specific value for
1578 :attr:`_header_re` and :attr:`_meta_re`.
1589 :attr:`_header_re` and :attr:`_meta_re`.
1579 """
1590 """
1580 _meta_re = None
1591 _meta_re = None
1581 _header_re = None
1592 _header_re = None
1582
1593
1583 def __init__(self, raw_diff):
1594 def __init__(self, raw_diff):
1584 self.raw = raw_diff
1595 self.raw = raw_diff
1585
1596
1586 def chunks(self):
1597 def chunks(self):
1587 """
1598 """
1588 split the diff in chunks of separate --git a/file b/file chunks
1599 split the diff in chunks of separate --git a/file b/file chunks
1589 to make diffs consistent we must prepend with \n, and make sure
1600 to make diffs consistent we must prepend with \n, and make sure
1590 we can detect last chunk as this was also has special rule
1601 we can detect last chunk as this was also has special rule
1591 """
1602 """
1592
1603
1593 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1604 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1594 header = diff_parts[0]
1605 header = diff_parts[0]
1595
1606
1596 if self._meta_re:
1607 if self._meta_re:
1597 match = self._meta_re.match(header)
1608 match = self._meta_re.match(header)
1598
1609
1599 chunks = diff_parts[1:]
1610 chunks = diff_parts[1:]
1600 total_chunks = len(chunks)
1611 total_chunks = len(chunks)
1601
1612
1602 return (
1613 return (
1603 DiffChunk(chunk, self, cur_chunk == total_chunks)
1614 DiffChunk(chunk, self, cur_chunk == total_chunks)
1604 for cur_chunk, chunk in enumerate(chunks, start=1))
1615 for cur_chunk, chunk in enumerate(chunks, start=1))
1605
1616
1606
1617
1607 class DiffChunk(object):
1618 class DiffChunk(object):
1608
1619
1609 def __init__(self, chunk, diff, last_chunk):
1620 def __init__(self, chunk, diff, last_chunk):
1610 self._diff = diff
1621 self._diff = diff
1611
1622
1612 # since we split by \ndiff --git that part is lost from original diff
1623 # since we split by \ndiff --git that part is lost from original diff
1613 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1624 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1614 if not last_chunk:
1625 if not last_chunk:
1615 chunk += '\n'
1626 chunk += '\n'
1616
1627
1617 match = self._diff._header_re.match(chunk)
1628 match = self._diff._header_re.match(chunk)
1618 self.header = match.groupdict()
1629 self.header = match.groupdict()
1619 self.diff = chunk[match.end():]
1630 self.diff = chunk[match.end():]
1620 self.raw = chunk
1631 self.raw = chunk
1632
1633
1634 class BasePathPermissionChecker(object):
1635
1636 def __init__(self, username, has_full_access = False):
1637 self.username = username
1638 self.has_full_access = has_full_access
1639
1640 def has_access(self, path):
1641 raise NotImplemented()
@@ -1,693 +1,695 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(filename, line, type)"><%
3 <%def name="diff_line_anchor(filename, line, type)"><%
4 return '%s_%s_%i' % (h.safeid(filename), type, line)
4 return '%s_%s_%i' % (h.safeid(filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None
47 deleted_files_comments=None
48
48
49 )">
49 )">
50
50
51 %if use_comments:
51 %if use_comments:
52 <div id="cb-comments-inline-container-template" class="js-template">
52 <div id="cb-comments-inline-container-template" class="js-template">
53 ${inline_comments_container([])}
53 ${inline_comments_container([])}
54 </div>
54 </div>
55 <div class="js-template" id="cb-comment-inline-form-template">
55 <div class="js-template" id="cb-comment-inline-form-template">
56 <div class="comment-inline-form ac">
56 <div class="comment-inline-form ac">
57
57
58 %if c.rhodecode_user.username != h.DEFAULT_USER:
58 %if c.rhodecode_user.username != h.DEFAULT_USER:
59 ## render template for inline comments
59 ## render template for inline comments
60 ${commentblock.comment_form(form_type='inline')}
60 ${commentblock.comment_form(form_type='inline')}
61 %else:
61 %else:
62 ${h.form('', class_='inline-form comment-form-login', method='get')}
62 ${h.form('', class_='inline-form comment-form-login', method='get')}
63 <div class="pull-left">
63 <div class="pull-left">
64 <div class="comment-help pull-right">
64 <div class="comment-help pull-right">
65 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
65 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
66 </div>
66 </div>
67 </div>
67 </div>
68 <div class="comment-button pull-right">
68 <div class="comment-button pull-right">
69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
69 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
70 ${_('Cancel')}
70 ${_('Cancel')}
71 </button>
71 </button>
72 </div>
72 </div>
73 <div class="clearfix"></div>
73 <div class="clearfix"></div>
74 ${h.end_form()}
74 ${h.end_form()}
75 %endif
75 %endif
76 </div>
76 </div>
77 </div>
77 </div>
78
78
79 %endif
79 %endif
80 <%
80 <%
81 collapse_all = len(diffset.files) > collapse_when_files_over
81 collapse_all = len(diffset.files) > collapse_when_files_over
82 %>
82 %>
83
83
84 %if c.diffmode == 'sideside':
84 %if c.diffmode == 'sideside':
85 <style>
85 <style>
86 .wrapper {
86 .wrapper {
87 max-width: 1600px !important;
87 max-width: 1600px !important;
88 }
88 }
89 </style>
89 </style>
90 %endif
90 %endif
91
91
92 %if ruler_at_chars:
92 %if ruler_at_chars:
93 <style>
93 <style>
94 .diff table.cb .cb-content:after {
94 .diff table.cb .cb-content:after {
95 content: "";
95 content: "";
96 border-left: 1px solid blue;
96 border-left: 1px solid blue;
97 position: absolute;
97 position: absolute;
98 top: 0;
98 top: 0;
99 height: 18px;
99 height: 18px;
100 opacity: .2;
100 opacity: .2;
101 z-index: 10;
101 z-index: 10;
102 //## +5 to account for diff action (+/-)
102 //## +5 to account for diff action (+/-)
103 left: ${ruler_at_chars + 5}ch;
103 left: ${ruler_at_chars + 5}ch;
104 </style>
104 </style>
105 %endif
105 %endif
106
106
107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
107 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
108 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
109 %if commit:
109 %if commit:
110 <div class="pull-right">
110 <div class="pull-right">
111 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
111 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
112 ${_('Browse Files')}
112 ${_('Browse Files')}
113 </a>
113 </a>
114 </div>
114 </div>
115 %endif
115 %endif
116 <h2 class="clearinner">
116 <h2 class="clearinner">
117 %if commit:
117 %if commit:
118 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
118 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
119 ${h.age_component(commit.date)} -
119 ${h.age_component(commit.date)} -
120 %endif
120 %endif
121
121
122 %if diffset.limited_diff:
122 %if diffset.limited_diff:
123 ${_('The requested commit is too big and content was truncated.')}
123 ${_('The requested commit is too big and content was truncated.')}
124
124
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 %else:
127 %else:
128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
128 ${_ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
129 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
129 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
130 %endif
130 %endif
131
131
132 </h2>
132 </h2>
133 </div>
133 </div>
134
134
135 %if not diffset.files:
135 %if diffset.has_hidden_changes:
136 <p class="empty_data">${_('Some changes may be hidden')}</p>
137 %elif not diffset.files:
136 <p class="empty_data">${_('No files')}</p>
138 <p class="empty_data">${_('No files')}</p>
137 %endif
139 %endif
138
140
139 <div class="filediffs">
141 <div class="filediffs">
140 ## initial value could be marked as False later on
142 ## initial value could be marked as False later on
141 <% over_lines_changed_limit = False %>
143 <% over_lines_changed_limit = False %>
142 %for i, filediff in enumerate(diffset.files):
144 %for i, filediff in enumerate(diffset.files):
143
145
144 <%
146 <%
145 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
147 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
146 over_lines_changed_limit = lines_changed > lines_changed_limit
148 over_lines_changed_limit = lines_changed > lines_changed_limit
147 %>
149 %>
148 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
150 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
149 <div
151 <div
150 class="filediff"
152 class="filediff"
151 data-f-path="${filediff.patch['filename']}"
153 data-f-path="${filediff.patch['filename']}"
152 id="a_${h.FID('', filediff.patch['filename'])}">
154 id="a_${h.FID('', filediff.patch['filename'])}">
153 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
155 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
154 <div class="filediff-collapse-indicator"></div>
156 <div class="filediff-collapse-indicator"></div>
155 ${diff_ops(filediff)}
157 ${diff_ops(filediff)}
156 </label>
158 </label>
157 ${diff_menu(filediff, use_comments=use_comments)}
159 ${diff_menu(filediff, use_comments=use_comments)}
158 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
160 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
159 %if not filediff.hunks:
161 %if not filediff.hunks:
160 %for op_id, op_text in filediff.patch['stats']['ops'].items():
162 %for op_id, op_text in filediff.patch['stats']['ops'].items():
161 <tr>
163 <tr>
162 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
164 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
163 %if op_id == DEL_FILENODE:
165 %if op_id == DEL_FILENODE:
164 ${_('File was deleted')}
166 ${_('File was deleted')}
165 %elif op_id == BIN_FILENODE:
167 %elif op_id == BIN_FILENODE:
166 ${_('Binary file hidden')}
168 ${_('Binary file hidden')}
167 %else:
169 %else:
168 ${op_text}
170 ${op_text}
169 %endif
171 %endif
170 </td>
172 </td>
171 </tr>
173 </tr>
172 %endfor
174 %endfor
173 %endif
175 %endif
174 %if filediff.limited_diff:
176 %if filediff.limited_diff:
175 <tr class="cb-warning cb-collapser">
177 <tr class="cb-warning cb-collapser">
176 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
178 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
177 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
179 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
178 </td>
180 </td>
179 </tr>
181 </tr>
180 %else:
182 %else:
181 %if over_lines_changed_limit:
183 %if over_lines_changed_limit:
182 <tr class="cb-warning cb-collapser">
184 <tr class="cb-warning cb-collapser">
183 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
185 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
184 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
186 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
185 <a href="#" class="cb-expand"
187 <a href="#" class="cb-expand"
186 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
188 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
187 </a>
189 </a>
188 <a href="#" class="cb-collapse"
190 <a href="#" class="cb-collapse"
189 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
191 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
190 </a>
192 </a>
191 </td>
193 </td>
192 </tr>
194 </tr>
193 %endif
195 %endif
194 %endif
196 %endif
195
197
196 %for hunk in filediff.hunks:
198 %for hunk in filediff.hunks:
197 <tr class="cb-hunk">
199 <tr class="cb-hunk">
198 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
200 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
199 ## TODO: dan: add ajax loading of more context here
201 ## TODO: dan: add ajax loading of more context here
200 ## <a href="#">
202 ## <a href="#">
201 <i class="icon-more"></i>
203 <i class="icon-more"></i>
202 ## </a>
204 ## </a>
203 </td>
205 </td>
204 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
206 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
205 @@
207 @@
206 -${hunk.source_start},${hunk.source_length}
208 -${hunk.source_start},${hunk.source_length}
207 +${hunk.target_start},${hunk.target_length}
209 +${hunk.target_start},${hunk.target_length}
208 ${hunk.section_header}
210 ${hunk.section_header}
209 </td>
211 </td>
210 </tr>
212 </tr>
211 %if c.diffmode == 'unified':
213 %if c.diffmode == 'unified':
212 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
214 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
213 %elif c.diffmode == 'sideside':
215 %elif c.diffmode == 'sideside':
214 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
216 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
215 %else:
217 %else:
216 <tr class="cb-line">
218 <tr class="cb-line">
217 <td>unknown diff mode</td>
219 <td>unknown diff mode</td>
218 </tr>
220 </tr>
219 %endif
221 %endif
220 %endfor
222 %endfor
221
223
222 ## outdated comments that do not fit into currently displayed lines
224 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in filediff.left_comments.items():
225 % for lineno, comments in filediff.left_comments.items():
224
226
225 %if c.diffmode == 'unified':
227 %if c.diffmode == 'unified':
226 <tr class="cb-line">
228 <tr class="cb-line">
227 <td class="cb-data cb-context"></td>
229 <td class="cb-data cb-context"></td>
228 <td class="cb-lineno cb-context"></td>
230 <td class="cb-lineno cb-context"></td>
229 <td class="cb-lineno cb-context"></td>
231 <td class="cb-lineno cb-context"></td>
230 <td class="cb-content cb-context">
232 <td class="cb-content cb-context">
231 ${inline_comments_container(comments)}
233 ${inline_comments_container(comments)}
232 </td>
234 </td>
233 </tr>
235 </tr>
234 %elif c.diffmode == 'sideside':
236 %elif c.diffmode == 'sideside':
235 <tr class="cb-line">
237 <tr class="cb-line">
236 <td class="cb-data cb-context"></td>
238 <td class="cb-data cb-context"></td>
237 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
238 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
239 % if lineno.startswith('o'):
241 % if lineno.startswith('o'):
240 ${inline_comments_container(comments)}
242 ${inline_comments_container(comments)}
241 % endif
243 % endif
242 </td>
244 </td>
243
245
244 <td class="cb-data cb-context"></td>
246 <td class="cb-data cb-context"></td>
245 <td class="cb-lineno cb-context"></td>
247 <td class="cb-lineno cb-context"></td>
246 <td class="cb-content cb-context">
248 <td class="cb-content cb-context">
247 % if lineno.startswith('n'):
249 % if lineno.startswith('n'):
248 ${inline_comments_container(comments)}
250 ${inline_comments_container(comments)}
249 % endif
251 % endif
250 </td>
252 </td>
251 </tr>
253 </tr>
252 %endif
254 %endif
253
255
254 % endfor
256 % endfor
255
257
256 </table>
258 </table>
257 </div>
259 </div>
258 %endfor
260 %endfor
259
261
260 ## outdated comments that are made for a file that has been deleted
262 ## outdated comments that are made for a file that has been deleted
261 % for filename, comments_dict in (deleted_files_comments or {}).items():
263 % for filename, comments_dict in (deleted_files_comments or {}).items():
262
264
263 <div class="filediffs filediff-outdated" style="display: none">
265 <div class="filediffs filediff-outdated" style="display: none">
264 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
266 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
265 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
267 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
266 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
268 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
267 <div class="filediff-collapse-indicator"></div>
269 <div class="filediff-collapse-indicator"></div>
268 <span class="pill">
270 <span class="pill">
269 ## file was deleted
271 ## file was deleted
270 <strong>${filename}</strong>
272 <strong>${filename}</strong>
271 </span>
273 </span>
272 <span class="pill-group" style="float: left">
274 <span class="pill-group" style="float: left">
273 ## file op, doesn't need translation
275 ## file op, doesn't need translation
274 <span class="pill" op="removed">removed in this version</span>
276 <span class="pill" op="removed">removed in this version</span>
275 </span>
277 </span>
276 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
278 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
277 <span class="pill-group" style="float: right">
279 <span class="pill-group" style="float: right">
278 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
280 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
279 </span>
281 </span>
280 </label>
282 </label>
281
283
282 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
284 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
283 <tr>
285 <tr>
284 % if c.diffmode == 'unified':
286 % if c.diffmode == 'unified':
285 <td></td>
287 <td></td>
286 %endif
288 %endif
287
289
288 <td></td>
290 <td></td>
289 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
291 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
290 ${_('File was deleted in this version, and outdated comments were made on it')}
292 ${_('File was deleted in this version, and outdated comments were made on it')}
291 </td>
293 </td>
292 </tr>
294 </tr>
293 %if c.diffmode == 'unified':
295 %if c.diffmode == 'unified':
294 <tr class="cb-line">
296 <tr class="cb-line">
295 <td class="cb-data cb-context"></td>
297 <td class="cb-data cb-context"></td>
296 <td class="cb-lineno cb-context"></td>
298 <td class="cb-lineno cb-context"></td>
297 <td class="cb-lineno cb-context"></td>
299 <td class="cb-lineno cb-context"></td>
298 <td class="cb-content cb-context">
300 <td class="cb-content cb-context">
299 ${inline_comments_container(comments_dict['comments'])}
301 ${inline_comments_container(comments_dict['comments'])}
300 </td>
302 </td>
301 </tr>
303 </tr>
302 %elif c.diffmode == 'sideside':
304 %elif c.diffmode == 'sideside':
303 <tr class="cb-line">
305 <tr class="cb-line">
304 <td class="cb-data cb-context"></td>
306 <td class="cb-data cb-context"></td>
305 <td class="cb-lineno cb-context"></td>
307 <td class="cb-lineno cb-context"></td>
306 <td class="cb-content cb-context"></td>
308 <td class="cb-content cb-context"></td>
307
309
308 <td class="cb-data cb-context"></td>
310 <td class="cb-data cb-context"></td>
309 <td class="cb-lineno cb-context"></td>
311 <td class="cb-lineno cb-context"></td>
310 <td class="cb-content cb-context">
312 <td class="cb-content cb-context">
311 ${inline_comments_container(comments_dict['comments'])}
313 ${inline_comments_container(comments_dict['comments'])}
312 </td>
314 </td>
313 </tr>
315 </tr>
314 %endif
316 %endif
315 </table>
317 </table>
316 </div>
318 </div>
317 </div>
319 </div>
318 % endfor
320 % endfor
319
321
320 </div>
322 </div>
321 </div>
323 </div>
322 </%def>
324 </%def>
323
325
324 <%def name="diff_ops(filediff)">
326 <%def name="diff_ops(filediff)">
325 <%
327 <%
326 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
328 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
327 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
329 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
328 %>
330 %>
329 <span class="pill">
331 <span class="pill">
330 %if filediff.source_file_path and filediff.target_file_path:
332 %if filediff.source_file_path and filediff.target_file_path:
331 %if filediff.source_file_path != filediff.target_file_path:
333 %if filediff.source_file_path != filediff.target_file_path:
332 ## file was renamed, or copied
334 ## file was renamed, or copied
333 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
335 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
334 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
336 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
335 <% final_path = filediff.target_file_path %>
337 <% final_path = filediff.target_file_path %>
336 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
338 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
337 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
339 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
338 <% final_path = filediff.target_file_path %>
340 <% final_path = filediff.target_file_path %>
339 %endif
341 %endif
340 %else:
342 %else:
341 ## file was modified
343 ## file was modified
342 <strong>${filediff.source_file_path}</strong>
344 <strong>${filediff.source_file_path}</strong>
343 <% final_path = filediff.source_file_path %>
345 <% final_path = filediff.source_file_path %>
344 %endif
346 %endif
345 %else:
347 %else:
346 %if filediff.source_file_path:
348 %if filediff.source_file_path:
347 ## file was deleted
349 ## file was deleted
348 <strong>${filediff.source_file_path}</strong>
350 <strong>${filediff.source_file_path}</strong>
349 <% final_path = filediff.source_file_path %>
351 <% final_path = filediff.source_file_path %>
350 %else:
352 %else:
351 ## file was added
353 ## file was added
352 <strong>${filediff.target_file_path}</strong>
354 <strong>${filediff.target_file_path}</strong>
353 <% final_path = filediff.target_file_path %>
355 <% final_path = filediff.target_file_path %>
354 %endif
356 %endif
355 %endif
357 %endif
356 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
358 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
357 </span>
359 </span>
358 <span class="pill-group" style="float: left">
360 <span class="pill-group" style="float: left">
359 %if filediff.limited_diff:
361 %if filediff.limited_diff:
360 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
362 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
361 %endif
363 %endif
362
364
363 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 <span class="pill" op="renamed">renamed</span>
366 <span class="pill" op="renamed">renamed</span>
365 %endif
367 %endif
366
368
367 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
369 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
368 <span class="pill" op="copied">copied</span>
370 <span class="pill" op="copied">copied</span>
369 %endif
371 %endif
370
372
371 %if NEW_FILENODE in filediff.patch['stats']['ops']:
373 %if NEW_FILENODE in filediff.patch['stats']['ops']:
372 <span class="pill" op="created">created</span>
374 <span class="pill" op="created">created</span>
373 %if filediff['target_mode'].startswith('120'):
375 %if filediff['target_mode'].startswith('120'):
374 <span class="pill" op="symlink">symlink</span>
376 <span class="pill" op="symlink">symlink</span>
375 %else:
377 %else:
376 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
378 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
377 %endif
379 %endif
378 %endif
380 %endif
379
381
380 %if DEL_FILENODE in filediff.patch['stats']['ops']:
382 %if DEL_FILENODE in filediff.patch['stats']['ops']:
381 <span class="pill" op="removed">removed</span>
383 <span class="pill" op="removed">removed</span>
382 %endif
384 %endif
383
385
384 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
386 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
385 <span class="pill" op="mode">
387 <span class="pill" op="mode">
386 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
388 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
387 </span>
389 </span>
388 %endif
390 %endif
389 </span>
391 </span>
390
392
391 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
393 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
392
394
393 <span class="pill-group" style="float: right">
395 <span class="pill-group" style="float: right">
394 %if BIN_FILENODE in filediff.patch['stats']['ops']:
396 %if BIN_FILENODE in filediff.patch['stats']['ops']:
395 <span class="pill" op="binary">binary</span>
397 <span class="pill" op="binary">binary</span>
396 %if MOD_FILENODE in filediff.patch['stats']['ops']:
398 %if MOD_FILENODE in filediff.patch['stats']['ops']:
397 <span class="pill" op="modified">modified</span>
399 <span class="pill" op="modified">modified</span>
398 %endif
400 %endif
399 %endif
401 %endif
400 %if filediff.patch['stats']['added']:
402 %if filediff.patch['stats']['added']:
401 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
403 <span class="pill" op="added">+${filediff.patch['stats']['added']}</span>
402 %endif
404 %endif
403 %if filediff.patch['stats']['deleted']:
405 %if filediff.patch['stats']['deleted']:
404 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
406 <span class="pill" op="deleted">-${filediff.patch['stats']['deleted']}</span>
405 %endif
407 %endif
406 </span>
408 </span>
407
409
408 </%def>
410 </%def>
409
411
410 <%def name="nice_mode(filemode)">
412 <%def name="nice_mode(filemode)">
411 ${filemode.startswith('100') and filemode[3:] or filemode}
413 ${filemode.startswith('100') and filemode[3:] or filemode}
412 </%def>
414 </%def>
413
415
414 <%def name="diff_menu(filediff, use_comments=False)">
416 <%def name="diff_menu(filediff, use_comments=False)">
415 <div class="filediff-menu">
417 <div class="filediff-menu">
416 %if filediff.diffset.source_ref:
418 %if filediff.diffset.source_ref:
417 %if filediff.operation in ['D', 'M']:
419 %if filediff.operation in ['D', 'M']:
418 <a
420 <a
419 class="tooltip"
421 class="tooltip"
420 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
422 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
421 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
423 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
422 >
424 >
423 ${_('Show file before')}
425 ${_('Show file before')}
424 </a> |
426 </a> |
425 %else:
427 %else:
426 <span
428 <span
427 class="tooltip"
429 class="tooltip"
428 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
430 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
429 >
431 >
430 ${_('Show file before')}
432 ${_('Show file before')}
431 </span> |
433 </span> |
432 %endif
434 %endif
433 %if filediff.operation in ['A', 'M']:
435 %if filediff.operation in ['A', 'M']:
434 <a
436 <a
435 class="tooltip"
437 class="tooltip"
436 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
438 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
437 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
439 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
438 >
440 >
439 ${_('Show file after')}
441 ${_('Show file after')}
440 </a> |
442 </a> |
441 %else:
443 %else:
442 <span
444 <span
443 class="tooltip"
445 class="tooltip"
444 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
446 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
445 >
447 >
446 ${_('Show file after')}
448 ${_('Show file after')}
447 </span> |
449 </span> |
448 %endif
450 %endif
449 <a
451 <a
450 class="tooltip"
452 class="tooltip"
451 title="${h.tooltip(_('Raw diff'))}"
453 title="${h.tooltip(_('Raw diff'))}"
452 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
454 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
453 >
455 >
454 ${_('Raw diff')}
456 ${_('Raw diff')}
455 </a> |
457 </a> |
456 <a
458 <a
457 class="tooltip"
459 class="tooltip"
458 title="${h.tooltip(_('Download diff'))}"
460 title="${h.tooltip(_('Download diff'))}"
459 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
461 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
460 >
462 >
461 ${_('Download diff')}
463 ${_('Download diff')}
462 </a>
464 </a>
463 % if use_comments:
465 % if use_comments:
464 |
466 |
465 % endif
467 % endif
466
468
467 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
469 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
468 %if hasattr(c, 'ignorews_url'):
470 %if hasattr(c, 'ignorews_url'):
469 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
471 ${c.ignorews_url(request, h.FID('', filediff.patch['filename']))}
470 %endif
472 %endif
471 %if hasattr(c, 'context_url'):
473 %if hasattr(c, 'context_url'):
472 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
474 ${c.context_url(request, h.FID('', filediff.patch['filename']))}
473 %endif
475 %endif
474
476
475 %if use_comments:
477 %if use_comments:
476 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
478 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
477 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
479 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
478 </a>
480 </a>
479 %endif
481 %endif
480 %endif
482 %endif
481 </div>
483 </div>
482 </%def>
484 </%def>
483
485
484
486
485 <%def name="inline_comments_container(comments)">
487 <%def name="inline_comments_container(comments)">
486 <div class="inline-comments">
488 <div class="inline-comments">
487 %for comment in comments:
489 %for comment in comments:
488 ${commentblock.comment_block(comment, inline=True)}
490 ${commentblock.comment_block(comment, inline=True)}
489 %endfor
491 %endfor
490
492
491 % if comments and comments[-1].outdated:
493 % if comments and comments[-1].outdated:
492 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
494 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
493 style="display: none;}">
495 style="display: none;}">
494 ${_('Add another comment')}
496 ${_('Add another comment')}
495 </span>
497 </span>
496 % else:
498 % else:
497 <span onclick="return Rhodecode.comments.createComment(this)"
499 <span onclick="return Rhodecode.comments.createComment(this)"
498 class="btn btn-secondary cb-comment-add-button">
500 class="btn btn-secondary cb-comment-add-button">
499 ${_('Add another comment')}
501 ${_('Add another comment')}
500 </span>
502 </span>
501 % endif
503 % endif
502
504
503 </div>
505 </div>
504 </%def>
506 </%def>
505
507
506
508
507 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
509 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
508 %for i, line in enumerate(hunk.sideside):
510 %for i, line in enumerate(hunk.sideside):
509 <%
511 <%
510 old_line_anchor, new_line_anchor = None, None
512 old_line_anchor, new_line_anchor = None, None
511 if line.original.lineno:
513 if line.original.lineno:
512 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
514 old_line_anchor = diff_line_anchor(hunk.source_file_path, line.original.lineno, 'o')
513 if line.modified.lineno:
515 if line.modified.lineno:
514 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
516 new_line_anchor = diff_line_anchor(hunk.target_file_path, line.modified.lineno, 'n')
515 %>
517 %>
516
518
517 <tr class="cb-line">
519 <tr class="cb-line">
518 <td class="cb-data ${action_class(line.original.action)}"
520 <td class="cb-data ${action_class(line.original.action)}"
519 data-line-number="${line.original.lineno}"
521 data-line-number="${line.original.lineno}"
520 >
522 >
521 <div>
523 <div>
522 %if line.original.comments:
524 %if line.original.comments:
523 <% has_outdated = any([x.outdated for x in line.original.comments]) %>
525 <% has_outdated = any([x.outdated for x in line.original.comments]) %>
524 % if has_outdated:
526 % if has_outdated:
525 <i title="${_('comments including outdated')}:${len(line.original.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
527 <i title="${_('comments including outdated')}:${len(line.original.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
526 % else:
528 % else:
527 <i title="${_('comments')}: ${len(line.original.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
529 <i title="${_('comments')}: ${len(line.original.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
528 % endif
530 % endif
529 %endif
531 %endif
530 </div>
532 </div>
531 </td>
533 </td>
532 <td class="cb-lineno ${action_class(line.original.action)}"
534 <td class="cb-lineno ${action_class(line.original.action)}"
533 data-line-number="${line.original.lineno}"
535 data-line-number="${line.original.lineno}"
534 %if old_line_anchor:
536 %if old_line_anchor:
535 id="${old_line_anchor}"
537 id="${old_line_anchor}"
536 %endif
538 %endif
537 >
539 >
538 %if line.original.lineno:
540 %if line.original.lineno:
539 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
541 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
540 %endif
542 %endif
541 </td>
543 </td>
542 <td class="cb-content ${action_class(line.original.action)}"
544 <td class="cb-content ${action_class(line.original.action)}"
543 data-line-number="o${line.original.lineno}"
545 data-line-number="o${line.original.lineno}"
544 >
546 >
545 %if use_comments and line.original.lineno:
547 %if use_comments and line.original.lineno:
546 ${render_add_comment_button()}
548 ${render_add_comment_button()}
547 %endif
549 %endif
548 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
550 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
549 %if use_comments and line.original.lineno and line.original.comments:
551 %if use_comments and line.original.lineno and line.original.comments:
550 ${inline_comments_container(line.original.comments)}
552 ${inline_comments_container(line.original.comments)}
551 %endif
553 %endif
552 </td>
554 </td>
553 <td class="cb-data ${action_class(line.modified.action)}"
555 <td class="cb-data ${action_class(line.modified.action)}"
554 data-line-number="${line.modified.lineno}"
556 data-line-number="${line.modified.lineno}"
555 >
557 >
556 <div>
558 <div>
557 %if line.modified.comments:
559 %if line.modified.comments:
558 <% has_outdated = any([x.outdated for x in line.modified.comments]) %>
560 <% has_outdated = any([x.outdated for x in line.modified.comments]) %>
559 % if has_outdated:
561 % if has_outdated:
560 <i title="${_('comments including outdated')}:${len(line.modified.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
562 <i title="${_('comments including outdated')}:${len(line.modified.comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
561 % else:
563 % else:
562 <i title="${_('comments')}: ${len(line.modified.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
564 <i title="${_('comments')}: ${len(line.modified.comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
563 % endif
565 % endif
564 %endif
566 %endif
565 </div>
567 </div>
566 </td>
568 </td>
567 <td class="cb-lineno ${action_class(line.modified.action)}"
569 <td class="cb-lineno ${action_class(line.modified.action)}"
568 data-line-number="${line.modified.lineno}"
570 data-line-number="${line.modified.lineno}"
569 %if new_line_anchor:
571 %if new_line_anchor:
570 id="${new_line_anchor}"
572 id="${new_line_anchor}"
571 %endif
573 %endif
572 >
574 >
573 %if line.modified.lineno:
575 %if line.modified.lineno:
574 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
576 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
575 %endif
577 %endif
576 </td>
578 </td>
577 <td class="cb-content ${action_class(line.modified.action)}"
579 <td class="cb-content ${action_class(line.modified.action)}"
578 data-line-number="n${line.modified.lineno}"
580 data-line-number="n${line.modified.lineno}"
579 >
581 >
580 %if use_comments and line.modified.lineno:
582 %if use_comments and line.modified.lineno:
581 ${render_add_comment_button()}
583 ${render_add_comment_button()}
582 %endif
584 %endif
583 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
585 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
584 %if use_comments and line.modified.lineno and line.modified.comments:
586 %if use_comments and line.modified.lineno and line.modified.comments:
585 ${inline_comments_container(line.modified.comments)}
587 ${inline_comments_container(line.modified.comments)}
586 %endif
588 %endif
587 </td>
589 </td>
588 </tr>
590 </tr>
589 %endfor
591 %endfor
590 </%def>
592 </%def>
591
593
592
594
593 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
595 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
594 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
596 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
595 <%
597 <%
596 old_line_anchor, new_line_anchor = None, None
598 old_line_anchor, new_line_anchor = None, None
597 if old_line_no:
599 if old_line_no:
598 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
600 old_line_anchor = diff_line_anchor(hunk.source_file_path, old_line_no, 'o')
599 if new_line_no:
601 if new_line_no:
600 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
602 new_line_anchor = diff_line_anchor(hunk.target_file_path, new_line_no, 'n')
601 %>
603 %>
602 <tr class="cb-line">
604 <tr class="cb-line">
603 <td class="cb-data ${action_class(action)}">
605 <td class="cb-data ${action_class(action)}">
604 <div>
606 <div>
605 % if comments:
607 % if comments:
606 <% has_outdated = any([x.outdated for x in comments]) %>
608 <% has_outdated = any([x.outdated for x in comments]) %>
607 % if has_outdated:
609 % if has_outdated:
608 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
610 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
609 % else:
611 % else:
610 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
612 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
611 % endif
613 % endif
612 % endif
614 % endif
613 </div>
615 </div>
614 </td>
616 </td>
615 <td class="cb-lineno ${action_class(action)}"
617 <td class="cb-lineno ${action_class(action)}"
616 data-line-number="${old_line_no}"
618 data-line-number="${old_line_no}"
617 %if old_line_anchor:
619 %if old_line_anchor:
618 id="${old_line_anchor}"
620 id="${old_line_anchor}"
619 %endif
621 %endif
620 >
622 >
621 %if old_line_anchor:
623 %if old_line_anchor:
622 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
624 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
623 %endif
625 %endif
624 </td>
626 </td>
625 <td class="cb-lineno ${action_class(action)}"
627 <td class="cb-lineno ${action_class(action)}"
626 data-line-number="${new_line_no}"
628 data-line-number="${new_line_no}"
627 %if new_line_anchor:
629 %if new_line_anchor:
628 id="${new_line_anchor}"
630 id="${new_line_anchor}"
629 %endif
631 %endif
630 >
632 >
631 %if new_line_anchor:
633 %if new_line_anchor:
632 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
634 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
633 %endif
635 %endif
634 </td>
636 </td>
635 <td class="cb-content ${action_class(action)}"
637 <td class="cb-content ${action_class(action)}"
636 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
638 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
637 >
639 >
638 %if use_comments:
640 %if use_comments:
639 ${render_add_comment_button()}
641 ${render_add_comment_button()}
640 %endif
642 %endif
641 <span class="cb-code">${action} ${content or '' | n}</span>
643 <span class="cb-code">${action} ${content or '' | n}</span>
642 %if use_comments and comments:
644 %if use_comments and comments:
643 ${inline_comments_container(comments)}
645 ${inline_comments_container(comments)}
644 %endif
646 %endif
645 </td>
647 </td>
646 </tr>
648 </tr>
647 %endfor
649 %endfor
648 </%def>
650 </%def>
649
651
650 <%def name="render_add_comment_button()">
652 <%def name="render_add_comment_button()">
651 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
653 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
652 <span><i class="icon-comment"></i></span>
654 <span><i class="icon-comment"></i></span>
653 </button>
655 </button>
654 </%def>
656 </%def>
655
657
656 <%def name="render_diffset_menu()">
658 <%def name="render_diffset_menu()">
657
659
658 <div class="diffset-menu clearinner">
660 <div class="diffset-menu clearinner">
659 <div class="pull-right">
661 <div class="pull-right">
660 <div class="btn-group">
662 <div class="btn-group">
661
663
662 <a
664 <a
663 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
665 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
664 title="${h.tooltip(_('View side by side'))}"
666 title="${h.tooltip(_('View side by side'))}"
665 href="${h.current_route_path(request, diffmode='sideside')}">
667 href="${h.current_route_path(request, diffmode='sideside')}">
666 <span>${_('Side by Side')}</span>
668 <span>${_('Side by Side')}</span>
667 </a>
669 </a>
668 <a
670 <a
669 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
671 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
670 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
672 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
671 <span>${_('Unified')}</span>
673 <span>${_('Unified')}</span>
672 </a>
674 </a>
673 </div>
675 </div>
674 </div>
676 </div>
675
677
676 <div class="pull-left">
678 <div class="pull-left">
677 <div class="btn-group">
679 <div class="btn-group">
678 <a
680 <a
679 class="btn"
681 class="btn"
680 href="#"
682 href="#"
681 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
683 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
682 <a
684 <a
683 class="btn"
685 class="btn"
684 href="#"
686 href="#"
685 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
687 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
686 <a
688 <a
687 class="btn"
689 class="btn"
688 href="#"
690 href="#"
689 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
691 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
690 </div>
692 </div>
691 </div>
693 </div>
692 </div>
694 </div>
693 </%def>
695 </%def>
@@ -1,34 +1,38 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 ${_('%(user)s commited on %(date)s UTC') % {
3 ${_('%(user)s commited on %(date)s UTC') % {
4 'user': h.person(commit.author),
4 'user': h.person(commit.author),
5 'date': h.format_date(commit.date)
5 'date': h.format_date(commit.date)
6 }}
6 }}
7 <br/>
7 <br/>
8 % if commit.branch:
8 % if commit.branch:
9 branch: ${commit.branch} <br/>
9 branch: ${commit.branch} <br/>
10 % endif
10 % endif
11
11
12 % for bookmark in getattr(commit, 'bookmarks', []):
12 % for bookmark in getattr(commit, 'bookmarks', []):
13 bookmark: ${bookmark} <br/>
13 bookmark: ${bookmark} <br/>
14 % endfor
14 % endfor
15
15
16 % for tag in commit.tags:
16 % for tag in commit.tags:
17 tag: ${tag} <br/>
17 tag: ${tag} <br/>
18 % endfor
18 % endfor
19
19
20 % if has_hidden_changes:
21 Has hidden changes<br/>
22 % endif
23
20 commit: <a href="${h.route_url('repo_commit', repo_name=c.rhodecode_db_repo.repo_name, commit_id=commit.raw_id)}">${h.show_id(commit)}</a>
24 commit: <a href="${h.route_url('repo_commit', repo_name=c.rhodecode_db_repo.repo_name, commit_id=commit.raw_id)}">${h.show_id(commit)}</a>
21 <pre>
25 <pre>
22 ${h.urlify_commit_message(commit.message)}
26 ${h.urlify_commit_message(commit.message)}
23
27
24 % for change in parsed_diff:
28 % for change in parsed_diff:
25 % if limited_diff:
29 % if limited_diff:
26 ${_('Commit was too big and was cut off...')}
30 ${_('Commit was too big and was cut off...')}
27 % endif
31 % endif
28 ${change['operation']} ${change['filename']} ${'(%(added)s lines added, %(removed)s lines removed)' % {'added': change['stats']['added'], 'removed': change['stats']['deleted']}}
32 ${change['operation']} ${change['filename']} ${'(%(added)s lines added, %(removed)s lines removed)' % {'added': change['stats']['added'], 'removed': change['stats']['deleted']}}
29 % endfor
33 % endfor
30
34
31 % if feed_include_diff:
35 % if feed_include_diff:
32 ${diff_processor.as_raw()}
36 ${c.path_filter.get_raw_patch(diff_processor)}
33 % endif
37 % endif
34 </pre>
38 </pre>
General Comments 0
You need to be logged in to leave comments. Login now