##// END OF EJS Templates
path-filter: enable for quick search menu.
marcink -
r3817:d10a30f6 stable
parent child Browse files
Show More
@@ -1,728 +1,734 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 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 c.is_super_admin = c.auth_user.is_admin
172 172
173 173 c.can_create_repo = c.is_super_admin
174 174 c.can_create_repo_group = c.is_super_admin
175 175 c.can_create_user_group = c.is_super_admin
176 176
177 177 c.is_delegated_admin = False
178 178
179 179 if not c.auth_user.is_default and not c.is_super_admin:
180 180 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
181 181 user=self.request.user)
182 182 repositories = c.auth_user.repositories_admin or c.can_create_repo
183 183
184 184 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
185 185 user=self.request.user)
186 186 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
187 187
188 188 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
189 189 user=self.request.user)
190 190 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
191 191 # delegated admin can create, or manage some objects
192 192 c.is_delegated_admin = repositories or repository_groups or user_groups
193 193 return c
194 194
195 195 def _get_template_context(self, tmpl_args, **kwargs):
196 196
197 197 local_tmpl_args = {
198 198 'defaults': {},
199 199 'errors': {},
200 200 'c': tmpl_args
201 201 }
202 202 local_tmpl_args.update(kwargs)
203 203 return local_tmpl_args
204 204
205 205 def load_default_context(self):
206 206 """
207 207 example:
208 208
209 209 def load_default_context(self):
210 210 c = self._get_local_tmpl_context()
211 211 c.custom_var = 'foobar'
212 212
213 213 return c
214 214 """
215 215 raise NotImplementedError('Needs implementation in view class')
216 216
217 217
218 218 class RepoAppView(BaseAppView):
219 219
220 220 def __init__(self, context, request):
221 221 super(RepoAppView, self).__init__(context, request)
222 222 self.db_repo = request.db_repo
223 223 self.db_repo_name = self.db_repo.repo_name
224 224 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
225 225
226 226 def _handle_missing_requirements(self, error):
227 227 log.error(
228 228 'Requirements are missing for repository %s: %s',
229 229 self.db_repo_name, safe_unicode(error))
230 230
231 231 def _get_local_tmpl_context(self, include_app_defaults=True):
232 232 _ = self.request.translate
233 233 c = super(RepoAppView, self)._get_local_tmpl_context(
234 234 include_app_defaults=include_app_defaults)
235 235
236 236 # register common vars for this type of view
237 237 c.rhodecode_db_repo = self.db_repo
238 238 c.repo_name = self.db_repo_name
239 239 c.repository_pull_requests = self.db_repo_pull_requests
240 240 c.repository_is_user_following = ScmModel().is_following_repo(
241 241 self.db_repo_name, self._rhodecode_user.user_id)
242 242 self.path_filter = PathFilter(None)
243 243
244 244 c.repository_requirements_missing = {}
245 245 try:
246 246 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
247 247 # NOTE(marcink):
248 248 # comparison to None since if it's an object __bool__ is expensive to
249 249 # calculate
250 250 if self.rhodecode_vcs_repo is not None:
251 251 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
252 252 c.auth_user.username)
253 253 self.path_filter = PathFilter(path_perms)
254 254 except RepositoryRequirementError as e:
255 255 c.repository_requirements_missing = {'error': str(e)}
256 256 self._handle_missing_requirements(e)
257 257 self.rhodecode_vcs_repo = None
258 258
259 259 c.path_filter = self.path_filter # used by atom_feed_entry.mako
260 260
261 261 if self.rhodecode_vcs_repo is None:
262 262 # unable to fetch this repo as vcs instance, report back to user
263 263 h.flash(_(
264 264 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
265 265 "Please check if it exist, or is not damaged.") %
266 266 {'repo_name': c.repo_name},
267 267 category='error', ignore_duplicate=True)
268 268 if c.repository_requirements_missing:
269 269 route = self.request.matched_route.name
270 270 if route.startswith(('edit_repo', 'repo_summary')):
271 271 # allow summary and edit repo on missing requirements
272 272 return c
273 273
274 274 raise HTTPFound(
275 275 h.route_path('repo_summary', repo_name=self.db_repo_name))
276 276
277 277 else: # redirect if we don't show missing requirements
278 278 raise HTTPFound(h.route_path('home'))
279 279
280 280 c.has_origin_repo_read_perm = False
281 281 if self.db_repo.fork:
282 282 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
283 283 'repository.write', 'repository.read', 'repository.admin')(
284 284 self.db_repo.fork.repo_name, 'summary fork link')
285 285
286 286 return c
287 287
288 288 def _get_f_path_unchecked(self, matchdict, default=None):
289 289 """
290 290 Should only be used by redirects, everything else should call _get_f_path
291 291 """
292 292 f_path = matchdict.get('f_path')
293 293 if f_path:
294 294 # fix for multiple initial slashes that causes errors for GIT
295 295 return f_path.lstrip('/')
296 296
297 297 return default
298 298
299 299 def _get_f_path(self, matchdict, default=None):
300 300 f_path_match = self._get_f_path_unchecked(matchdict, default)
301 301 return self.path_filter.assert_path_permissions(f_path_match)
302 302
303 303 def _get_general_setting(self, target_repo, settings_key, default=False):
304 304 settings_model = VcsSettingsModel(repo=target_repo)
305 305 settings = settings_model.get_general_settings()
306 306 return settings.get(settings_key, default)
307 307
308 308 def get_recache_flag(self):
309 309 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
310 310 flag_val = self.request.GET.get(flag_name)
311 311 if str2bool(flag_val):
312 312 return True
313 313 return False
314 314
315 315
316 316 class PathFilter(object):
317 317
318 318 # Expects and instance of BasePathPermissionChecker or None
319 319 def __init__(self, permission_checker):
320 320 self.permission_checker = permission_checker
321 321
322 322 def assert_path_permissions(self, path):
323 if path and self.permission_checker and not self.permission_checker.has_access(path):
324 raise HTTPForbidden()
325 return path
323 if self.path_access_allowed(path):
324 return path
325 raise HTTPForbidden()
326
327 def path_access_allowed(self, path):
328 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
329 if self.permission_checker:
330 return path and self.permission_checker.has_access(path)
331 return True
326 332
327 333 def filter_patchset(self, patchset):
328 334 if not self.permission_checker or not patchset:
329 335 return patchset, False
330 336 had_filtered = False
331 337 filtered_patchset = []
332 338 for patch in patchset:
333 339 filename = patch.get('filename', None)
334 340 if not filename or self.permission_checker.has_access(filename):
335 341 filtered_patchset.append(patch)
336 342 else:
337 343 had_filtered = True
338 344 if had_filtered:
339 345 if isinstance(patchset, diffs.LimitedDiffContainer):
340 346 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
341 347 return filtered_patchset, True
342 348 else:
343 349 return patchset, False
344 350
345 351 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
346 352 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
347 353 result = diffset.render_patchset(
348 354 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
349 355 result.has_hidden_changes = has_hidden_changes
350 356 return result
351 357
352 358 def get_raw_patch(self, diff_processor):
353 359 if self.permission_checker is None:
354 360 return diff_processor.as_raw()
355 361 elif self.permission_checker.has_full_access:
356 362 return diff_processor.as_raw()
357 363 else:
358 364 return '# Repository has user-specific filters, raw patch generation is disabled.'
359 365
360 366 @property
361 367 def is_enabled(self):
362 368 return self.permission_checker is not None
363 369
364 370
365 371 class RepoGroupAppView(BaseAppView):
366 372 def __init__(self, context, request):
367 373 super(RepoGroupAppView, self).__init__(context, request)
368 374 self.db_repo_group = request.db_repo_group
369 375 self.db_repo_group_name = self.db_repo_group.group_name
370 376
371 377 def _get_local_tmpl_context(self, include_app_defaults=True):
372 378 _ = self.request.translate
373 379 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
374 380 include_app_defaults=include_app_defaults)
375 381 c.repo_group = self.db_repo_group
376 382 return c
377 383
378 384 def _revoke_perms_on_yourself(self, form_result):
379 385 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
380 386 form_result['perm_updates'])
381 387 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
382 388 form_result['perm_additions'])
383 389 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
384 390 form_result['perm_deletions'])
385 391 admin_perm = 'group.admin'
386 392 if _updates and _updates[0][1] != admin_perm or \
387 393 _additions and _additions[0][1] != admin_perm or \
388 394 _deletions and _deletions[0][1] != admin_perm:
389 395 return True
390 396 return False
391 397
392 398
393 399 class UserGroupAppView(BaseAppView):
394 400 def __init__(self, context, request):
395 401 super(UserGroupAppView, self).__init__(context, request)
396 402 self.db_user_group = request.db_user_group
397 403 self.db_user_group_name = self.db_user_group.users_group_name
398 404
399 405
400 406 class UserAppView(BaseAppView):
401 407 def __init__(self, context, request):
402 408 super(UserAppView, self).__init__(context, request)
403 409 self.db_user = request.db_user
404 410 self.db_user_id = self.db_user.user_id
405 411
406 412 _ = self.request.translate
407 413 if not request.db_user_supports_default:
408 414 if self.db_user.username == User.DEFAULT_USER:
409 415 h.flash(_("Editing user `{}` is disabled.".format(
410 416 User.DEFAULT_USER)), category='warning')
411 417 raise HTTPFound(h.route_path('users'))
412 418
413 419
414 420 class DataGridAppView(object):
415 421 """
416 422 Common class to have re-usable grid rendering components
417 423 """
418 424
419 425 def _extract_ordering(self, request, column_map=None):
420 426 column_map = column_map or {}
421 427 column_index = safe_int(request.GET.get('order[0][column]'))
422 428 order_dir = request.GET.get(
423 429 'order[0][dir]', 'desc')
424 430 order_by = request.GET.get(
425 431 'columns[%s][data][sort]' % column_index, 'name_raw')
426 432
427 433 # translate datatable to DB columns
428 434 order_by = column_map.get(order_by) or order_by
429 435
430 436 search_q = request.GET.get('search[value]')
431 437 return search_q, order_by, order_dir
432 438
433 439 def _extract_chunk(self, request):
434 440 start = safe_int(request.GET.get('start'), 0)
435 441 length = safe_int(request.GET.get('length'), 25)
436 442 draw = safe_int(request.GET.get('draw'))
437 443 return draw, start, length
438 444
439 445 def _get_order_col(self, order_by, model):
440 446 if isinstance(order_by, compat.string_types):
441 447 try:
442 448 return operator.attrgetter(order_by)(model)
443 449 except AttributeError:
444 450 return None
445 451 else:
446 452 return order_by
447 453
448 454
449 455 class BaseReferencesView(RepoAppView):
450 456 """
451 457 Base for reference view for branches, tags and bookmarks.
452 458 """
453 459 def load_default_context(self):
454 460 c = self._get_local_tmpl_context()
455 461
456 462
457 463 return c
458 464
459 465 def load_refs_context(self, ref_items, partials_template):
460 466 _render = self.request.get_partial_renderer(partials_template)
461 467 pre_load = ["author", "date", "message"]
462 468
463 469 is_svn = h.is_svn(self.rhodecode_vcs_repo)
464 470 is_hg = h.is_hg(self.rhodecode_vcs_repo)
465 471
466 472 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
467 473
468 474 closed_refs = {}
469 475 if is_hg:
470 476 closed_refs = self.rhodecode_vcs_repo.branches_closed
471 477
472 478 data = []
473 479 for ref_name, commit_id in ref_items:
474 480 commit = self.rhodecode_vcs_repo.get_commit(
475 481 commit_id=commit_id, pre_load=pre_load)
476 482 closed = ref_name in closed_refs
477 483
478 484 # TODO: johbo: Unify generation of reference links
479 485 use_commit_id = '/' in ref_name or is_svn
480 486
481 487 if use_commit_id:
482 488 files_url = h.route_path(
483 489 'repo_files',
484 490 repo_name=self.db_repo_name,
485 491 f_path=ref_name if is_svn else '',
486 492 commit_id=commit_id)
487 493
488 494 else:
489 495 files_url = h.route_path(
490 496 'repo_files',
491 497 repo_name=self.db_repo_name,
492 498 f_path=ref_name if is_svn else '',
493 499 commit_id=ref_name,
494 500 _query=dict(at=ref_name))
495 501
496 502 data.append({
497 503 "name": _render('name', ref_name, files_url, closed),
498 504 "name_raw": ref_name,
499 505 "date": _render('date', commit.date),
500 506 "date_raw": datetime_to_time(commit.date),
501 507 "author": _render('author', commit.author),
502 508 "commit": _render(
503 509 'commit', commit.message, commit.raw_id, commit.idx),
504 510 "commit_raw": commit.idx,
505 511 "compare": _render(
506 512 'compare', format_ref_id(ref_name, commit.raw_id)),
507 513 })
508 514
509 515 return data
510 516
511 517
512 518 class RepoRoutePredicate(object):
513 519 def __init__(self, val, config):
514 520 self.val = val
515 521
516 522 def text(self):
517 523 return 'repo_route = %s' % self.val
518 524
519 525 phash = text
520 526
521 527 def __call__(self, info, request):
522 528 if hasattr(request, 'vcs_call'):
523 529 # skip vcs calls
524 530 return
525 531
526 532 repo_name = info['match']['repo_name']
527 533 repo_model = repo.RepoModel()
528 534
529 535 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
530 536
531 537 def redirect_if_creating(route_info, db_repo):
532 538 skip_views = ['edit_repo_advanced_delete']
533 539 route = route_info['route']
534 540 # we should skip delete view so we can actually "remove" repositories
535 541 # if they get stuck in creating state.
536 542 if route.name in skip_views:
537 543 return
538 544
539 545 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
540 546 repo_creating_url = request.route_path(
541 547 'repo_creating', repo_name=db_repo.repo_name)
542 548 raise HTTPFound(repo_creating_url)
543 549
544 550 if by_name_match:
545 551 # register this as request object we can re-use later
546 552 request.db_repo = by_name_match
547 553 redirect_if_creating(info, by_name_match)
548 554 return True
549 555
550 556 by_id_match = repo_model.get_repo_by_id(repo_name)
551 557 if by_id_match:
552 558 request.db_repo = by_id_match
553 559 redirect_if_creating(info, by_id_match)
554 560 return True
555 561
556 562 return False
557 563
558 564
559 565 class RepoForbidArchivedRoutePredicate(object):
560 566 def __init__(self, val, config):
561 567 self.val = val
562 568
563 569 def text(self):
564 570 return 'repo_forbid_archived = %s' % self.val
565 571
566 572 phash = text
567 573
568 574 def __call__(self, info, request):
569 575 _ = request.translate
570 576 rhodecode_db_repo = request.db_repo
571 577
572 578 log.debug(
573 579 '%s checking if archived flag for repo for %s',
574 580 self.__class__.__name__, rhodecode_db_repo.repo_name)
575 581
576 582 if rhodecode_db_repo.archived:
577 583 log.warning('Current view is not supported for archived repo:%s',
578 584 rhodecode_db_repo.repo_name)
579 585
580 586 h.flash(
581 587 h.literal(_('Action not supported for archived repository.')),
582 588 category='warning')
583 589 summary_url = request.route_path(
584 590 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
585 591 raise HTTPFound(summary_url)
586 592 return True
587 593
588 594
589 595 class RepoTypeRoutePredicate(object):
590 596 def __init__(self, val, config):
591 597 self.val = val or ['hg', 'git', 'svn']
592 598
593 599 def text(self):
594 600 return 'repo_accepted_type = %s' % self.val
595 601
596 602 phash = text
597 603
598 604 def __call__(self, info, request):
599 605 if hasattr(request, 'vcs_call'):
600 606 # skip vcs calls
601 607 return
602 608
603 609 rhodecode_db_repo = request.db_repo
604 610
605 611 log.debug(
606 612 '%s checking repo type for %s in %s',
607 613 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
608 614
609 615 if rhodecode_db_repo.repo_type in self.val:
610 616 return True
611 617 else:
612 618 log.warning('Current view is not supported for repo type:%s',
613 619 rhodecode_db_repo.repo_type)
614 620 return False
615 621
616 622
617 623 class RepoGroupRoutePredicate(object):
618 624 def __init__(self, val, config):
619 625 self.val = val
620 626
621 627 def text(self):
622 628 return 'repo_group_route = %s' % self.val
623 629
624 630 phash = text
625 631
626 632 def __call__(self, info, request):
627 633 if hasattr(request, 'vcs_call'):
628 634 # skip vcs calls
629 635 return
630 636
631 637 repo_group_name = info['match']['repo_group_name']
632 638 repo_group_model = repo_group.RepoGroupModel()
633 639 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
634 640
635 641 if by_name_match:
636 642 # register this as request object we can re-use later
637 643 request.db_repo_group = by_name_match
638 644 return True
639 645
640 646 return False
641 647
642 648
643 649 class UserGroupRoutePredicate(object):
644 650 def __init__(self, val, config):
645 651 self.val = val
646 652
647 653 def text(self):
648 654 return 'user_group_route = %s' % self.val
649 655
650 656 phash = text
651 657
652 658 def __call__(self, info, request):
653 659 if hasattr(request, 'vcs_call'):
654 660 # skip vcs calls
655 661 return
656 662
657 663 user_group_id = info['match']['user_group_id']
658 664 user_group_model = user_group.UserGroup()
659 665 by_id_match = user_group_model.get(user_group_id, cache=False)
660 666
661 667 if by_id_match:
662 668 # register this as request object we can re-use later
663 669 request.db_user_group = by_id_match
664 670 return True
665 671
666 672 return False
667 673
668 674
669 675 class UserRoutePredicateBase(object):
670 676 supports_default = None
671 677
672 678 def __init__(self, val, config):
673 679 self.val = val
674 680
675 681 def text(self):
676 682 raise NotImplementedError()
677 683
678 684 def __call__(self, info, request):
679 685 if hasattr(request, 'vcs_call'):
680 686 # skip vcs calls
681 687 return
682 688
683 689 user_id = info['match']['user_id']
684 690 user_model = user.User()
685 691 by_id_match = user_model.get(user_id, cache=False)
686 692
687 693 if by_id_match:
688 694 # register this as request object we can re-use later
689 695 request.db_user = by_id_match
690 696 request.db_user_supports_default = self.supports_default
691 697 return True
692 698
693 699 return False
694 700
695 701
696 702 class UserRoutePredicate(UserRoutePredicateBase):
697 703 supports_default = False
698 704
699 705 def text(self):
700 706 return 'user_route = %s' % self.val
701 707
702 708 phash = text
703 709
704 710
705 711 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
706 712 supports_default = True
707 713
708 714 def text(self):
709 715 return 'user_with_default_route = %s' % self.val
710 716
711 717 phash = text
712 718
713 719
714 720 def includeme(config):
715 721 config.add_route_predicate(
716 722 'repo_route', RepoRoutePredicate)
717 723 config.add_route_predicate(
718 724 'repo_accepted_types', RepoTypeRoutePredicate)
719 725 config.add_route_predicate(
720 726 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
721 727 config.add_route_predicate(
722 728 'repo_group_route', RepoGroupRoutePredicate)
723 729 config.add_route_predicate(
724 730 'user_group_route', UserGroupRoutePredicate)
725 731 config.add_route_predicate(
726 732 'user_route_with_default', UserRouteWithDefaultPredicate)
727 733 config.add_route_predicate(
728 734 'user_route', UserRoutePredicate)
@@ -1,1526 +1,1528 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 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_rev[1]
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError):
185 185 msg = _('No such commit exists for this repository')
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if not repo.is_empty():
241 241 commit = repo.get_commit(commit_id=commit_id)
242 242 if commit:
243 243 branch_name = commit.branch
244 244 sha_commit_id = commit.raw_id
245 245
246 246 return branch_name, sha_commit_id, is_head
247 247
248 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
249 249
250 250 repo_id = self.db_repo.repo_id
251 251 force_recache = self.get_recache_flag()
252 252
253 253 cache_seconds = safe_int(
254 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 255 cache_on = not force_recache and cache_seconds > 0
256 256 log.debug(
257 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 258 'with caching: %s[TTL: %ss]' % (
259 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 260
261 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 263
264 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
265 265 condition=cache_on)
266 266 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
267 267 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
268 268 ver, repo_id, commit_id, f_path)
269 269
270 270 c.full_load = full_load
271 271 return render(
272 272 'rhodecode:templates/files/files_browser_tree.mako',
273 273 self._get_template_context(c), self.request)
274 274
275 275 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
276 276
277 277 def _get_archive_spec(self, fname):
278 278 log.debug('Detecting archive spec for: `%s`', fname)
279 279
280 280 fileformat = None
281 281 ext = None
282 282 content_type = None
283 283 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
284 284
285 285 if fname.endswith(extension):
286 286 fileformat = a_type
287 287 log.debug('archive is of type: %s', fileformat)
288 288 ext = extension
289 289 break
290 290
291 291 if not fileformat:
292 292 raise ValueError()
293 293
294 294 # left over part of whole fname is the commit
295 295 commit_id = fname[:-len(ext)]
296 296
297 297 return commit_id, ext, fileformat, content_type
298 298
299 299 def create_pure_path(self, *parts):
300 300 # Split paths and sanitize them, removing any ../ etc
301 301 sanitized_path = [
302 302 x for x in pathlib2.PurePath(*parts).parts
303 303 if x not in ['.', '..']]
304 304
305 305 pure_path = pathlib2.PurePath(*sanitized_path)
306 306 return pure_path
307 307
308 308 @LoginRequired()
309 309 @HasRepoPermissionAnyDecorator(
310 310 'repository.read', 'repository.write', 'repository.admin')
311 311 @view_config(
312 312 route_name='repo_archivefile', request_method='GET',
313 313 renderer=None)
314 314 def repo_archivefile(self):
315 315 # archive cache config
316 316 from rhodecode import CONFIG
317 317 _ = self.request.translate
318 318 self.load_default_context()
319 319 default_at_path = '/'
320 320 fname = self.request.matchdict['fname']
321 321 subrepos = self.request.GET.get('subrepos') == 'true'
322 322 at_path = self.request.GET.get('at_path') or default_at_path
323 323
324 324 if not self.db_repo.enable_downloads:
325 325 return Response(_('Downloads disabled'))
326 326
327 327 try:
328 328 commit_id, ext, fileformat, content_type = \
329 329 self._get_archive_spec(fname)
330 330 except ValueError:
331 331 return Response(_('Unknown archive type for: `{}`').format(
332 332 h.escape(fname)))
333 333
334 334 try:
335 335 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
336 336 except CommitDoesNotExistError:
337 337 return Response(_('Unknown commit_id {}').format(
338 338 h.escape(commit_id)))
339 339 except EmptyRepositoryError:
340 340 return Response(_('Empty repository'))
341 341
342 342 try:
343 343 at_path = commit.get_node(at_path).path or default_at_path
344 344 except Exception:
345 345 return Response(_('No node at path {} for this repository').format(at_path))
346 346
347 347 path_sha = sha1(at_path)[:8]
348 348
349 349 # original backward compat name of archive
350 350 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
351 351 short_sha = safe_str(commit.short_id)
352 352
353 353 if at_path == default_at_path:
354 354 archive_name = '{}-{}{}{}'.format(
355 355 clean_name,
356 356 '-sub' if subrepos else '',
357 357 short_sha,
358 358 ext)
359 359 # custom path and new name
360 360 else:
361 361 archive_name = '{}-{}{}-{}{}'.format(
362 362 clean_name,
363 363 '-sub' if subrepos else '',
364 364 short_sha,
365 365 path_sha,
366 366 ext)
367 367
368 368 use_cached_archive = False
369 369 archive_cache_enabled = CONFIG.get(
370 370 'archive_cache_dir') and not self.request.GET.get('no_cache')
371 371 cached_archive_path = None
372 372
373 373 if archive_cache_enabled:
374 374 # check if we it's ok to write
375 375 if not os.path.isdir(CONFIG['archive_cache_dir']):
376 376 os.makedirs(CONFIG['archive_cache_dir'])
377 377 cached_archive_path = os.path.join(
378 378 CONFIG['archive_cache_dir'], archive_name)
379 379 if os.path.isfile(cached_archive_path):
380 380 log.debug('Found cached archive in %s', cached_archive_path)
381 381 fd, archive = None, cached_archive_path
382 382 use_cached_archive = True
383 383 else:
384 384 log.debug('Archive %s is not yet cached', archive_name)
385 385
386 386 if not use_cached_archive:
387 387 # generate new archive
388 388 fd, archive = tempfile.mkstemp()
389 389 log.debug('Creating new temp archive in %s', archive)
390 390 try:
391 391 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
392 392 archive_at_path=at_path)
393 393 except ImproperArchiveTypeError:
394 394 return _('Unknown archive type')
395 395 if archive_cache_enabled:
396 396 # if we generated the archive and we have cache enabled
397 397 # let's use this for future
398 398 log.debug('Storing new archive in %s', cached_archive_path)
399 399 shutil.move(archive, cached_archive_path)
400 400 archive = cached_archive_path
401 401
402 402 # store download action
403 403 audit_logger.store_web(
404 404 'repo.archive.download', action_data={
405 405 'user_agent': self.request.user_agent,
406 406 'archive_name': archive_name,
407 407 'archive_spec': fname,
408 408 'archive_cached': use_cached_archive},
409 409 user=self._rhodecode_user,
410 410 repo=self.db_repo,
411 411 commit=True
412 412 )
413 413
414 414 def get_chunked_archive(archive_path):
415 415 with open(archive_path, 'rb') as stream:
416 416 while True:
417 417 data = stream.read(16 * 1024)
418 418 if not data:
419 419 if fd: # fd means we used temporary file
420 420 os.close(fd)
421 421 if not archive_cache_enabled:
422 422 log.debug('Destroying temp archive %s', archive_path)
423 423 os.remove(archive_path)
424 424 break
425 425 yield data
426 426
427 427 response = Response(app_iter=get_chunked_archive(archive))
428 428 response.content_disposition = str(
429 429 'attachment; filename=%s' % archive_name)
430 430 response.content_type = str(content_type)
431 431
432 432 return response
433 433
434 434 def _get_file_node(self, commit_id, f_path):
435 435 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
436 436 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
437 437 try:
438 438 node = commit.get_node(f_path)
439 439 if node.is_dir():
440 440 raise NodeError('%s path is a %s not a file'
441 441 % (node, type(node)))
442 442 except NodeDoesNotExistError:
443 443 commit = EmptyCommit(
444 444 commit_id=commit_id,
445 445 idx=commit.idx,
446 446 repo=commit.repository,
447 447 alias=commit.repository.alias,
448 448 message=commit.message,
449 449 author=commit.author,
450 450 date=commit.date)
451 451 node = FileNode(f_path, '', commit=commit)
452 452 else:
453 453 commit = EmptyCommit(
454 454 repo=self.rhodecode_vcs_repo,
455 455 alias=self.rhodecode_vcs_repo.alias)
456 456 node = FileNode(f_path, '', commit=commit)
457 457 return node
458 458
459 459 @LoginRequired()
460 460 @HasRepoPermissionAnyDecorator(
461 461 'repository.read', 'repository.write', 'repository.admin')
462 462 @view_config(
463 463 route_name='repo_files_diff', request_method='GET',
464 464 renderer=None)
465 465 def repo_files_diff(self):
466 466 c = self.load_default_context()
467 467 f_path = self._get_f_path(self.request.matchdict)
468 468 diff1 = self.request.GET.get('diff1', '')
469 469 diff2 = self.request.GET.get('diff2', '')
470 470
471 471 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
472 472
473 473 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
474 474 line_context = self.request.GET.get('context', 3)
475 475
476 476 if not any((diff1, diff2)):
477 477 h.flash(
478 478 'Need query parameter "diff1" or "diff2" to generate a diff.',
479 479 category='error')
480 480 raise HTTPBadRequest()
481 481
482 482 c.action = self.request.GET.get('diff')
483 483 if c.action not in ['download', 'raw']:
484 484 compare_url = h.route_path(
485 485 'repo_compare',
486 486 repo_name=self.db_repo_name,
487 487 source_ref_type='rev',
488 488 source_ref=diff1,
489 489 target_repo=self.db_repo_name,
490 490 target_ref_type='rev',
491 491 target_ref=diff2,
492 492 _query=dict(f_path=f_path))
493 493 # redirect to new view if we render diff
494 494 raise HTTPFound(compare_url)
495 495
496 496 try:
497 497 node1 = self._get_file_node(diff1, path1)
498 498 node2 = self._get_file_node(diff2, f_path)
499 499 except (RepositoryError, NodeError):
500 500 log.exception("Exception while trying to get node from repository")
501 501 raise HTTPFound(
502 502 h.route_path('repo_files', repo_name=self.db_repo_name,
503 503 commit_id='tip', f_path=f_path))
504 504
505 505 if all(isinstance(node.commit, EmptyCommit)
506 506 for node in (node1, node2)):
507 507 raise HTTPNotFound()
508 508
509 509 c.commit_1 = node1.commit
510 510 c.commit_2 = node2.commit
511 511
512 512 if c.action == 'download':
513 513 _diff = diffs.get_gitdiff(node1, node2,
514 514 ignore_whitespace=ignore_whitespace,
515 515 context=line_context)
516 516 diff = diffs.DiffProcessor(_diff, format='gitdiff')
517 517
518 518 response = Response(self.path_filter.get_raw_patch(diff))
519 519 response.content_type = 'text/plain'
520 520 response.content_disposition = (
521 521 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
522 522 )
523 523 charset = self._get_default_encoding(c)
524 524 if charset:
525 525 response.charset = charset
526 526 return response
527 527
528 528 elif c.action == 'raw':
529 529 _diff = diffs.get_gitdiff(node1, node2,
530 530 ignore_whitespace=ignore_whitespace,
531 531 context=line_context)
532 532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 533
534 534 response = Response(self.path_filter.get_raw_patch(diff))
535 535 response.content_type = 'text/plain'
536 536 charset = self._get_default_encoding(c)
537 537 if charset:
538 538 response.charset = charset
539 539 return response
540 540
541 541 # in case we ever end up here
542 542 raise HTTPNotFound()
543 543
544 544 @LoginRequired()
545 545 @HasRepoPermissionAnyDecorator(
546 546 'repository.read', 'repository.write', 'repository.admin')
547 547 @view_config(
548 548 route_name='repo_files_diff_2way_redirect', request_method='GET',
549 549 renderer=None)
550 550 def repo_files_diff_2way_redirect(self):
551 551 """
552 552 Kept only to make OLD links work
553 553 """
554 554 f_path = self._get_f_path_unchecked(self.request.matchdict)
555 555 diff1 = self.request.GET.get('diff1', '')
556 556 diff2 = self.request.GET.get('diff2', '')
557 557
558 558 if not any((diff1, diff2)):
559 559 h.flash(
560 560 'Need query parameter "diff1" or "diff2" to generate a diff.',
561 561 category='error')
562 562 raise HTTPBadRequest()
563 563
564 564 compare_url = h.route_path(
565 565 'repo_compare',
566 566 repo_name=self.db_repo_name,
567 567 source_ref_type='rev',
568 568 source_ref=diff1,
569 569 target_ref_type='rev',
570 570 target_ref=diff2,
571 571 _query=dict(f_path=f_path, diffmode='sideside',
572 572 target_repo=self.db_repo_name,))
573 573 raise HTTPFound(compare_url)
574 574
575 575 @LoginRequired()
576 576 @HasRepoPermissionAnyDecorator(
577 577 'repository.read', 'repository.write', 'repository.admin')
578 578 @view_config(
579 579 route_name='repo_files', request_method='GET',
580 580 renderer=None)
581 581 @view_config(
582 582 route_name='repo_files:default_path', request_method='GET',
583 583 renderer=None)
584 584 @view_config(
585 585 route_name='repo_files:default_commit', request_method='GET',
586 586 renderer=None)
587 587 @view_config(
588 588 route_name='repo_files:rendered', request_method='GET',
589 589 renderer=None)
590 590 @view_config(
591 591 route_name='repo_files:annotated', request_method='GET',
592 592 renderer=None)
593 593 def repo_files(self):
594 594 c = self.load_default_context()
595 595
596 596 view_name = getattr(self.request.matched_route, 'name', None)
597 597
598 598 c.annotate = view_name == 'repo_files:annotated'
599 599 # default is false, but .rst/.md files later are auto rendered, we can
600 600 # overwrite auto rendering by setting this GET flag
601 601 c.renderer = view_name == 'repo_files:rendered' or \
602 602 not self.request.GET.get('no-render', False)
603 603
604 604 # redirect to given commit_id from form if given
605 605 get_commit_id = self.request.GET.get('at_rev', None)
606 606 if get_commit_id:
607 607 self._get_commit_or_redirect(get_commit_id)
608 608
609 609 commit_id, f_path = self._get_commit_and_path()
610 610 c.commit = self._get_commit_or_redirect(commit_id)
611 611 c.branch = self.request.GET.get('branch', None)
612 612 c.f_path = f_path
613 613
614 614 # prev link
615 615 try:
616 616 prev_commit = c.commit.prev(c.branch)
617 617 c.prev_commit = prev_commit
618 618 c.url_prev = h.route_path(
619 619 'repo_files', repo_name=self.db_repo_name,
620 620 commit_id=prev_commit.raw_id, f_path=f_path)
621 621 if c.branch:
622 622 c.url_prev += '?branch=%s' % c.branch
623 623 except (CommitDoesNotExistError, VCSError):
624 624 c.url_prev = '#'
625 625 c.prev_commit = EmptyCommit()
626 626
627 627 # next link
628 628 try:
629 629 next_commit = c.commit.next(c.branch)
630 630 c.next_commit = next_commit
631 631 c.url_next = h.route_path(
632 632 'repo_files', repo_name=self.db_repo_name,
633 633 commit_id=next_commit.raw_id, f_path=f_path)
634 634 if c.branch:
635 635 c.url_next += '?branch=%s' % c.branch
636 636 except (CommitDoesNotExistError, VCSError):
637 637 c.url_next = '#'
638 638 c.next_commit = EmptyCommit()
639 639
640 640 # files or dirs
641 641 try:
642 642 c.file = c.commit.get_node(f_path)
643 643 c.file_author = True
644 644 c.file_tree = ''
645 645
646 646 # load file content
647 647 if c.file.is_file():
648 648 c.lf_node = c.file.get_largefile_node()
649 649
650 650 c.file_source_page = 'true'
651 651 c.file_last_commit = c.file.last_commit
652 652 if c.file.size < c.visual.cut_off_limit_diff:
653 653 if c.annotate: # annotation has precedence over renderer
654 654 c.annotated_lines = filenode_as_annotated_lines_tokens(
655 655 c.file
656 656 )
657 657 else:
658 658 c.renderer = (
659 659 c.renderer and h.renderer_from_filename(c.file.path)
660 660 )
661 661 if not c.renderer:
662 662 c.lines = filenode_as_lines_tokens(c.file)
663 663
664 664 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
665 665 commit_id, self.rhodecode_vcs_repo)
666 666 c.on_branch_head = is_head
667 667
668 668 branch = c.commit.branch if (
669 669 c.commit.branch and '/' not in c.commit.branch) else None
670 670 c.branch_or_raw_id = branch or c.commit.raw_id
671 671 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
672 672
673 673 author = c.file_last_commit.author
674 674 c.authors = [[
675 675 h.email(author),
676 676 h.person(author, 'username_or_name_or_email'),
677 677 1
678 678 ]]
679 679
680 680 else: # load tree content at path
681 681 c.file_source_page = 'false'
682 682 c.authors = []
683 683 # this loads a simple tree without metadata to speed things up
684 684 # later via ajax we call repo_nodetree_full and fetch whole
685 685 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
686 686
687 687 except RepositoryError as e:
688 688 h.flash(safe_str(h.escape(e)), category='error')
689 689 raise HTTPNotFound()
690 690
691 691 if self.request.environ.get('HTTP_X_PJAX'):
692 692 html = render('rhodecode:templates/files/files_pjax.mako',
693 693 self._get_template_context(c), self.request)
694 694 else:
695 695 html = render('rhodecode:templates/files/files.mako',
696 696 self._get_template_context(c), self.request)
697 697 return Response(html)
698 698
699 699 @HasRepoPermissionAnyDecorator(
700 700 'repository.read', 'repository.write', 'repository.admin')
701 701 @view_config(
702 702 route_name='repo_files:annotated_previous', request_method='GET',
703 703 renderer=None)
704 704 def repo_files_annotated_previous(self):
705 705 self.load_default_context()
706 706
707 707 commit_id, f_path = self._get_commit_and_path()
708 708 commit = self._get_commit_or_redirect(commit_id)
709 709 prev_commit_id = commit.raw_id
710 710 line_anchor = self.request.GET.get('line_anchor')
711 711 is_file = False
712 712 try:
713 713 _file = commit.get_node(f_path)
714 714 is_file = _file.is_file()
715 715 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
716 716 pass
717 717
718 718 if is_file:
719 719 history = commit.get_path_history(f_path)
720 720 prev_commit_id = history[1].raw_id \
721 721 if len(history) > 1 else prev_commit_id
722 722 prev_url = h.route_path(
723 723 'repo_files:annotated', repo_name=self.db_repo_name,
724 724 commit_id=prev_commit_id, f_path=f_path,
725 725 _anchor='L{}'.format(line_anchor))
726 726
727 727 raise HTTPFound(prev_url)
728 728
729 729 @LoginRequired()
730 730 @HasRepoPermissionAnyDecorator(
731 731 'repository.read', 'repository.write', 'repository.admin')
732 732 @view_config(
733 733 route_name='repo_nodetree_full', request_method='GET',
734 734 renderer=None, xhr=True)
735 735 @view_config(
736 736 route_name='repo_nodetree_full:default_path', request_method='GET',
737 737 renderer=None, xhr=True)
738 738 def repo_nodetree_full(self):
739 739 """
740 740 Returns rendered html of file tree that contains commit date,
741 741 author, commit_id for the specified combination of
742 742 repo, commit_id and file path
743 743 """
744 744 c = self.load_default_context()
745 745
746 746 commit_id, f_path = self._get_commit_and_path()
747 747 commit = self._get_commit_or_redirect(commit_id)
748 748 try:
749 749 dir_node = commit.get_node(f_path)
750 750 except RepositoryError as e:
751 751 return Response('error: {}'.format(h.escape(safe_str(e))))
752 752
753 753 if dir_node.is_file():
754 754 return Response('')
755 755
756 756 c.file = dir_node
757 757 c.commit = commit
758 758
759 759 html = self._get_tree_at_commit(
760 760 c, commit.raw_id, dir_node.path, full_load=True)
761 761
762 762 return Response(html)
763 763
764 764 def _get_attachement_headers(self, f_path):
765 765 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
766 766 safe_path = f_name.replace('"', '\\"')
767 767 encoded_path = urllib.quote(f_name)
768 768
769 769 return "attachment; " \
770 770 "filename=\"{}\"; " \
771 771 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
772 772
773 773 @LoginRequired()
774 774 @HasRepoPermissionAnyDecorator(
775 775 'repository.read', 'repository.write', 'repository.admin')
776 776 @view_config(
777 777 route_name='repo_file_raw', request_method='GET',
778 778 renderer=None)
779 779 def repo_file_raw(self):
780 780 """
781 781 Action for show as raw, some mimetypes are "rendered",
782 782 those include images, icons.
783 783 """
784 784 c = self.load_default_context()
785 785
786 786 commit_id, f_path = self._get_commit_and_path()
787 787 commit = self._get_commit_or_redirect(commit_id)
788 788 file_node = self._get_filenode_or_redirect(commit, f_path)
789 789
790 790 raw_mimetype_mapping = {
791 791 # map original mimetype to a mimetype used for "show as raw"
792 792 # you can also provide a content-disposition to override the
793 793 # default "attachment" disposition.
794 794 # orig_type: (new_type, new_dispo)
795 795
796 796 # show images inline:
797 797 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
798 798 # for example render an SVG with javascript inside or even render
799 799 # HTML.
800 800 'image/x-icon': ('image/x-icon', 'inline'),
801 801 'image/png': ('image/png', 'inline'),
802 802 'image/gif': ('image/gif', 'inline'),
803 803 'image/jpeg': ('image/jpeg', 'inline'),
804 804 'application/pdf': ('application/pdf', 'inline'),
805 805 }
806 806
807 807 mimetype = file_node.mimetype
808 808 try:
809 809 mimetype, disposition = raw_mimetype_mapping[mimetype]
810 810 except KeyError:
811 811 # we don't know anything special about this, handle it safely
812 812 if file_node.is_binary:
813 813 # do same as download raw for binary files
814 814 mimetype, disposition = 'application/octet-stream', 'attachment'
815 815 else:
816 816 # do not just use the original mimetype, but force text/plain,
817 817 # otherwise it would serve text/html and that might be unsafe.
818 818 # Note: underlying vcs library fakes text/plain mimetype if the
819 819 # mimetype can not be determined and it thinks it is not
820 820 # binary.This might lead to erroneous text display in some
821 821 # cases, but helps in other cases, like with text files
822 822 # without extension.
823 823 mimetype, disposition = 'text/plain', 'inline'
824 824
825 825 if disposition == 'attachment':
826 826 disposition = self._get_attachement_headers(f_path)
827 827
828 828 def stream_node():
829 829 yield file_node.raw_bytes
830 830
831 831 response = Response(app_iter=stream_node())
832 832 response.content_disposition = disposition
833 833 response.content_type = mimetype
834 834
835 835 charset = self._get_default_encoding(c)
836 836 if charset:
837 837 response.charset = charset
838 838
839 839 return response
840 840
841 841 @LoginRequired()
842 842 @HasRepoPermissionAnyDecorator(
843 843 'repository.read', 'repository.write', 'repository.admin')
844 844 @view_config(
845 845 route_name='repo_file_download', request_method='GET',
846 846 renderer=None)
847 847 @view_config(
848 848 route_name='repo_file_download:legacy', request_method='GET',
849 849 renderer=None)
850 850 def repo_file_download(self):
851 851 c = self.load_default_context()
852 852
853 853 commit_id, f_path = self._get_commit_and_path()
854 854 commit = self._get_commit_or_redirect(commit_id)
855 855 file_node = self._get_filenode_or_redirect(commit, f_path)
856 856
857 857 if self.request.GET.get('lf'):
858 858 # only if lf get flag is passed, we download this file
859 859 # as LFS/Largefile
860 860 lf_node = file_node.get_largefile_node()
861 861 if lf_node:
862 862 # overwrite our pointer with the REAL large-file
863 863 file_node = lf_node
864 864
865 865 disposition = self._get_attachement_headers(f_path)
866 866
867 867 def stream_node():
868 868 yield file_node.raw_bytes
869 869
870 870 response = Response(app_iter=stream_node())
871 871 response.content_disposition = disposition
872 872 response.content_type = file_node.mimetype
873 873
874 874 charset = self._get_default_encoding(c)
875 875 if charset:
876 876 response.charset = charset
877 877
878 878 return response
879 879
880 880 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
881 881
882 882 cache_seconds = safe_int(
883 883 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
884 884 cache_on = cache_seconds > 0
885 885 log.debug(
886 886 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
887 887 'with caching: %s[TTL: %ss]' % (
888 888 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
889 889
890 890 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
891 891 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
892 892
893 893 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
894 894 condition=cache_on)
895 895 def compute_file_search(repo_id, commit_id, f_path):
896 896 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
897 897 repo_id, commit_id, f_path)
898 898 try:
899 899 _d, _f = ScmModel().get_nodes(
900 900 repo_name, commit_id, f_path, flat=False)
901 901 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
902 902 log.exception(safe_str(e))
903 903 h.flash(safe_str(h.escape(e)), category='error')
904 904 raise HTTPFound(h.route_path(
905 905 'repo_files', repo_name=self.db_repo_name,
906 906 commit_id='tip', f_path='/'))
907
907 908 return _d + _f
908 909
909 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
910 result = compute_file_search(self.db_repo.repo_id, commit_id, f_path)
911 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
910 912
911 913 @LoginRequired()
912 914 @HasRepoPermissionAnyDecorator(
913 915 'repository.read', 'repository.write', 'repository.admin')
914 916 @view_config(
915 917 route_name='repo_files_nodelist', request_method='GET',
916 918 renderer='json_ext', xhr=True)
917 919 def repo_nodelist(self):
918 920 self.load_default_context()
919 921
920 922 commit_id, f_path = self._get_commit_and_path()
921 923 commit = self._get_commit_or_redirect(commit_id)
922 924
923 925 metadata = self._get_nodelist_at_commit(
924 926 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
925 927 return {'nodes': metadata}
926 928
927 929 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
928 930 items = []
929 931 for name, commit_id in branches_or_tags.items():
930 932 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
931 933 items.append((sym_ref, name, ref_type))
932 934 return items
933 935
934 936 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
935 937 return commit_id
936 938
937 939 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
938 940 new_f_path = vcspath.join(name, f_path)
939 941 return u'%s@%s' % (new_f_path, commit_id)
940 942
941 943 def _get_node_history(self, commit_obj, f_path, commits=None):
942 944 """
943 945 get commit history for given node
944 946
945 947 :param commit_obj: commit to calculate history
946 948 :param f_path: path for node to calculate history for
947 949 :param commits: if passed don't calculate history and take
948 950 commits defined in this list
949 951 """
950 952 _ = self.request.translate
951 953
952 954 # calculate history based on tip
953 955 tip = self.rhodecode_vcs_repo.get_commit()
954 956 if commits is None:
955 957 pre_load = ["author", "branch"]
956 958 try:
957 959 commits = tip.get_path_history(f_path, pre_load=pre_load)
958 960 except (NodeDoesNotExistError, CommitError):
959 961 # this node is not present at tip!
960 962 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
961 963
962 964 history = []
963 965 commits_group = ([], _("Changesets"))
964 966 for commit in commits:
965 967 branch = ' (%s)' % commit.branch if commit.branch else ''
966 968 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
967 969 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
968 970 history.append(commits_group)
969 971
970 972 symbolic_reference = self._symbolic_reference
971 973
972 974 if self.rhodecode_vcs_repo.alias == 'svn':
973 975 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
974 976 f_path, self.rhodecode_vcs_repo)
975 977 if adjusted_f_path != f_path:
976 978 log.debug(
977 979 'Recognized svn tag or branch in file "%s", using svn '
978 980 'specific symbolic references', f_path)
979 981 f_path = adjusted_f_path
980 982 symbolic_reference = self._symbolic_reference_svn
981 983
982 984 branches = self._create_references(
983 985 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
984 986 branches_group = (branches, _("Branches"))
985 987
986 988 tags = self._create_references(
987 989 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
988 990 tags_group = (tags, _("Tags"))
989 991
990 992 history.append(branches_group)
991 993 history.append(tags_group)
992 994
993 995 return history, commits
994 996
995 997 @LoginRequired()
996 998 @HasRepoPermissionAnyDecorator(
997 999 'repository.read', 'repository.write', 'repository.admin')
998 1000 @view_config(
999 1001 route_name='repo_file_history', request_method='GET',
1000 1002 renderer='json_ext')
1001 1003 def repo_file_history(self):
1002 1004 self.load_default_context()
1003 1005
1004 1006 commit_id, f_path = self._get_commit_and_path()
1005 1007 commit = self._get_commit_or_redirect(commit_id)
1006 1008 file_node = self._get_filenode_or_redirect(commit, f_path)
1007 1009
1008 1010 if file_node.is_file():
1009 1011 file_history, _hist = self._get_node_history(commit, f_path)
1010 1012
1011 1013 res = []
1012 1014 for obj in file_history:
1013 1015 res.append({
1014 1016 'text': obj[1],
1015 1017 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
1016 1018 })
1017 1019
1018 1020 data = {
1019 1021 'more': False,
1020 1022 'results': res
1021 1023 }
1022 1024 return data
1023 1025
1024 1026 log.warning('Cannot fetch history for directory')
1025 1027 raise HTTPBadRequest()
1026 1028
1027 1029 @LoginRequired()
1028 1030 @HasRepoPermissionAnyDecorator(
1029 1031 'repository.read', 'repository.write', 'repository.admin')
1030 1032 @view_config(
1031 1033 route_name='repo_file_authors', request_method='GET',
1032 1034 renderer='rhodecode:templates/files/file_authors_box.mako')
1033 1035 def repo_file_authors(self):
1034 1036 c = self.load_default_context()
1035 1037
1036 1038 commit_id, f_path = self._get_commit_and_path()
1037 1039 commit = self._get_commit_or_redirect(commit_id)
1038 1040 file_node = self._get_filenode_or_redirect(commit, f_path)
1039 1041
1040 1042 if not file_node.is_file():
1041 1043 raise HTTPBadRequest()
1042 1044
1043 1045 c.file_last_commit = file_node.last_commit
1044 1046 if self.request.GET.get('annotate') == '1':
1045 1047 # use _hist from annotation if annotation mode is on
1046 1048 commit_ids = set(x[1] for x in file_node.annotate)
1047 1049 _hist = (
1048 1050 self.rhodecode_vcs_repo.get_commit(commit_id)
1049 1051 for commit_id in commit_ids)
1050 1052 else:
1051 1053 _f_history, _hist = self._get_node_history(commit, f_path)
1052 1054 c.file_author = False
1053 1055
1054 1056 unique = collections.OrderedDict()
1055 1057 for commit in _hist:
1056 1058 author = commit.author
1057 1059 if author not in unique:
1058 1060 unique[commit.author] = [
1059 1061 h.email(author),
1060 1062 h.person(author, 'username_or_name_or_email'),
1061 1063 1 # counter
1062 1064 ]
1063 1065
1064 1066 else:
1065 1067 # increase counter
1066 1068 unique[commit.author][2] += 1
1067 1069
1068 1070 c.authors = [val for val in unique.values()]
1069 1071
1070 1072 return self._get_template_context(c)
1071 1073
1072 1074 @LoginRequired()
1073 1075 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1074 1076 @view_config(
1075 1077 route_name='repo_files_remove_file', request_method='GET',
1076 1078 renderer='rhodecode:templates/files/files_delete.mako')
1077 1079 def repo_files_remove_file(self):
1078 1080 _ = self.request.translate
1079 1081 c = self.load_default_context()
1080 1082 commit_id, f_path = self._get_commit_and_path()
1081 1083
1082 1084 self._ensure_not_locked()
1083 1085 _branch_name, _sha_commit_id, is_head = \
1084 1086 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1085 1087
1086 1088 self.forbid_non_head(is_head, f_path)
1087 1089 self.check_branch_permission(_branch_name)
1088 1090
1089 1091 c.commit = self._get_commit_or_redirect(commit_id)
1090 1092 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1091 1093
1092 1094 c.default_message = _(
1093 1095 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1094 1096 c.f_path = f_path
1095 1097
1096 1098 return self._get_template_context(c)
1097 1099
1098 1100 @LoginRequired()
1099 1101 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1100 1102 @CSRFRequired()
1101 1103 @view_config(
1102 1104 route_name='repo_files_delete_file', request_method='POST',
1103 1105 renderer=None)
1104 1106 def repo_files_delete_file(self):
1105 1107 _ = self.request.translate
1106 1108
1107 1109 c = self.load_default_context()
1108 1110 commit_id, f_path = self._get_commit_and_path()
1109 1111
1110 1112 self._ensure_not_locked()
1111 1113 _branch_name, _sha_commit_id, is_head = \
1112 1114 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1113 1115
1114 1116 self.forbid_non_head(is_head, f_path)
1115 1117 self.check_branch_permission(_branch_name)
1116 1118
1117 1119 c.commit = self._get_commit_or_redirect(commit_id)
1118 1120 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1119 1121
1120 1122 c.default_message = _(
1121 1123 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1122 1124 c.f_path = f_path
1123 1125 node_path = f_path
1124 1126 author = self._rhodecode_db_user.full_contact
1125 1127 message = self.request.POST.get('message') or c.default_message
1126 1128 try:
1127 1129 nodes = {
1128 1130 node_path: {
1129 1131 'content': ''
1130 1132 }
1131 1133 }
1132 1134 ScmModel().delete_nodes(
1133 1135 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1134 1136 message=message,
1135 1137 nodes=nodes,
1136 1138 parent_commit=c.commit,
1137 1139 author=author,
1138 1140 )
1139 1141
1140 1142 h.flash(
1141 1143 _('Successfully deleted file `{}`').format(
1142 1144 h.escape(f_path)), category='success')
1143 1145 except Exception:
1144 1146 log.exception('Error during commit operation')
1145 1147 h.flash(_('Error occurred during commit'), category='error')
1146 1148 raise HTTPFound(
1147 1149 h.route_path('repo_commit', repo_name=self.db_repo_name,
1148 1150 commit_id='tip'))
1149 1151
1150 1152 @LoginRequired()
1151 1153 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1152 1154 @view_config(
1153 1155 route_name='repo_files_edit_file', request_method='GET',
1154 1156 renderer='rhodecode:templates/files/files_edit.mako')
1155 1157 def repo_files_edit_file(self):
1156 1158 _ = self.request.translate
1157 1159 c = self.load_default_context()
1158 1160 commit_id, f_path = self._get_commit_and_path()
1159 1161
1160 1162 self._ensure_not_locked()
1161 1163 _branch_name, _sha_commit_id, is_head = \
1162 1164 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1163 1165
1164 1166 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1165 1167 self.check_branch_permission(_branch_name, commit_id=commit_id)
1166 1168
1167 1169 c.commit = self._get_commit_or_redirect(commit_id)
1168 1170 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1169 1171
1170 1172 if c.file.is_binary:
1171 1173 files_url = h.route_path(
1172 1174 'repo_files',
1173 1175 repo_name=self.db_repo_name,
1174 1176 commit_id=c.commit.raw_id, f_path=f_path)
1175 1177 raise HTTPFound(files_url)
1176 1178
1177 1179 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1178 1180 c.f_path = f_path
1179 1181
1180 1182 return self._get_template_context(c)
1181 1183
1182 1184 @LoginRequired()
1183 1185 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1184 1186 @CSRFRequired()
1185 1187 @view_config(
1186 1188 route_name='repo_files_update_file', request_method='POST',
1187 1189 renderer=None)
1188 1190 def repo_files_update_file(self):
1189 1191 _ = self.request.translate
1190 1192 c = self.load_default_context()
1191 1193 commit_id, f_path = self._get_commit_and_path()
1192 1194
1193 1195 self._ensure_not_locked()
1194 1196
1195 1197 c.commit = self._get_commit_or_redirect(commit_id)
1196 1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1197 1199
1198 1200 if c.file.is_binary:
1199 1201 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1200 1202 commit_id=c.commit.raw_id, f_path=f_path))
1201 1203
1202 1204 _branch_name, _sha_commit_id, is_head = \
1203 1205 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1204 1206
1205 1207 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1206 1208 self.check_branch_permission(_branch_name, commit_id=commit_id)
1207 1209
1208 1210 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1209 1211 c.f_path = f_path
1210 1212
1211 1213 old_content = c.file.content
1212 1214 sl = old_content.splitlines(1)
1213 1215 first_line = sl[0] if sl else ''
1214 1216
1215 1217 r_post = self.request.POST
1216 1218 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1217 1219 line_ending_mode = detect_mode(first_line, 0)
1218 1220 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1219 1221
1220 1222 message = r_post.get('message') or c.default_message
1221 1223 org_node_path = c.file.unicode_path
1222 1224 filename = r_post['filename']
1223 1225
1224 1226 root_path = c.file.dir_path
1225 1227 pure_path = self.create_pure_path(root_path, filename)
1226 1228 node_path = safe_unicode(bytes(pure_path))
1227 1229
1228 1230 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1229 1231 commit_id=commit_id)
1230 1232 if content == old_content and node_path == org_node_path:
1231 1233 h.flash(_('No changes detected on {}').format(org_node_path),
1232 1234 category='warning')
1233 1235 raise HTTPFound(default_redirect_url)
1234 1236
1235 1237 try:
1236 1238 mapping = {
1237 1239 org_node_path: {
1238 1240 'org_filename': org_node_path,
1239 1241 'filename': node_path,
1240 1242 'content': content,
1241 1243 'lexer': '',
1242 1244 'op': 'mod',
1243 1245 'mode': c.file.mode
1244 1246 }
1245 1247 }
1246 1248
1247 1249 commit = ScmModel().update_nodes(
1248 1250 user=self._rhodecode_db_user.user_id,
1249 1251 repo=self.db_repo,
1250 1252 message=message,
1251 1253 nodes=mapping,
1252 1254 parent_commit=c.commit,
1253 1255 )
1254 1256
1255 1257 h.flash(_('Successfully committed changes to file `{}`').format(
1256 1258 h.escape(f_path)), category='success')
1257 1259 default_redirect_url = h.route_path(
1258 1260 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1259 1261
1260 1262 except Exception:
1261 1263 log.exception('Error occurred during commit')
1262 1264 h.flash(_('Error occurred during commit'), category='error')
1263 1265
1264 1266 raise HTTPFound(default_redirect_url)
1265 1267
1266 1268 @LoginRequired()
1267 1269 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1268 1270 @view_config(
1269 1271 route_name='repo_files_add_file', request_method='GET',
1270 1272 renderer='rhodecode:templates/files/files_add.mako')
1271 1273 @view_config(
1272 1274 route_name='repo_files_upload_file', request_method='GET',
1273 1275 renderer='rhodecode:templates/files/files_upload.mako')
1274 1276 def repo_files_add_file(self):
1275 1277 _ = self.request.translate
1276 1278 c = self.load_default_context()
1277 1279 commit_id, f_path = self._get_commit_and_path()
1278 1280
1279 1281 self._ensure_not_locked()
1280 1282
1281 1283 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1282 1284 if c.commit is None:
1283 1285 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1284 1286
1285 1287 if self.rhodecode_vcs_repo.is_empty():
1286 1288 # for empty repository we cannot check for current branch, we rely on
1287 1289 # c.commit.branch instead
1288 1290 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1289 1291 else:
1290 1292 _branch_name, _sha_commit_id, is_head = \
1291 1293 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1292 1294
1293 1295 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1294 1296 self.check_branch_permission(_branch_name, commit_id=commit_id)
1295 1297
1296 1298 c.default_message = (_('Added file via RhodeCode Enterprise'))
1297 1299 c.f_path = f_path.lstrip('/') # ensure not relative path
1298 1300
1299 1301 return self._get_template_context(c)
1300 1302
1301 1303 @LoginRequired()
1302 1304 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1303 1305 @CSRFRequired()
1304 1306 @view_config(
1305 1307 route_name='repo_files_create_file', request_method='POST',
1306 1308 renderer=None)
1307 1309 def repo_files_create_file(self):
1308 1310 _ = self.request.translate
1309 1311 c = self.load_default_context()
1310 1312 commit_id, f_path = self._get_commit_and_path()
1311 1313
1312 1314 self._ensure_not_locked()
1313 1315
1314 1316 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1315 1317 if c.commit is None:
1316 1318 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1317 1319
1318 1320 # calculate redirect URL
1319 1321 if self.rhodecode_vcs_repo.is_empty():
1320 1322 default_redirect_url = h.route_path(
1321 1323 'repo_summary', repo_name=self.db_repo_name)
1322 1324 else:
1323 1325 default_redirect_url = h.route_path(
1324 1326 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1325 1327
1326 1328 if self.rhodecode_vcs_repo.is_empty():
1327 1329 # for empty repository we cannot check for current branch, we rely on
1328 1330 # c.commit.branch instead
1329 1331 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1330 1332 else:
1331 1333 _branch_name, _sha_commit_id, is_head = \
1332 1334 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1333 1335
1334 1336 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1335 1337 self.check_branch_permission(_branch_name, commit_id=commit_id)
1336 1338
1337 1339 c.default_message = (_('Added file via RhodeCode Enterprise'))
1338 1340 c.f_path = f_path
1339 1341
1340 1342 r_post = self.request.POST
1341 1343 message = r_post.get('message') or c.default_message
1342 1344 filename = r_post.get('filename')
1343 1345 unix_mode = 0
1344 1346 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1345 1347
1346 1348 if not filename:
1347 1349 # If there's no commit, redirect to repo summary
1348 1350 if type(c.commit) is EmptyCommit:
1349 1351 redirect_url = h.route_path(
1350 1352 'repo_summary', repo_name=self.db_repo_name)
1351 1353 else:
1352 1354 redirect_url = default_redirect_url
1353 1355 h.flash(_('No filename specified'), category='warning')
1354 1356 raise HTTPFound(redirect_url)
1355 1357
1356 1358 root_path = f_path
1357 1359 pure_path = self.create_pure_path(root_path, filename)
1358 1360 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1359 1361
1360 1362 author = self._rhodecode_db_user.full_contact
1361 1363 nodes = {
1362 1364 node_path: {
1363 1365 'content': content
1364 1366 }
1365 1367 }
1366 1368
1367 1369 try:
1368 1370
1369 1371 commit = ScmModel().create_nodes(
1370 1372 user=self._rhodecode_db_user.user_id,
1371 1373 repo=self.db_repo,
1372 1374 message=message,
1373 1375 nodes=nodes,
1374 1376 parent_commit=c.commit,
1375 1377 author=author,
1376 1378 )
1377 1379
1378 1380 h.flash(_('Successfully committed new file `{}`').format(
1379 1381 h.escape(node_path)), category='success')
1380 1382
1381 1383 default_redirect_url = h.route_path(
1382 1384 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1383 1385
1384 1386 except NonRelativePathError:
1385 1387 log.exception('Non Relative path found')
1386 1388 h.flash(_('The location specified must be a relative path and must not '
1387 1389 'contain .. in the path'), category='warning')
1388 1390 raise HTTPFound(default_redirect_url)
1389 1391 except (NodeError, NodeAlreadyExistsError) as e:
1390 1392 h.flash(_(h.escape(e)), category='error')
1391 1393 except Exception:
1392 1394 log.exception('Error occurred during commit')
1393 1395 h.flash(_('Error occurred during commit'), category='error')
1394 1396
1395 1397 raise HTTPFound(default_redirect_url)
1396 1398
1397 1399 @LoginRequired()
1398 1400 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1399 1401 @CSRFRequired()
1400 1402 @view_config(
1401 1403 route_name='repo_files_upload_file', request_method='POST',
1402 1404 renderer='json_ext')
1403 1405 def repo_files_upload_file(self):
1404 1406 _ = self.request.translate
1405 1407 c = self.load_default_context()
1406 1408 commit_id, f_path = self._get_commit_and_path()
1407 1409
1408 1410 self._ensure_not_locked()
1409 1411
1410 1412 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1411 1413 if c.commit is None:
1412 1414 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1413 1415
1414 1416 # calculate redirect URL
1415 1417 if self.rhodecode_vcs_repo.is_empty():
1416 1418 default_redirect_url = h.route_path(
1417 1419 'repo_summary', repo_name=self.db_repo_name)
1418 1420 else:
1419 1421 default_redirect_url = h.route_path(
1420 1422 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1421 1423
1422 1424 if self.rhodecode_vcs_repo.is_empty():
1423 1425 # for empty repository we cannot check for current branch, we rely on
1424 1426 # c.commit.branch instead
1425 1427 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1426 1428 else:
1427 1429 _branch_name, _sha_commit_id, is_head = \
1428 1430 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1429 1431
1430 1432 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1431 1433 if error:
1432 1434 return {
1433 1435 'error': error,
1434 1436 'redirect_url': default_redirect_url
1435 1437 }
1436 1438 error = self.check_branch_permission(_branch_name, json_mode=True)
1437 1439 if error:
1438 1440 return {
1439 1441 'error': error,
1440 1442 'redirect_url': default_redirect_url
1441 1443 }
1442 1444
1443 1445 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1444 1446 c.f_path = f_path
1445 1447
1446 1448 r_post = self.request.POST
1447 1449
1448 1450 message = c.default_message
1449 1451 user_message = r_post.getall('message')
1450 1452 if isinstance(user_message, list) and user_message:
1451 1453 # we take the first from duplicated results if it's not empty
1452 1454 message = user_message[0] if user_message[0] else message
1453 1455
1454 1456 nodes = {}
1455 1457
1456 1458 for file_obj in r_post.getall('files_upload') or []:
1457 1459 content = file_obj.file
1458 1460 filename = file_obj.filename
1459 1461
1460 1462 root_path = f_path
1461 1463 pure_path = self.create_pure_path(root_path, filename)
1462 1464 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1463 1465
1464 1466 nodes[node_path] = {
1465 1467 'content': content
1466 1468 }
1467 1469
1468 1470 if not nodes:
1469 1471 error = 'missing files'
1470 1472 return {
1471 1473 'error': error,
1472 1474 'redirect_url': default_redirect_url
1473 1475 }
1474 1476
1475 1477 author = self._rhodecode_db_user.full_contact
1476 1478
1477 1479 try:
1478 1480 commit = ScmModel().create_nodes(
1479 1481 user=self._rhodecode_db_user.user_id,
1480 1482 repo=self.db_repo,
1481 1483 message=message,
1482 1484 nodes=nodes,
1483 1485 parent_commit=c.commit,
1484 1486 author=author,
1485 1487 )
1486 1488 if len(nodes) == 1:
1487 1489 flash_message = _('Successfully committed {} new files').format(len(nodes))
1488 1490 else:
1489 1491 flash_message = _('Successfully committed 1 new file')
1490 1492
1491 1493 h.flash(flash_message, category='success')
1492 1494
1493 1495 default_redirect_url = h.route_path(
1494 1496 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1495 1497
1496 1498 except NonRelativePathError:
1497 1499 log.exception('Non Relative path found')
1498 1500 error = _('The location specified must be a relative path and must not '
1499 1501 'contain .. in the path')
1500 1502 h.flash(error, category='warning')
1501 1503
1502 1504 return {
1503 1505 'error': error,
1504 1506 'redirect_url': default_redirect_url
1505 1507 }
1506 1508 except (NodeError, NodeAlreadyExistsError) as e:
1507 1509 error = h.escape(e)
1508 1510 h.flash(error, category='error')
1509 1511
1510 1512 return {
1511 1513 'error': error,
1512 1514 'redirect_url': default_redirect_url
1513 1515 }
1514 1516 except Exception:
1515 1517 log.exception('Error occurred during commit')
1516 1518 error = _('Error occurred during commit')
1517 1519 h.flash(error, category='error')
1518 1520 return {
1519 1521 'error': error,
1520 1522 'redirect_url': default_redirect_url
1521 1523 }
1522 1524
1523 1525 return {
1524 1526 'error': None,
1525 1527 'redirect_url': default_redirect_url
1526 1528 }
General Comments 0
You need to be logged in to leave comments. Login now