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