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