##// END OF EJS Templates
files: only check for git_lfs/hg_largefiles if they are enabled....
marcink -
r3894:22ee809d default
parent child Browse files
Show More
@@ -1,734 +1,739 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid import compat
26 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 27
28 28 from rhodecode.lib import helpers as h, diffs
29 29 from rhodecode.lib.utils2 import (
30 30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
32 32 from rhodecode.model import repo
33 33 from rhodecode.model import repo_group
34 34 from rhodecode.model import user_group
35 35 from rhodecode.model import user
36 36 from rhodecode.model.db import User
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.settings import VcsSettingsModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 ADMIN_PREFIX = '/_admin'
44 44 STATIC_FILE_PREFIX = '/_static'
45 45
46 46 URL_NAME_REQUIREMENTS = {
47 47 # group name can have a slash in them, but they must not end with a slash
48 48 'group_name': r'.*?[^/]',
49 49 'repo_group_name': r'.*?[^/]',
50 50 # repo names can have a slash in them, but they must not end with a slash
51 51 'repo_name': r'.*?[^/]',
52 52 # file path eats up everything at the end
53 53 'f_path': r'.*',
54 54 # reference types
55 55 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
56 56 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
57 57 }
58 58
59 59
60 60 def add_route_with_slash(config,name, pattern, **kw):
61 61 config.add_route(name, pattern, **kw)
62 62 if not pattern.endswith('/'):
63 63 config.add_route(name + '_slash', pattern + '/', **kw)
64 64
65 65
66 66 def add_route_requirements(route_path, requirements=None):
67 67 """
68 68 Adds regex requirements to pyramid routes using a mapping dict
69 69 e.g::
70 70 add_route_requirements('{repo_name}/settings')
71 71 """
72 72 requirements = requirements or URL_NAME_REQUIREMENTS
73 73 for key, regex in requirements.items():
74 74 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
75 75 return route_path
76 76
77 77
78 78 def get_format_ref_id(repo):
79 79 """Returns a `repo` specific reference formatter function"""
80 80 if h.is_svn(repo):
81 81 return _format_ref_id_svn
82 82 else:
83 83 return _format_ref_id
84 84
85 85
86 86 def _format_ref_id(name, raw_id):
87 87 """Default formatting of a given reference `name`"""
88 88 return name
89 89
90 90
91 91 def _format_ref_id_svn(name, raw_id):
92 92 """Special way of formatting a reference for Subversion including path"""
93 93 return '%s@%s' % (name, raw_id)
94 94
95 95
96 96 class TemplateArgs(StrictAttributeDict):
97 97 pass
98 98
99 99
100 100 class BaseAppView(object):
101 101
102 102 def __init__(self, context, request):
103 103 self.request = request
104 104 self.context = context
105 105 self.session = request.session
106 106 if not hasattr(request, 'user'):
107 107 # NOTE(marcink): edge case, we ended up in matched route
108 108 # but probably of web-app context, e.g API CALL/VCS CALL
109 109 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
110 110 log.warning('Unable to process request `%s` in this scope', request)
111 111 raise HTTPBadRequest()
112 112
113 113 self._rhodecode_user = request.user # auth user
114 114 self._rhodecode_db_user = self._rhodecode_user.get_instance()
115 115 self._maybe_needs_password_change(
116 116 request.matched_route.name, self._rhodecode_db_user)
117 117
118 118 def _maybe_needs_password_change(self, view_name, user_obj):
119 119 log.debug('Checking if user %s needs password change on view %s',
120 120 user_obj, view_name)
121 121 skip_user_views = [
122 122 'logout', 'login',
123 123 'my_account_password', 'my_account_password_update'
124 124 ]
125 125
126 126 if not user_obj:
127 127 return
128 128
129 129 if user_obj.username == User.DEFAULT_USER:
130 130 return
131 131
132 132 now = time.time()
133 133 should_change = user_obj.user_data.get('force_password_change')
134 134 change_after = safe_int(should_change) or 0
135 135 if should_change and now > change_after:
136 136 log.debug('User %s requires password change', user_obj)
137 137 h.flash('You are required to change your password', 'warning',
138 138 ignore_duplicate=True)
139 139
140 140 if view_name not in skip_user_views:
141 141 raise HTTPFound(
142 142 self.request.route_path('my_account_password'))
143 143
144 144 def _log_creation_exception(self, e, repo_name):
145 145 _ = self.request.translate
146 146 reason = None
147 147 if len(e.args) == 2:
148 148 reason = e.args[1]
149 149
150 150 if reason == 'INVALID_CERTIFICATE':
151 151 log.exception(
152 152 'Exception creating a repository: invalid certificate')
153 153 msg = (_('Error creating repository %s: invalid certificate')
154 154 % repo_name)
155 155 else:
156 156 log.exception("Exception creating a repository")
157 157 msg = (_('Error creating repository %s')
158 158 % repo_name)
159 159 return msg
160 160
161 161 def _get_local_tmpl_context(self, include_app_defaults=True):
162 162 c = TemplateArgs()
163 163 c.auth_user = self.request.user
164 164 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
165 165 c.rhodecode_user = self.request.user
166 166
167 167 if include_app_defaults:
168 168 from rhodecode.lib.base import attach_context_attributes
169 169 attach_context_attributes(c, self.request, self.request.user.user_id)
170 170
171 171 c.is_super_admin = c.auth_user.is_admin
172 172
173 173 c.can_create_repo = c.is_super_admin
174 174 c.can_create_repo_group = c.is_super_admin
175 175 c.can_create_user_group = c.is_super_admin
176 176
177 177 c.is_delegated_admin = False
178 178
179 179 if not c.auth_user.is_default and not c.is_super_admin:
180 180 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
181 181 user=self.request.user)
182 182 repositories = c.auth_user.repositories_admin or c.can_create_repo
183 183
184 184 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
185 185 user=self.request.user)
186 186 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
187 187
188 188 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
189 189 user=self.request.user)
190 190 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
191 191 # delegated admin can create, or manage some objects
192 192 c.is_delegated_admin = repositories or repository_groups or user_groups
193 193 return c
194 194
195 195 def _get_template_context(self, tmpl_args, **kwargs):
196 196
197 197 local_tmpl_args = {
198 198 'defaults': {},
199 199 'errors': {},
200 200 'c': tmpl_args
201 201 }
202 202 local_tmpl_args.update(kwargs)
203 203 return local_tmpl_args
204 204
205 205 def load_default_context(self):
206 206 """
207 207 example:
208 208
209 209 def load_default_context(self):
210 210 c = self._get_local_tmpl_context()
211 211 c.custom_var = 'foobar'
212 212
213 213 return c
214 214 """
215 215 raise NotImplementedError('Needs implementation in view class')
216 216
217 217
218 218 class RepoAppView(BaseAppView):
219 219
220 220 def __init__(self, context, request):
221 221 super(RepoAppView, self).__init__(context, request)
222 222 self.db_repo = request.db_repo
223 223 self.db_repo_name = self.db_repo.repo_name
224 224 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
225 225
226 226 def _handle_missing_requirements(self, error):
227 227 log.error(
228 228 'Requirements are missing for repository %s: %s',
229 229 self.db_repo_name, safe_unicode(error))
230 230
231 231 def _get_local_tmpl_context(self, include_app_defaults=True):
232 232 _ = self.request.translate
233 233 c = super(RepoAppView, self)._get_local_tmpl_context(
234 234 include_app_defaults=include_app_defaults)
235 235
236 236 # register common vars for this type of view
237 237 c.rhodecode_db_repo = self.db_repo
238 238 c.repo_name = self.db_repo_name
239 239 c.repository_pull_requests = self.db_repo_pull_requests
240 240 c.repository_is_user_following = ScmModel().is_following_repo(
241 241 self.db_repo_name, self._rhodecode_user.user_id)
242 242 self.path_filter = PathFilter(None)
243 243
244 244 c.repository_requirements_missing = {}
245 245 try:
246 246 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
247 247 # NOTE(marcink):
248 248 # comparison to None since if it's an object __bool__ is expensive to
249 249 # calculate
250 250 if self.rhodecode_vcs_repo is not None:
251 251 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
252 252 c.auth_user.username)
253 253 self.path_filter = PathFilter(path_perms)
254 254 except RepositoryRequirementError as e:
255 255 c.repository_requirements_missing = {'error': str(e)}
256 256 self._handle_missing_requirements(e)
257 257 self.rhodecode_vcs_repo = None
258 258
259 259 c.path_filter = self.path_filter # used by atom_feed_entry.mako
260 260
261 261 if self.rhodecode_vcs_repo is None:
262 262 # unable to fetch this repo as vcs instance, report back to user
263 263 h.flash(_(
264 264 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
265 265 "Please check if it exist, or is not damaged.") %
266 266 {'repo_name': c.repo_name},
267 267 category='error', ignore_duplicate=True)
268 268 if c.repository_requirements_missing:
269 269 route = self.request.matched_route.name
270 270 if route.startswith(('edit_repo', 'repo_summary')):
271 271 # allow summary and edit repo on missing requirements
272 272 return c
273 273
274 274 raise HTTPFound(
275 275 h.route_path('repo_summary', repo_name=self.db_repo_name))
276 276
277 277 else: # redirect if we don't show missing requirements
278 278 raise HTTPFound(h.route_path('home'))
279 279
280 280 c.has_origin_repo_read_perm = False
281 281 if self.db_repo.fork:
282 282 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
283 283 'repository.write', 'repository.read', 'repository.admin')(
284 284 self.db_repo.fork.repo_name, 'summary fork link')
285 285
286 286 return c
287 287
288 288 def _get_f_path_unchecked(self, matchdict, default=None):
289 289 """
290 290 Should only be used by redirects, everything else should call _get_f_path
291 291 """
292 292 f_path = matchdict.get('f_path')
293 293 if f_path:
294 294 # fix for multiple initial slashes that causes errors for GIT
295 295 return f_path.lstrip('/')
296 296
297 297 return default
298 298
299 299 def _get_f_path(self, matchdict, default=None):
300 300 f_path_match = self._get_f_path_unchecked(matchdict, default)
301 301 return self.path_filter.assert_path_permissions(f_path_match)
302 302
303 303 def _get_general_setting(self, target_repo, settings_key, default=False):
304 304 settings_model = VcsSettingsModel(repo=target_repo)
305 305 settings = settings_model.get_general_settings()
306 306 return settings.get(settings_key, default)
307 307
308 def _get_repo_setting(self, target_repo, settings_key, default=False):
309 settings_model = VcsSettingsModel(repo=target_repo)
310 settings = settings_model.get_repo_settings_inherited()
311 return settings.get(settings_key, default)
312
308 313 def get_recache_flag(self):
309 314 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
310 315 flag_val = self.request.GET.get(flag_name)
311 316 if str2bool(flag_val):
312 317 return True
313 318 return False
314 319
315 320
316 321 class PathFilter(object):
317 322
318 323 # Expects and instance of BasePathPermissionChecker or None
319 324 def __init__(self, permission_checker):
320 325 self.permission_checker = permission_checker
321 326
322 327 def assert_path_permissions(self, path):
323 328 if self.path_access_allowed(path):
324 329 return path
325 330 raise HTTPForbidden()
326 331
327 332 def path_access_allowed(self, path):
328 333 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
329 334 if self.permission_checker:
330 335 return path and self.permission_checker.has_access(path)
331 336 return True
332 337
333 338 def filter_patchset(self, patchset):
334 339 if not self.permission_checker or not patchset:
335 340 return patchset, False
336 341 had_filtered = False
337 342 filtered_patchset = []
338 343 for patch in patchset:
339 344 filename = patch.get('filename', None)
340 345 if not filename or self.permission_checker.has_access(filename):
341 346 filtered_patchset.append(patch)
342 347 else:
343 348 had_filtered = True
344 349 if had_filtered:
345 350 if isinstance(patchset, diffs.LimitedDiffContainer):
346 351 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
347 352 return filtered_patchset, True
348 353 else:
349 354 return patchset, False
350 355
351 356 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
352 357 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
353 358 result = diffset.render_patchset(
354 359 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
355 360 result.has_hidden_changes = has_hidden_changes
356 361 return result
357 362
358 363 def get_raw_patch(self, diff_processor):
359 364 if self.permission_checker is None:
360 365 return diff_processor.as_raw()
361 366 elif self.permission_checker.has_full_access:
362 367 return diff_processor.as_raw()
363 368 else:
364 369 return '# Repository has user-specific filters, raw patch generation is disabled.'
365 370
366 371 @property
367 372 def is_enabled(self):
368 373 return self.permission_checker is not None
369 374
370 375
371 376 class RepoGroupAppView(BaseAppView):
372 377 def __init__(self, context, request):
373 378 super(RepoGroupAppView, self).__init__(context, request)
374 379 self.db_repo_group = request.db_repo_group
375 380 self.db_repo_group_name = self.db_repo_group.group_name
376 381
377 382 def _get_local_tmpl_context(self, include_app_defaults=True):
378 383 _ = self.request.translate
379 384 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
380 385 include_app_defaults=include_app_defaults)
381 386 c.repo_group = self.db_repo_group
382 387 return c
383 388
384 389 def _revoke_perms_on_yourself(self, form_result):
385 390 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
386 391 form_result['perm_updates'])
387 392 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
388 393 form_result['perm_additions'])
389 394 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
390 395 form_result['perm_deletions'])
391 396 admin_perm = 'group.admin'
392 397 if _updates and _updates[0][1] != admin_perm or \
393 398 _additions and _additions[0][1] != admin_perm or \
394 399 _deletions and _deletions[0][1] != admin_perm:
395 400 return True
396 401 return False
397 402
398 403
399 404 class UserGroupAppView(BaseAppView):
400 405 def __init__(self, context, request):
401 406 super(UserGroupAppView, self).__init__(context, request)
402 407 self.db_user_group = request.db_user_group
403 408 self.db_user_group_name = self.db_user_group.users_group_name
404 409
405 410
406 411 class UserAppView(BaseAppView):
407 412 def __init__(self, context, request):
408 413 super(UserAppView, self).__init__(context, request)
409 414 self.db_user = request.db_user
410 415 self.db_user_id = self.db_user.user_id
411 416
412 417 _ = self.request.translate
413 418 if not request.db_user_supports_default:
414 419 if self.db_user.username == User.DEFAULT_USER:
415 420 h.flash(_("Editing user `{}` is disabled.".format(
416 421 User.DEFAULT_USER)), category='warning')
417 422 raise HTTPFound(h.route_path('users'))
418 423
419 424
420 425 class DataGridAppView(object):
421 426 """
422 427 Common class to have re-usable grid rendering components
423 428 """
424 429
425 430 def _extract_ordering(self, request, column_map=None):
426 431 column_map = column_map or {}
427 432 column_index = safe_int(request.GET.get('order[0][column]'))
428 433 order_dir = request.GET.get(
429 434 'order[0][dir]', 'desc')
430 435 order_by = request.GET.get(
431 436 'columns[%s][data][sort]' % column_index, 'name_raw')
432 437
433 438 # translate datatable to DB columns
434 439 order_by = column_map.get(order_by) or order_by
435 440
436 441 search_q = request.GET.get('search[value]')
437 442 return search_q, order_by, order_dir
438 443
439 444 def _extract_chunk(self, request):
440 445 start = safe_int(request.GET.get('start'), 0)
441 446 length = safe_int(request.GET.get('length'), 25)
442 447 draw = safe_int(request.GET.get('draw'))
443 448 return draw, start, length
444 449
445 450 def _get_order_col(self, order_by, model):
446 451 if isinstance(order_by, compat.string_types):
447 452 try:
448 453 return operator.attrgetter(order_by)(model)
449 454 except AttributeError:
450 455 return None
451 456 else:
452 457 return order_by
453 458
454 459
455 460 class BaseReferencesView(RepoAppView):
456 461 """
457 462 Base for reference view for branches, tags and bookmarks.
458 463 """
459 464 def load_default_context(self):
460 465 c = self._get_local_tmpl_context()
461 466
462 467
463 468 return c
464 469
465 470 def load_refs_context(self, ref_items, partials_template):
466 471 _render = self.request.get_partial_renderer(partials_template)
467 472 pre_load = ["author", "date", "message", "parents"]
468 473
469 474 is_svn = h.is_svn(self.rhodecode_vcs_repo)
470 475 is_hg = h.is_hg(self.rhodecode_vcs_repo)
471 476
472 477 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
473 478
474 479 closed_refs = {}
475 480 if is_hg:
476 481 closed_refs = self.rhodecode_vcs_repo.branches_closed
477 482
478 483 data = []
479 484 for ref_name, commit_id in ref_items:
480 485 commit = self.rhodecode_vcs_repo.get_commit(
481 486 commit_id=commit_id, pre_load=pre_load)
482 487 closed = ref_name in closed_refs
483 488
484 489 # TODO: johbo: Unify generation of reference links
485 490 use_commit_id = '/' in ref_name or is_svn
486 491
487 492 if use_commit_id:
488 493 files_url = h.route_path(
489 494 'repo_files',
490 495 repo_name=self.db_repo_name,
491 496 f_path=ref_name if is_svn else '',
492 497 commit_id=commit_id)
493 498
494 499 else:
495 500 files_url = h.route_path(
496 501 'repo_files',
497 502 repo_name=self.db_repo_name,
498 503 f_path=ref_name if is_svn else '',
499 504 commit_id=ref_name,
500 505 _query=dict(at=ref_name))
501 506
502 507 data.append({
503 508 "name": _render('name', ref_name, files_url, closed),
504 509 "name_raw": ref_name,
505 510 "date": _render('date', commit.date),
506 511 "date_raw": datetime_to_time(commit.date),
507 512 "author": _render('author', commit.author),
508 513 "commit": _render(
509 514 'commit', commit.message, commit.raw_id, commit.idx),
510 515 "commit_raw": commit.idx,
511 516 "compare": _render(
512 517 'compare', format_ref_id(ref_name, commit.raw_id)),
513 518 })
514 519
515 520 return data
516 521
517 522
518 523 class RepoRoutePredicate(object):
519 524 def __init__(self, val, config):
520 525 self.val = val
521 526
522 527 def text(self):
523 528 return 'repo_route = %s' % self.val
524 529
525 530 phash = text
526 531
527 532 def __call__(self, info, request):
528 533 if hasattr(request, 'vcs_call'):
529 534 # skip vcs calls
530 535 return
531 536
532 537 repo_name = info['match']['repo_name']
533 538 repo_model = repo.RepoModel()
534 539
535 540 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
536 541
537 542 def redirect_if_creating(route_info, db_repo):
538 543 skip_views = ['edit_repo_advanced_delete']
539 544 route = route_info['route']
540 545 # we should skip delete view so we can actually "remove" repositories
541 546 # if they get stuck in creating state.
542 547 if route.name in skip_views:
543 548 return
544 549
545 550 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
546 551 repo_creating_url = request.route_path(
547 552 'repo_creating', repo_name=db_repo.repo_name)
548 553 raise HTTPFound(repo_creating_url)
549 554
550 555 if by_name_match:
551 556 # register this as request object we can re-use later
552 557 request.db_repo = by_name_match
553 558 redirect_if_creating(info, by_name_match)
554 559 return True
555 560
556 561 by_id_match = repo_model.get_repo_by_id(repo_name)
557 562 if by_id_match:
558 563 request.db_repo = by_id_match
559 564 redirect_if_creating(info, by_id_match)
560 565 return True
561 566
562 567 return False
563 568
564 569
565 570 class RepoForbidArchivedRoutePredicate(object):
566 571 def __init__(self, val, config):
567 572 self.val = val
568 573
569 574 def text(self):
570 575 return 'repo_forbid_archived = %s' % self.val
571 576
572 577 phash = text
573 578
574 579 def __call__(self, info, request):
575 580 _ = request.translate
576 581 rhodecode_db_repo = request.db_repo
577 582
578 583 log.debug(
579 584 '%s checking if archived flag for repo for %s',
580 585 self.__class__.__name__, rhodecode_db_repo.repo_name)
581 586
582 587 if rhodecode_db_repo.archived:
583 588 log.warning('Current view is not supported for archived repo:%s',
584 589 rhodecode_db_repo.repo_name)
585 590
586 591 h.flash(
587 592 h.literal(_('Action not supported for archived repository.')),
588 593 category='warning')
589 594 summary_url = request.route_path(
590 595 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
591 596 raise HTTPFound(summary_url)
592 597 return True
593 598
594 599
595 600 class RepoTypeRoutePredicate(object):
596 601 def __init__(self, val, config):
597 602 self.val = val or ['hg', 'git', 'svn']
598 603
599 604 def text(self):
600 605 return 'repo_accepted_type = %s' % self.val
601 606
602 607 phash = text
603 608
604 609 def __call__(self, info, request):
605 610 if hasattr(request, 'vcs_call'):
606 611 # skip vcs calls
607 612 return
608 613
609 614 rhodecode_db_repo = request.db_repo
610 615
611 616 log.debug(
612 617 '%s checking repo type for %s in %s',
613 618 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
614 619
615 620 if rhodecode_db_repo.repo_type in self.val:
616 621 return True
617 622 else:
618 623 log.warning('Current view is not supported for repo type:%s',
619 624 rhodecode_db_repo.repo_type)
620 625 return False
621 626
622 627
623 628 class RepoGroupRoutePredicate(object):
624 629 def __init__(self, val, config):
625 630 self.val = val
626 631
627 632 def text(self):
628 633 return 'repo_group_route = %s' % self.val
629 634
630 635 phash = text
631 636
632 637 def __call__(self, info, request):
633 638 if hasattr(request, 'vcs_call'):
634 639 # skip vcs calls
635 640 return
636 641
637 642 repo_group_name = info['match']['repo_group_name']
638 643 repo_group_model = repo_group.RepoGroupModel()
639 644 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
640 645
641 646 if by_name_match:
642 647 # register this as request object we can re-use later
643 648 request.db_repo_group = by_name_match
644 649 return True
645 650
646 651 return False
647 652
648 653
649 654 class UserGroupRoutePredicate(object):
650 655 def __init__(self, val, config):
651 656 self.val = val
652 657
653 658 def text(self):
654 659 return 'user_group_route = %s' % self.val
655 660
656 661 phash = text
657 662
658 663 def __call__(self, info, request):
659 664 if hasattr(request, 'vcs_call'):
660 665 # skip vcs calls
661 666 return
662 667
663 668 user_group_id = info['match']['user_group_id']
664 669 user_group_model = user_group.UserGroup()
665 670 by_id_match = user_group_model.get(user_group_id, cache=False)
666 671
667 672 if by_id_match:
668 673 # register this as request object we can re-use later
669 674 request.db_user_group = by_id_match
670 675 return True
671 676
672 677 return False
673 678
674 679
675 680 class UserRoutePredicateBase(object):
676 681 supports_default = None
677 682
678 683 def __init__(self, val, config):
679 684 self.val = val
680 685
681 686 def text(self):
682 687 raise NotImplementedError()
683 688
684 689 def __call__(self, info, request):
685 690 if hasattr(request, 'vcs_call'):
686 691 # skip vcs calls
687 692 return
688 693
689 694 user_id = info['match']['user_id']
690 695 user_model = user.User()
691 696 by_id_match = user_model.get(user_id, cache=False)
692 697
693 698 if by_id_match:
694 699 # register this as request object we can re-use later
695 700 request.db_user = by_id_match
696 701 request.db_user_supports_default = self.supports_default
697 702 return True
698 703
699 704 return False
700 705
701 706
702 707 class UserRoutePredicate(UserRoutePredicateBase):
703 708 supports_default = False
704 709
705 710 def text(self):
706 711 return 'user_route = %s' % self.val
707 712
708 713 phash = text
709 714
710 715
711 716 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
712 717 supports_default = True
713 718
714 719 def text(self):
715 720 return 'user_with_default_route = %s' % self.val
716 721
717 722 phash = text
718 723
719 724
720 725 def includeme(config):
721 726 config.add_route_predicate(
722 727 'repo_route', RepoRoutePredicate)
723 728 config.add_route_predicate(
724 729 'repo_accepted_types', RepoTypeRoutePredicate)
725 730 config.add_route_predicate(
726 731 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
727 732 config.add_route_predicate(
728 733 'repo_group_route', RepoGroupRoutePredicate)
729 734 config.add_route_predicate(
730 735 'user_group_route', UserGroupRoutePredicate)
731 736 config.add_route_predicate(
732 737 'user_route_with_default', UserRouteWithDefaultPredicate)
733 738 config.add_route_predicate(
734 739 'user_route', UserRoutePredicate)
@@ -1,1528 +1,1547 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 def _is_lf_enabled(self, target_repo):
309 lf_enabled = False
310
311 lf_key_for_vcs_map = {
312 'hg': 'extensions_largefiles',
313 'git': 'vcs_git_lfs_enabled'
314 }
315
316 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
317
318 if lf_key_for_vcs:
319 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
320
321 return lf_enabled
322
308 323 @LoginRequired()
309 324 @HasRepoPermissionAnyDecorator(
310 325 'repository.read', 'repository.write', 'repository.admin')
311 326 @view_config(
312 327 route_name='repo_archivefile', request_method='GET',
313 328 renderer=None)
314 329 def repo_archivefile(self):
315 330 # archive cache config
316 331 from rhodecode import CONFIG
317 332 _ = self.request.translate
318 333 self.load_default_context()
319 334 default_at_path = '/'
320 335 fname = self.request.matchdict['fname']
321 336 subrepos = self.request.GET.get('subrepos') == 'true'
322 337 at_path = self.request.GET.get('at_path') or default_at_path
323 338
324 339 if not self.db_repo.enable_downloads:
325 340 return Response(_('Downloads disabled'))
326 341
327 342 try:
328 343 commit_id, ext, fileformat, content_type = \
329 344 self._get_archive_spec(fname)
330 345 except ValueError:
331 346 return Response(_('Unknown archive type for: `{}`').format(
332 347 h.escape(fname)))
333 348
334 349 try:
335 350 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
336 351 except CommitDoesNotExistError:
337 352 return Response(_('Unknown commit_id {}').format(
338 353 h.escape(commit_id)))
339 354 except EmptyRepositoryError:
340 355 return Response(_('Empty repository'))
341 356
342 357 try:
343 358 at_path = commit.get_node(at_path).path or default_at_path
344 359 except Exception:
345 360 return Response(_('No node at path {} for this repository').format(at_path))
346 361
347 362 path_sha = sha1(at_path)[:8]
348 363
349 364 # original backward compat name of archive
350 365 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
351 366 short_sha = safe_str(commit.short_id)
352 367
353 368 if at_path == default_at_path:
354 369 archive_name = '{}-{}{}{}'.format(
355 370 clean_name,
356 371 '-sub' if subrepos else '',
357 372 short_sha,
358 373 ext)
359 374 # custom path and new name
360 375 else:
361 376 archive_name = '{}-{}{}-{}{}'.format(
362 377 clean_name,
363 378 '-sub' if subrepos else '',
364 379 short_sha,
365 380 path_sha,
366 381 ext)
367 382
368 383 use_cached_archive = False
369 384 archive_cache_enabled = CONFIG.get(
370 385 'archive_cache_dir') and not self.request.GET.get('no_cache')
371 386 cached_archive_path = None
372 387
373 388 if archive_cache_enabled:
374 389 # check if we it's ok to write
375 390 if not os.path.isdir(CONFIG['archive_cache_dir']):
376 391 os.makedirs(CONFIG['archive_cache_dir'])
377 392 cached_archive_path = os.path.join(
378 393 CONFIG['archive_cache_dir'], archive_name)
379 394 if os.path.isfile(cached_archive_path):
380 395 log.debug('Found cached archive in %s', cached_archive_path)
381 396 fd, archive = None, cached_archive_path
382 397 use_cached_archive = True
383 398 else:
384 399 log.debug('Archive %s is not yet cached', archive_name)
385 400
386 401 if not use_cached_archive:
387 402 # generate new archive
388 403 fd, archive = tempfile.mkstemp()
389 404 log.debug('Creating new temp archive in %s', archive)
390 405 try:
391 406 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
392 407 archive_at_path=at_path)
393 408 except ImproperArchiveTypeError:
394 409 return _('Unknown archive type')
395 410 if archive_cache_enabled:
396 411 # if we generated the archive and we have cache enabled
397 412 # let's use this for future
398 413 log.debug('Storing new archive in %s', cached_archive_path)
399 414 shutil.move(archive, cached_archive_path)
400 415 archive = cached_archive_path
401 416
402 417 # store download action
403 418 audit_logger.store_web(
404 419 'repo.archive.download', action_data={
405 420 'user_agent': self.request.user_agent,
406 421 'archive_name': archive_name,
407 422 'archive_spec': fname,
408 423 'archive_cached': use_cached_archive},
409 424 user=self._rhodecode_user,
410 425 repo=self.db_repo,
411 426 commit=True
412 427 )
413 428
414 429 def get_chunked_archive(archive_path):
415 430 with open(archive_path, 'rb') as stream:
416 431 while True:
417 432 data = stream.read(16 * 1024)
418 433 if not data:
419 434 if fd: # fd means we used temporary file
420 435 os.close(fd)
421 436 if not archive_cache_enabled:
422 437 log.debug('Destroying temp archive %s', archive_path)
423 438 os.remove(archive_path)
424 439 break
425 440 yield data
426 441
427 442 response = Response(app_iter=get_chunked_archive(archive))
428 443 response.content_disposition = str(
429 444 'attachment; filename=%s' % archive_name)
430 445 response.content_type = str(content_type)
431 446
432 447 return response
433 448
434 449 def _get_file_node(self, commit_id, f_path):
435 450 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
436 451 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
437 452 try:
438 453 node = commit.get_node(f_path)
439 454 if node.is_dir():
440 455 raise NodeError('%s path is a %s not a file'
441 456 % (node, type(node)))
442 457 except NodeDoesNotExistError:
443 458 commit = EmptyCommit(
444 459 commit_id=commit_id,
445 460 idx=commit.idx,
446 461 repo=commit.repository,
447 462 alias=commit.repository.alias,
448 463 message=commit.message,
449 464 author=commit.author,
450 465 date=commit.date)
451 466 node = FileNode(f_path, '', commit=commit)
452 467 else:
453 468 commit = EmptyCommit(
454 469 repo=self.rhodecode_vcs_repo,
455 470 alias=self.rhodecode_vcs_repo.alias)
456 471 node = FileNode(f_path, '', commit=commit)
457 472 return node
458 473
459 474 @LoginRequired()
460 475 @HasRepoPermissionAnyDecorator(
461 476 'repository.read', 'repository.write', 'repository.admin')
462 477 @view_config(
463 478 route_name='repo_files_diff', request_method='GET',
464 479 renderer=None)
465 480 def repo_files_diff(self):
466 481 c = self.load_default_context()
467 482 f_path = self._get_f_path(self.request.matchdict)
468 483 diff1 = self.request.GET.get('diff1', '')
469 484 diff2 = self.request.GET.get('diff2', '')
470 485
471 486 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
472 487
473 488 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
474 489 line_context = self.request.GET.get('context', 3)
475 490
476 491 if not any((diff1, diff2)):
477 492 h.flash(
478 493 'Need query parameter "diff1" or "diff2" to generate a diff.',
479 494 category='error')
480 495 raise HTTPBadRequest()
481 496
482 497 c.action = self.request.GET.get('diff')
483 498 if c.action not in ['download', 'raw']:
484 499 compare_url = h.route_path(
485 500 'repo_compare',
486 501 repo_name=self.db_repo_name,
487 502 source_ref_type='rev',
488 503 source_ref=diff1,
489 504 target_repo=self.db_repo_name,
490 505 target_ref_type='rev',
491 506 target_ref=diff2,
492 507 _query=dict(f_path=f_path))
493 508 # redirect to new view if we render diff
494 509 raise HTTPFound(compare_url)
495 510
496 511 try:
497 512 node1 = self._get_file_node(diff1, path1)
498 513 node2 = self._get_file_node(diff2, f_path)
499 514 except (RepositoryError, NodeError):
500 515 log.exception("Exception while trying to get node from repository")
501 516 raise HTTPFound(
502 517 h.route_path('repo_files', repo_name=self.db_repo_name,
503 518 commit_id='tip', f_path=f_path))
504 519
505 520 if all(isinstance(node.commit, EmptyCommit)
506 521 for node in (node1, node2)):
507 522 raise HTTPNotFound()
508 523
509 524 c.commit_1 = node1.commit
510 525 c.commit_2 = node2.commit
511 526
512 527 if c.action == 'download':
513 528 _diff = diffs.get_gitdiff(node1, node2,
514 529 ignore_whitespace=ignore_whitespace,
515 530 context=line_context)
516 531 diff = diffs.DiffProcessor(_diff, format='gitdiff')
517 532
518 533 response = Response(self.path_filter.get_raw_patch(diff))
519 534 response.content_type = 'text/plain'
520 535 response.content_disposition = (
521 536 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
522 537 )
523 538 charset = self._get_default_encoding(c)
524 539 if charset:
525 540 response.charset = charset
526 541 return response
527 542
528 543 elif c.action == 'raw':
529 544 _diff = diffs.get_gitdiff(node1, node2,
530 545 ignore_whitespace=ignore_whitespace,
531 546 context=line_context)
532 547 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 548
534 549 response = Response(self.path_filter.get_raw_patch(diff))
535 550 response.content_type = 'text/plain'
536 551 charset = self._get_default_encoding(c)
537 552 if charset:
538 553 response.charset = charset
539 554 return response
540 555
541 556 # in case we ever end up here
542 557 raise HTTPNotFound()
543 558
544 559 @LoginRequired()
545 560 @HasRepoPermissionAnyDecorator(
546 561 'repository.read', 'repository.write', 'repository.admin')
547 562 @view_config(
548 563 route_name='repo_files_diff_2way_redirect', request_method='GET',
549 564 renderer=None)
550 565 def repo_files_diff_2way_redirect(self):
551 566 """
552 567 Kept only to make OLD links work
553 568 """
554 569 f_path = self._get_f_path_unchecked(self.request.matchdict)
555 570 diff1 = self.request.GET.get('diff1', '')
556 571 diff2 = self.request.GET.get('diff2', '')
557 572
558 573 if not any((diff1, diff2)):
559 574 h.flash(
560 575 'Need query parameter "diff1" or "diff2" to generate a diff.',
561 576 category='error')
562 577 raise HTTPBadRequest()
563 578
564 579 compare_url = h.route_path(
565 580 'repo_compare',
566 581 repo_name=self.db_repo_name,
567 582 source_ref_type='rev',
568 583 source_ref=diff1,
569 584 target_ref_type='rev',
570 585 target_ref=diff2,
571 586 _query=dict(f_path=f_path, diffmode='sideside',
572 587 target_repo=self.db_repo_name,))
573 588 raise HTTPFound(compare_url)
574 589
575 590 @LoginRequired()
576 591 @HasRepoPermissionAnyDecorator(
577 592 'repository.read', 'repository.write', 'repository.admin')
578 593 @view_config(
579 594 route_name='repo_files', request_method='GET',
580 595 renderer=None)
581 596 @view_config(
582 597 route_name='repo_files:default_path', request_method='GET',
583 598 renderer=None)
584 599 @view_config(
585 600 route_name='repo_files:default_commit', request_method='GET',
586 601 renderer=None)
587 602 @view_config(
588 603 route_name='repo_files:rendered', request_method='GET',
589 604 renderer=None)
590 605 @view_config(
591 606 route_name='repo_files:annotated', request_method='GET',
592 607 renderer=None)
593 608 def repo_files(self):
594 609 c = self.load_default_context()
595 610
596 611 view_name = getattr(self.request.matched_route, 'name', None)
597 612
598 613 c.annotate = view_name == 'repo_files:annotated'
599 614 # default is false, but .rst/.md files later are auto rendered, we can
600 615 # overwrite auto rendering by setting this GET flag
601 616 c.renderer = view_name == 'repo_files:rendered' or \
602 617 not self.request.GET.get('no-render', False)
603 618
604 619 # redirect to given commit_id from form if given
605 620 get_commit_id = self.request.GET.get('at_rev', None)
606 621 if get_commit_id:
607 622 self._get_commit_or_redirect(get_commit_id)
608 623
609 624 commit_id, f_path = self._get_commit_and_path()
610 625 c.commit = self._get_commit_or_redirect(commit_id)
611 626 c.branch = self.request.GET.get('branch', None)
612 627 c.f_path = f_path
613 628
614 629 # prev link
615 630 try:
616 631 prev_commit = c.commit.prev(c.branch)
617 632 c.prev_commit = prev_commit
618 633 c.url_prev = h.route_path(
619 634 'repo_files', repo_name=self.db_repo_name,
620 635 commit_id=prev_commit.raw_id, f_path=f_path)
621 636 if c.branch:
622 637 c.url_prev += '?branch=%s' % c.branch
623 638 except (CommitDoesNotExistError, VCSError):
624 639 c.url_prev = '#'
625 640 c.prev_commit = EmptyCommit()
626 641
627 642 # next link
628 643 try:
629 644 next_commit = c.commit.next(c.branch)
630 645 c.next_commit = next_commit
631 646 c.url_next = h.route_path(
632 647 'repo_files', repo_name=self.db_repo_name,
633 648 commit_id=next_commit.raw_id, f_path=f_path)
634 649 if c.branch:
635 650 c.url_next += '?branch=%s' % c.branch
636 651 except (CommitDoesNotExistError, VCSError):
637 652 c.url_next = '#'
638 653 c.next_commit = EmptyCommit()
639 654
640 655 # files or dirs
641 656 try:
642 657 c.file = c.commit.get_node(f_path)
643 658 c.file_author = True
644 659 c.file_tree = ''
645 660
646 661 # load file content
647 662 if c.file.is_file():
663 c.lf_node = {}
664
665 has_lf_enabled = self._is_lf_enabled(self.db_repo)
666 if has_lf_enabled:
648 667 c.lf_node = c.file.get_largefile_node()
649 668
650 669 c.file_source_page = 'true'
651 670 c.file_last_commit = c.file.last_commit
652 671 if c.file.size < c.visual.cut_off_limit_diff:
653 672 if c.annotate: # annotation has precedence over renderer
654 673 c.annotated_lines = filenode_as_annotated_lines_tokens(
655 674 c.file
656 675 )
657 676 else:
658 677 c.renderer = (
659 678 c.renderer and h.renderer_from_filename(c.file.path)
660 679 )
661 680 if not c.renderer:
662 681 c.lines = filenode_as_lines_tokens(c.file)
663 682
664 683 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
665 684 commit_id, self.rhodecode_vcs_repo)
666 685 c.on_branch_head = is_head
667 686
668 687 branch = c.commit.branch if (
669 688 c.commit.branch and '/' not in c.commit.branch) else None
670 689 c.branch_or_raw_id = branch or c.commit.raw_id
671 690 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
672 691
673 692 author = c.file_last_commit.author
674 693 c.authors = [[
675 694 h.email(author),
676 695 h.person(author, 'username_or_name_or_email'),
677 696 1
678 697 ]]
679 698
680 699 else: # load tree content at path
681 700 c.file_source_page = 'false'
682 701 c.authors = []
683 702 # this loads a simple tree without metadata to speed things up
684 703 # later via ajax we call repo_nodetree_full and fetch whole
685 704 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
686 705
687 706 except RepositoryError as e:
688 707 h.flash(safe_str(h.escape(e)), category='error')
689 708 raise HTTPNotFound()
690 709
691 710 if self.request.environ.get('HTTP_X_PJAX'):
692 711 html = render('rhodecode:templates/files/files_pjax.mako',
693 712 self._get_template_context(c), self.request)
694 713 else:
695 714 html = render('rhodecode:templates/files/files.mako',
696 715 self._get_template_context(c), self.request)
697 716 return Response(html)
698 717
699 718 @HasRepoPermissionAnyDecorator(
700 719 'repository.read', 'repository.write', 'repository.admin')
701 720 @view_config(
702 721 route_name='repo_files:annotated_previous', request_method='GET',
703 722 renderer=None)
704 723 def repo_files_annotated_previous(self):
705 724 self.load_default_context()
706 725
707 726 commit_id, f_path = self._get_commit_and_path()
708 727 commit = self._get_commit_or_redirect(commit_id)
709 728 prev_commit_id = commit.raw_id
710 729 line_anchor = self.request.GET.get('line_anchor')
711 730 is_file = False
712 731 try:
713 732 _file = commit.get_node(f_path)
714 733 is_file = _file.is_file()
715 734 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
716 735 pass
717 736
718 737 if is_file:
719 738 history = commit.get_path_history(f_path)
720 739 prev_commit_id = history[1].raw_id \
721 740 if len(history) > 1 else prev_commit_id
722 741 prev_url = h.route_path(
723 742 'repo_files:annotated', repo_name=self.db_repo_name,
724 743 commit_id=prev_commit_id, f_path=f_path,
725 744 _anchor='L{}'.format(line_anchor))
726 745
727 746 raise HTTPFound(prev_url)
728 747
729 748 @LoginRequired()
730 749 @HasRepoPermissionAnyDecorator(
731 750 'repository.read', 'repository.write', 'repository.admin')
732 751 @view_config(
733 752 route_name='repo_nodetree_full', request_method='GET',
734 753 renderer=None, xhr=True)
735 754 @view_config(
736 755 route_name='repo_nodetree_full:default_path', request_method='GET',
737 756 renderer=None, xhr=True)
738 757 def repo_nodetree_full(self):
739 758 """
740 759 Returns rendered html of file tree that contains commit date,
741 760 author, commit_id for the specified combination of
742 761 repo, commit_id and file path
743 762 """
744 763 c = self.load_default_context()
745 764
746 765 commit_id, f_path = self._get_commit_and_path()
747 766 commit = self._get_commit_or_redirect(commit_id)
748 767 try:
749 768 dir_node = commit.get_node(f_path)
750 769 except RepositoryError as e:
751 770 return Response('error: {}'.format(h.escape(safe_str(e))))
752 771
753 772 if dir_node.is_file():
754 773 return Response('')
755 774
756 775 c.file = dir_node
757 776 c.commit = commit
758 777
759 778 html = self._get_tree_at_commit(
760 779 c, commit.raw_id, dir_node.path, full_load=True)
761 780
762 781 return Response(html)
763 782
764 783 def _get_attachement_headers(self, f_path):
765 784 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
766 785 safe_path = f_name.replace('"', '\\"')
767 786 encoded_path = urllib.quote(f_name)
768 787
769 788 return "attachment; " \
770 789 "filename=\"{}\"; " \
771 790 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
772 791
773 792 @LoginRequired()
774 793 @HasRepoPermissionAnyDecorator(
775 794 'repository.read', 'repository.write', 'repository.admin')
776 795 @view_config(
777 796 route_name='repo_file_raw', request_method='GET',
778 797 renderer=None)
779 798 def repo_file_raw(self):
780 799 """
781 800 Action for show as raw, some mimetypes are "rendered",
782 801 those include images, icons.
783 802 """
784 803 c = self.load_default_context()
785 804
786 805 commit_id, f_path = self._get_commit_and_path()
787 806 commit = self._get_commit_or_redirect(commit_id)
788 807 file_node = self._get_filenode_or_redirect(commit, f_path)
789 808
790 809 raw_mimetype_mapping = {
791 810 # map original mimetype to a mimetype used for "show as raw"
792 811 # you can also provide a content-disposition to override the
793 812 # default "attachment" disposition.
794 813 # orig_type: (new_type, new_dispo)
795 814
796 815 # show images inline:
797 816 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
798 817 # for example render an SVG with javascript inside or even render
799 818 # HTML.
800 819 'image/x-icon': ('image/x-icon', 'inline'),
801 820 'image/png': ('image/png', 'inline'),
802 821 'image/gif': ('image/gif', 'inline'),
803 822 'image/jpeg': ('image/jpeg', 'inline'),
804 823 'application/pdf': ('application/pdf', 'inline'),
805 824 }
806 825
807 826 mimetype = file_node.mimetype
808 827 try:
809 828 mimetype, disposition = raw_mimetype_mapping[mimetype]
810 829 except KeyError:
811 830 # we don't know anything special about this, handle it safely
812 831 if file_node.is_binary:
813 832 # do same as download raw for binary files
814 833 mimetype, disposition = 'application/octet-stream', 'attachment'
815 834 else:
816 835 # do not just use the original mimetype, but force text/plain,
817 836 # otherwise it would serve text/html and that might be unsafe.
818 837 # Note: underlying vcs library fakes text/plain mimetype if the
819 838 # mimetype can not be determined and it thinks it is not
820 839 # binary.This might lead to erroneous text display in some
821 840 # cases, but helps in other cases, like with text files
822 841 # without extension.
823 842 mimetype, disposition = 'text/plain', 'inline'
824 843
825 844 if disposition == 'attachment':
826 845 disposition = self._get_attachement_headers(f_path)
827 846
828 847 def stream_node():
829 848 yield file_node.raw_bytes
830 849
831 850 response = Response(app_iter=stream_node())
832 851 response.content_disposition = disposition
833 852 response.content_type = mimetype
834 853
835 854 charset = self._get_default_encoding(c)
836 855 if charset:
837 856 response.charset = charset
838 857
839 858 return response
840 859
841 860 @LoginRequired()
842 861 @HasRepoPermissionAnyDecorator(
843 862 'repository.read', 'repository.write', 'repository.admin')
844 863 @view_config(
845 864 route_name='repo_file_download', request_method='GET',
846 865 renderer=None)
847 866 @view_config(
848 867 route_name='repo_file_download:legacy', request_method='GET',
849 868 renderer=None)
850 869 def repo_file_download(self):
851 870 c = self.load_default_context()
852 871
853 872 commit_id, f_path = self._get_commit_and_path()
854 873 commit = self._get_commit_or_redirect(commit_id)
855 874 file_node = self._get_filenode_or_redirect(commit, f_path)
856 875
857 876 if self.request.GET.get('lf'):
858 877 # only if lf get flag is passed, we download this file
859 878 # as LFS/Largefile
860 879 lf_node = file_node.get_largefile_node()
861 880 if lf_node:
862 881 # overwrite our pointer with the REAL large-file
863 882 file_node = lf_node
864 883
865 884 disposition = self._get_attachement_headers(f_path)
866 885
867 886 def stream_node():
868 887 yield file_node.raw_bytes
869 888
870 889 response = Response(app_iter=stream_node())
871 890 response.content_disposition = disposition
872 891 response.content_type = file_node.mimetype
873 892
874 893 charset = self._get_default_encoding(c)
875 894 if charset:
876 895 response.charset = charset
877 896
878 897 return response
879 898
880 899 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
881 900
882 901 cache_seconds = safe_int(
883 902 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
884 903 cache_on = cache_seconds > 0
885 904 log.debug(
886 905 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
887 906 'with caching: %s[TTL: %ss]' % (
888 907 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
889 908
890 909 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
891 910 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
892 911
893 912 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
894 913 condition=cache_on)
895 914 def compute_file_search(repo_id, commit_id, f_path):
896 915 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
897 916 repo_id, commit_id, f_path)
898 917 try:
899 918 _d, _f = ScmModel().get_nodes(
900 919 repo_name, commit_id, f_path, flat=False)
901 920 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
902 921 log.exception(safe_str(e))
903 922 h.flash(safe_str(h.escape(e)), category='error')
904 923 raise HTTPFound(h.route_path(
905 924 'repo_files', repo_name=self.db_repo_name,
906 925 commit_id='tip', f_path='/'))
907 926
908 927 return _d + _f
909 928
910 929 result = compute_file_search(self.db_repo.repo_id, commit_id, f_path)
911 930 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
912 931
913 932 @LoginRequired()
914 933 @HasRepoPermissionAnyDecorator(
915 934 'repository.read', 'repository.write', 'repository.admin')
916 935 @view_config(
917 936 route_name='repo_files_nodelist', request_method='GET',
918 937 renderer='json_ext', xhr=True)
919 938 def repo_nodelist(self):
920 939 self.load_default_context()
921 940
922 941 commit_id, f_path = self._get_commit_and_path()
923 942 commit = self._get_commit_or_redirect(commit_id)
924 943
925 944 metadata = self._get_nodelist_at_commit(
926 945 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
927 946 return {'nodes': metadata}
928 947
929 948 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
930 949 items = []
931 950 for name, commit_id in branches_or_tags.items():
932 951 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
933 952 items.append((sym_ref, name, ref_type))
934 953 return items
935 954
936 955 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
937 956 return commit_id
938 957
939 958 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
940 959 new_f_path = vcspath.join(name, f_path)
941 960 return u'%s@%s' % (new_f_path, commit_id)
942 961
943 962 def _get_node_history(self, commit_obj, f_path, commits=None):
944 963 """
945 964 get commit history for given node
946 965
947 966 :param commit_obj: commit to calculate history
948 967 :param f_path: path for node to calculate history for
949 968 :param commits: if passed don't calculate history and take
950 969 commits defined in this list
951 970 """
952 971 _ = self.request.translate
953 972
954 973 # calculate history based on tip
955 974 tip = self.rhodecode_vcs_repo.get_commit()
956 975 if commits is None:
957 976 pre_load = ["author", "branch"]
958 977 try:
959 978 commits = tip.get_path_history(f_path, pre_load=pre_load)
960 979 except (NodeDoesNotExistError, CommitError):
961 980 # this node is not present at tip!
962 981 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
963 982
964 983 history = []
965 984 commits_group = ([], _("Changesets"))
966 985 for commit in commits:
967 986 branch = ' (%s)' % commit.branch if commit.branch else ''
968 987 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
969 988 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
970 989 history.append(commits_group)
971 990
972 991 symbolic_reference = self._symbolic_reference
973 992
974 993 if self.rhodecode_vcs_repo.alias == 'svn':
975 994 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
976 995 f_path, self.rhodecode_vcs_repo)
977 996 if adjusted_f_path != f_path:
978 997 log.debug(
979 998 'Recognized svn tag or branch in file "%s", using svn '
980 999 'specific symbolic references', f_path)
981 1000 f_path = adjusted_f_path
982 1001 symbolic_reference = self._symbolic_reference_svn
983 1002
984 1003 branches = self._create_references(
985 1004 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
986 1005 branches_group = (branches, _("Branches"))
987 1006
988 1007 tags = self._create_references(
989 1008 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
990 1009 tags_group = (tags, _("Tags"))
991 1010
992 1011 history.append(branches_group)
993 1012 history.append(tags_group)
994 1013
995 1014 return history, commits
996 1015
997 1016 @LoginRequired()
998 1017 @HasRepoPermissionAnyDecorator(
999 1018 'repository.read', 'repository.write', 'repository.admin')
1000 1019 @view_config(
1001 1020 route_name='repo_file_history', request_method='GET',
1002 1021 renderer='json_ext')
1003 1022 def repo_file_history(self):
1004 1023 self.load_default_context()
1005 1024
1006 1025 commit_id, f_path = self._get_commit_and_path()
1007 1026 commit = self._get_commit_or_redirect(commit_id)
1008 1027 file_node = self._get_filenode_or_redirect(commit, f_path)
1009 1028
1010 1029 if file_node.is_file():
1011 1030 file_history, _hist = self._get_node_history(commit, f_path)
1012 1031
1013 1032 res = []
1014 1033 for obj in file_history:
1015 1034 res.append({
1016 1035 'text': obj[1],
1017 1036 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
1018 1037 })
1019 1038
1020 1039 data = {
1021 1040 'more': False,
1022 1041 'results': res
1023 1042 }
1024 1043 return data
1025 1044
1026 1045 log.warning('Cannot fetch history for directory')
1027 1046 raise HTTPBadRequest()
1028 1047
1029 1048 @LoginRequired()
1030 1049 @HasRepoPermissionAnyDecorator(
1031 1050 'repository.read', 'repository.write', 'repository.admin')
1032 1051 @view_config(
1033 1052 route_name='repo_file_authors', request_method='GET',
1034 1053 renderer='rhodecode:templates/files/file_authors_box.mako')
1035 1054 def repo_file_authors(self):
1036 1055 c = self.load_default_context()
1037 1056
1038 1057 commit_id, f_path = self._get_commit_and_path()
1039 1058 commit = self._get_commit_or_redirect(commit_id)
1040 1059 file_node = self._get_filenode_or_redirect(commit, f_path)
1041 1060
1042 1061 if not file_node.is_file():
1043 1062 raise HTTPBadRequest()
1044 1063
1045 1064 c.file_last_commit = file_node.last_commit
1046 1065 if self.request.GET.get('annotate') == '1':
1047 1066 # use _hist from annotation if annotation mode is on
1048 1067 commit_ids = set(x[1] for x in file_node.annotate)
1049 1068 _hist = (
1050 1069 self.rhodecode_vcs_repo.get_commit(commit_id)
1051 1070 for commit_id in commit_ids)
1052 1071 else:
1053 1072 _f_history, _hist = self._get_node_history(commit, f_path)
1054 1073 c.file_author = False
1055 1074
1056 1075 unique = collections.OrderedDict()
1057 1076 for commit in _hist:
1058 1077 author = commit.author
1059 1078 if author not in unique:
1060 1079 unique[commit.author] = [
1061 1080 h.email(author),
1062 1081 h.person(author, 'username_or_name_or_email'),
1063 1082 1 # counter
1064 1083 ]
1065 1084
1066 1085 else:
1067 1086 # increase counter
1068 1087 unique[commit.author][2] += 1
1069 1088
1070 1089 c.authors = [val for val in unique.values()]
1071 1090
1072 1091 return self._get_template_context(c)
1073 1092
1074 1093 @LoginRequired()
1075 1094 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1076 1095 @view_config(
1077 1096 route_name='repo_files_remove_file', request_method='GET',
1078 1097 renderer='rhodecode:templates/files/files_delete.mako')
1079 1098 def repo_files_remove_file(self):
1080 1099 _ = self.request.translate
1081 1100 c = self.load_default_context()
1082 1101 commit_id, f_path = self._get_commit_and_path()
1083 1102
1084 1103 self._ensure_not_locked()
1085 1104 _branch_name, _sha_commit_id, is_head = \
1086 1105 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1087 1106
1088 1107 self.forbid_non_head(is_head, f_path)
1089 1108 self.check_branch_permission(_branch_name)
1090 1109
1091 1110 c.commit = self._get_commit_or_redirect(commit_id)
1092 1111 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1093 1112
1094 1113 c.default_message = _(
1095 1114 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1096 1115 c.f_path = f_path
1097 1116
1098 1117 return self._get_template_context(c)
1099 1118
1100 1119 @LoginRequired()
1101 1120 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1102 1121 @CSRFRequired()
1103 1122 @view_config(
1104 1123 route_name='repo_files_delete_file', request_method='POST',
1105 1124 renderer=None)
1106 1125 def repo_files_delete_file(self):
1107 1126 _ = self.request.translate
1108 1127
1109 1128 c = self.load_default_context()
1110 1129 commit_id, f_path = self._get_commit_and_path()
1111 1130
1112 1131 self._ensure_not_locked()
1113 1132 _branch_name, _sha_commit_id, is_head = \
1114 1133 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1115 1134
1116 1135 self.forbid_non_head(is_head, f_path)
1117 1136 self.check_branch_permission(_branch_name)
1118 1137
1119 1138 c.commit = self._get_commit_or_redirect(commit_id)
1120 1139 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1121 1140
1122 1141 c.default_message = _(
1123 1142 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1124 1143 c.f_path = f_path
1125 1144 node_path = f_path
1126 1145 author = self._rhodecode_db_user.full_contact
1127 1146 message = self.request.POST.get('message') or c.default_message
1128 1147 try:
1129 1148 nodes = {
1130 1149 node_path: {
1131 1150 'content': ''
1132 1151 }
1133 1152 }
1134 1153 ScmModel().delete_nodes(
1135 1154 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1136 1155 message=message,
1137 1156 nodes=nodes,
1138 1157 parent_commit=c.commit,
1139 1158 author=author,
1140 1159 )
1141 1160
1142 1161 h.flash(
1143 1162 _('Successfully deleted file `{}`').format(
1144 1163 h.escape(f_path)), category='success')
1145 1164 except Exception:
1146 1165 log.exception('Error during commit operation')
1147 1166 h.flash(_('Error occurred during commit'), category='error')
1148 1167 raise HTTPFound(
1149 1168 h.route_path('repo_commit', repo_name=self.db_repo_name,
1150 1169 commit_id='tip'))
1151 1170
1152 1171 @LoginRequired()
1153 1172 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1154 1173 @view_config(
1155 1174 route_name='repo_files_edit_file', request_method='GET',
1156 1175 renderer='rhodecode:templates/files/files_edit.mako')
1157 1176 def repo_files_edit_file(self):
1158 1177 _ = self.request.translate
1159 1178 c = self.load_default_context()
1160 1179 commit_id, f_path = self._get_commit_and_path()
1161 1180
1162 1181 self._ensure_not_locked()
1163 1182 _branch_name, _sha_commit_id, is_head = \
1164 1183 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1165 1184
1166 1185 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1167 1186 self.check_branch_permission(_branch_name, commit_id=commit_id)
1168 1187
1169 1188 c.commit = self._get_commit_or_redirect(commit_id)
1170 1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1171 1190
1172 1191 if c.file.is_binary:
1173 1192 files_url = h.route_path(
1174 1193 'repo_files',
1175 1194 repo_name=self.db_repo_name,
1176 1195 commit_id=c.commit.raw_id, f_path=f_path)
1177 1196 raise HTTPFound(files_url)
1178 1197
1179 1198 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1180 1199 c.f_path = f_path
1181 1200
1182 1201 return self._get_template_context(c)
1183 1202
1184 1203 @LoginRequired()
1185 1204 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1186 1205 @CSRFRequired()
1187 1206 @view_config(
1188 1207 route_name='repo_files_update_file', request_method='POST',
1189 1208 renderer=None)
1190 1209 def repo_files_update_file(self):
1191 1210 _ = self.request.translate
1192 1211 c = self.load_default_context()
1193 1212 commit_id, f_path = self._get_commit_and_path()
1194 1213
1195 1214 self._ensure_not_locked()
1196 1215
1197 1216 c.commit = self._get_commit_or_redirect(commit_id)
1198 1217 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1199 1218
1200 1219 if c.file.is_binary:
1201 1220 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1202 1221 commit_id=c.commit.raw_id, f_path=f_path))
1203 1222
1204 1223 _branch_name, _sha_commit_id, is_head = \
1205 1224 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1206 1225
1207 1226 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1208 1227 self.check_branch_permission(_branch_name, commit_id=commit_id)
1209 1228
1210 1229 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1211 1230 c.f_path = f_path
1212 1231
1213 1232 old_content = c.file.content
1214 1233 sl = old_content.splitlines(1)
1215 1234 first_line = sl[0] if sl else ''
1216 1235
1217 1236 r_post = self.request.POST
1218 1237 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1219 1238 line_ending_mode = detect_mode(first_line, 0)
1220 1239 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1221 1240
1222 1241 message = r_post.get('message') or c.default_message
1223 1242 org_node_path = c.file.unicode_path
1224 1243 filename = r_post['filename']
1225 1244
1226 1245 root_path = c.file.dir_path
1227 1246 pure_path = self.create_pure_path(root_path, filename)
1228 1247 node_path = safe_unicode(bytes(pure_path))
1229 1248
1230 1249 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1231 1250 commit_id=commit_id)
1232 1251 if content == old_content and node_path == org_node_path:
1233 1252 h.flash(_('No changes detected on {}').format(org_node_path),
1234 1253 category='warning')
1235 1254 raise HTTPFound(default_redirect_url)
1236 1255
1237 1256 try:
1238 1257 mapping = {
1239 1258 org_node_path: {
1240 1259 'org_filename': org_node_path,
1241 1260 'filename': node_path,
1242 1261 'content': content,
1243 1262 'lexer': '',
1244 1263 'op': 'mod',
1245 1264 'mode': c.file.mode
1246 1265 }
1247 1266 }
1248 1267
1249 1268 commit = ScmModel().update_nodes(
1250 1269 user=self._rhodecode_db_user.user_id,
1251 1270 repo=self.db_repo,
1252 1271 message=message,
1253 1272 nodes=mapping,
1254 1273 parent_commit=c.commit,
1255 1274 )
1256 1275
1257 1276 h.flash(_('Successfully committed changes to file `{}`').format(
1258 1277 h.escape(f_path)), category='success')
1259 1278 default_redirect_url = h.route_path(
1260 1279 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1261 1280
1262 1281 except Exception:
1263 1282 log.exception('Error occurred during commit')
1264 1283 h.flash(_('Error occurred during commit'), category='error')
1265 1284
1266 1285 raise HTTPFound(default_redirect_url)
1267 1286
1268 1287 @LoginRequired()
1269 1288 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1270 1289 @view_config(
1271 1290 route_name='repo_files_add_file', request_method='GET',
1272 1291 renderer='rhodecode:templates/files/files_add.mako')
1273 1292 @view_config(
1274 1293 route_name='repo_files_upload_file', request_method='GET',
1275 1294 renderer='rhodecode:templates/files/files_upload.mako')
1276 1295 def repo_files_add_file(self):
1277 1296 _ = self.request.translate
1278 1297 c = self.load_default_context()
1279 1298 commit_id, f_path = self._get_commit_and_path()
1280 1299
1281 1300 self._ensure_not_locked()
1282 1301
1283 1302 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1284 1303 if c.commit is None:
1285 1304 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1286 1305
1287 1306 if self.rhodecode_vcs_repo.is_empty():
1288 1307 # for empty repository we cannot check for current branch, we rely on
1289 1308 # c.commit.branch instead
1290 1309 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1291 1310 else:
1292 1311 _branch_name, _sha_commit_id, is_head = \
1293 1312 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1294 1313
1295 1314 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1296 1315 self.check_branch_permission(_branch_name, commit_id=commit_id)
1297 1316
1298 1317 c.default_message = (_('Added file via RhodeCode Enterprise'))
1299 1318 c.f_path = f_path.lstrip('/') # ensure not relative path
1300 1319
1301 1320 return self._get_template_context(c)
1302 1321
1303 1322 @LoginRequired()
1304 1323 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1305 1324 @CSRFRequired()
1306 1325 @view_config(
1307 1326 route_name='repo_files_create_file', request_method='POST',
1308 1327 renderer=None)
1309 1328 def repo_files_create_file(self):
1310 1329 _ = self.request.translate
1311 1330 c = self.load_default_context()
1312 1331 commit_id, f_path = self._get_commit_and_path()
1313 1332
1314 1333 self._ensure_not_locked()
1315 1334
1316 1335 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1317 1336 if c.commit is None:
1318 1337 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1319 1338
1320 1339 # calculate redirect URL
1321 1340 if self.rhodecode_vcs_repo.is_empty():
1322 1341 default_redirect_url = h.route_path(
1323 1342 'repo_summary', repo_name=self.db_repo_name)
1324 1343 else:
1325 1344 default_redirect_url = h.route_path(
1326 1345 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1327 1346
1328 1347 if self.rhodecode_vcs_repo.is_empty():
1329 1348 # for empty repository we cannot check for current branch, we rely on
1330 1349 # c.commit.branch instead
1331 1350 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1332 1351 else:
1333 1352 _branch_name, _sha_commit_id, is_head = \
1334 1353 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1335 1354
1336 1355 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1337 1356 self.check_branch_permission(_branch_name, commit_id=commit_id)
1338 1357
1339 1358 c.default_message = (_('Added file via RhodeCode Enterprise'))
1340 1359 c.f_path = f_path
1341 1360
1342 1361 r_post = self.request.POST
1343 1362 message = r_post.get('message') or c.default_message
1344 1363 filename = r_post.get('filename')
1345 1364 unix_mode = 0
1346 1365 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1347 1366
1348 1367 if not filename:
1349 1368 # If there's no commit, redirect to repo summary
1350 1369 if type(c.commit) is EmptyCommit:
1351 1370 redirect_url = h.route_path(
1352 1371 'repo_summary', repo_name=self.db_repo_name)
1353 1372 else:
1354 1373 redirect_url = default_redirect_url
1355 1374 h.flash(_('No filename specified'), category='warning')
1356 1375 raise HTTPFound(redirect_url)
1357 1376
1358 1377 root_path = f_path
1359 1378 pure_path = self.create_pure_path(root_path, filename)
1360 1379 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1361 1380
1362 1381 author = self._rhodecode_db_user.full_contact
1363 1382 nodes = {
1364 1383 node_path: {
1365 1384 'content': content
1366 1385 }
1367 1386 }
1368 1387
1369 1388 try:
1370 1389
1371 1390 commit = ScmModel().create_nodes(
1372 1391 user=self._rhodecode_db_user.user_id,
1373 1392 repo=self.db_repo,
1374 1393 message=message,
1375 1394 nodes=nodes,
1376 1395 parent_commit=c.commit,
1377 1396 author=author,
1378 1397 )
1379 1398
1380 1399 h.flash(_('Successfully committed new file `{}`').format(
1381 1400 h.escape(node_path)), category='success')
1382 1401
1383 1402 default_redirect_url = h.route_path(
1384 1403 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1385 1404
1386 1405 except NonRelativePathError:
1387 1406 log.exception('Non Relative path found')
1388 1407 h.flash(_('The location specified must be a relative path and must not '
1389 1408 'contain .. in the path'), category='warning')
1390 1409 raise HTTPFound(default_redirect_url)
1391 1410 except (NodeError, NodeAlreadyExistsError) as e:
1392 1411 h.flash(_(h.escape(e)), category='error')
1393 1412 except Exception:
1394 1413 log.exception('Error occurred during commit')
1395 1414 h.flash(_('Error occurred during commit'), category='error')
1396 1415
1397 1416 raise HTTPFound(default_redirect_url)
1398 1417
1399 1418 @LoginRequired()
1400 1419 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1401 1420 @CSRFRequired()
1402 1421 @view_config(
1403 1422 route_name='repo_files_upload_file', request_method='POST',
1404 1423 renderer='json_ext')
1405 1424 def repo_files_upload_file(self):
1406 1425 _ = self.request.translate
1407 1426 c = self.load_default_context()
1408 1427 commit_id, f_path = self._get_commit_and_path()
1409 1428
1410 1429 self._ensure_not_locked()
1411 1430
1412 1431 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1413 1432 if c.commit is None:
1414 1433 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1415 1434
1416 1435 # calculate redirect URL
1417 1436 if self.rhodecode_vcs_repo.is_empty():
1418 1437 default_redirect_url = h.route_path(
1419 1438 'repo_summary', repo_name=self.db_repo_name)
1420 1439 else:
1421 1440 default_redirect_url = h.route_path(
1422 1441 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1423 1442
1424 1443 if self.rhodecode_vcs_repo.is_empty():
1425 1444 # for empty repository we cannot check for current branch, we rely on
1426 1445 # c.commit.branch instead
1427 1446 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1428 1447 else:
1429 1448 _branch_name, _sha_commit_id, is_head = \
1430 1449 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1431 1450
1432 1451 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1433 1452 if error:
1434 1453 return {
1435 1454 'error': error,
1436 1455 'redirect_url': default_redirect_url
1437 1456 }
1438 1457 error = self.check_branch_permission(_branch_name, json_mode=True)
1439 1458 if error:
1440 1459 return {
1441 1460 'error': error,
1442 1461 'redirect_url': default_redirect_url
1443 1462 }
1444 1463
1445 1464 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1446 1465 c.f_path = f_path
1447 1466
1448 1467 r_post = self.request.POST
1449 1468
1450 1469 message = c.default_message
1451 1470 user_message = r_post.getall('message')
1452 1471 if isinstance(user_message, list) and user_message:
1453 1472 # we take the first from duplicated results if it's not empty
1454 1473 message = user_message[0] if user_message[0] else message
1455 1474
1456 1475 nodes = {}
1457 1476
1458 1477 for file_obj in r_post.getall('files_upload') or []:
1459 1478 content = file_obj.file
1460 1479 filename = file_obj.filename
1461 1480
1462 1481 root_path = f_path
1463 1482 pure_path = self.create_pure_path(root_path, filename)
1464 1483 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1465 1484
1466 1485 nodes[node_path] = {
1467 1486 'content': content
1468 1487 }
1469 1488
1470 1489 if not nodes:
1471 1490 error = 'missing files'
1472 1491 return {
1473 1492 'error': error,
1474 1493 'redirect_url': default_redirect_url
1475 1494 }
1476 1495
1477 1496 author = self._rhodecode_db_user.full_contact
1478 1497
1479 1498 try:
1480 1499 commit = ScmModel().create_nodes(
1481 1500 user=self._rhodecode_db_user.user_id,
1482 1501 repo=self.db_repo,
1483 1502 message=message,
1484 1503 nodes=nodes,
1485 1504 parent_commit=c.commit,
1486 1505 author=author,
1487 1506 )
1488 1507 if len(nodes) == 1:
1489 1508 flash_message = _('Successfully committed {} new files').format(len(nodes))
1490 1509 else:
1491 1510 flash_message = _('Successfully committed 1 new file')
1492 1511
1493 1512 h.flash(flash_message, category='success')
1494 1513
1495 1514 default_redirect_url = h.route_path(
1496 1515 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1497 1516
1498 1517 except NonRelativePathError:
1499 1518 log.exception('Non Relative path found')
1500 1519 error = _('The location specified must be a relative path and must not '
1501 1520 'contain .. in the path')
1502 1521 h.flash(error, category='warning')
1503 1522
1504 1523 return {
1505 1524 'error': error,
1506 1525 'redirect_url': default_redirect_url
1507 1526 }
1508 1527 except (NodeError, NodeAlreadyExistsError) as e:
1509 1528 error = h.escape(e)
1510 1529 h.flash(error, category='error')
1511 1530
1512 1531 return {
1513 1532 'error': error,
1514 1533 'redirect_url': default_redirect_url
1515 1534 }
1516 1535 except Exception:
1517 1536 log.exception('Error occurred during commit')
1518 1537 error = _('Error occurred during commit')
1519 1538 h.flash(error, category='error')
1520 1539 return {
1521 1540 'error': error,
1522 1541 'redirect_url': default_redirect_url
1523 1542 }
1524 1543
1525 1544 return {
1526 1545 'error': None,
1527 1546 'redirect_url': default_redirect_url
1528 1547 }
@@ -1,380 +1,380 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 """
22 22 HG commit module
23 23 """
24 24
25 25 import os
26 26
27 27 from zope.cachedescriptors.property import Lazy as LazyProperty
28 28
29 29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 30 from rhodecode.lib.utils import safe_str, safe_unicode
31 31 from rhodecode.lib.vcs import path as vcspath
32 32 from rhodecode.lib.vcs.backends import base
33 33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 34 from rhodecode.lib.vcs.exceptions import CommitError
35 35 from rhodecode.lib.vcs.nodes import (
36 36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 38 LargeFileNode, LARGEFILE_PREFIX)
39 39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40 40
41 41
42 42 class MercurialCommit(base.BaseCommit):
43 43 """
44 44 Represents state of the repository at the single commit.
45 45 """
46 46
47 47 _filter_pre_load = [
48 48 # git specific property not supported here
49 49 "_commit",
50 50 ]
51 51
52 52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 53 raw_id = safe_str(raw_id)
54 54
55 55 self.repository = repository
56 56 self._remote = repository._remote
57 57
58 58 self.raw_id = raw_id
59 59 self.idx = idx
60 60
61 61 self._set_bulk_properties(pre_load)
62 62
63 63 # caches
64 64 self.nodes = {}
65 65
66 66 def _set_bulk_properties(self, pre_load):
67 67 if not pre_load:
68 68 return
69 69 pre_load = [entry for entry in pre_load
70 70 if entry not in self._filter_pre_load]
71 71 if not pre_load:
72 72 return
73 73
74 74 result = self._remote.bulk_request(self.raw_id, pre_load)
75 75 for attr, value in result.items():
76 76 if attr in ["author", "branch", "message"]:
77 77 value = safe_unicode(value)
78 78 elif attr == "affected_files":
79 79 value = map(safe_unicode, value)
80 80 elif attr == "date":
81 81 value = utcdate_fromtimestamp(*value)
82 82 elif attr in ["children", "parents"]:
83 83 value = self._make_commits(value)
84 84 elif attr in ["phase"]:
85 85 value = self._get_phase_text(value)
86 86 self.__dict__[attr] = value
87 87
88 88 @LazyProperty
89 89 def tags(self):
90 90 tags = [name for name, commit_id in self.repository.tags.iteritems()
91 91 if commit_id == self.raw_id]
92 92 return tags
93 93
94 94 @LazyProperty
95 95 def branch(self):
96 96 return safe_unicode(self._remote.ctx_branch(self.raw_id))
97 97
98 98 @LazyProperty
99 99 def bookmarks(self):
100 100 bookmarks = [
101 101 name for name, commit_id in self.repository.bookmarks.iteritems()
102 102 if commit_id == self.raw_id]
103 103 return bookmarks
104 104
105 105 @LazyProperty
106 106 def message(self):
107 107 return safe_unicode(self._remote.ctx_description(self.raw_id))
108 108
109 109 @LazyProperty
110 110 def committer(self):
111 111 return safe_unicode(self.author)
112 112
113 113 @LazyProperty
114 114 def author(self):
115 115 return safe_unicode(self._remote.ctx_user(self.raw_id))
116 116
117 117 @LazyProperty
118 118 def date(self):
119 119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.raw_id))
120 120
121 121 @LazyProperty
122 122 def status(self):
123 123 """
124 124 Returns modified, added, removed, deleted files for current commit
125 125 """
126 126 return self._remote.ctx_status(self.raw_id)
127 127
128 128 @LazyProperty
129 129 def _file_paths(self):
130 130 return self._remote.ctx_list(self.raw_id)
131 131
132 132 @LazyProperty
133 133 def _dir_paths(self):
134 134 p = list(set(get_dirs_for_path(*self._file_paths)))
135 135 p.insert(0, '')
136 136 return p
137 137
138 138 @LazyProperty
139 139 def _paths(self):
140 140 return self._dir_paths + self._file_paths
141 141
142 142 @LazyProperty
143 143 def id(self):
144 144 if self.last:
145 145 return u'tip'
146 146 return self.short_id
147 147
148 148 @LazyProperty
149 149 def short_id(self):
150 150 return self.raw_id[:12]
151 151
152 152 def _make_commits(self, indexes, pre_load=None):
153 153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
154 154 for idx in indexes if idx >= 0]
155 155
156 156 @LazyProperty
157 157 def parents(self):
158 158 """
159 159 Returns list of parent commits.
160 160 """
161 161 parents = self._remote.ctx_parents(self.raw_id)
162 162 return self._make_commits(parents)
163 163
164 164 def _get_phase_text(self, phase_id):
165 165 return {
166 166 0: 'public',
167 167 1: 'draft',
168 168 2: 'secret',
169 169 }.get(phase_id) or ''
170 170
171 171 @LazyProperty
172 172 def phase(self):
173 173 phase_id = self._remote.ctx_phase(self.raw_id)
174 174 phase_text = self._get_phase_text(phase_id)
175 175
176 176 return safe_unicode(phase_text)
177 177
178 178 @LazyProperty
179 179 def obsolete(self):
180 180 obsolete = self._remote.ctx_obsolete(self.raw_id)
181 181 return obsolete
182 182
183 183 @LazyProperty
184 184 def hidden(self):
185 185 hidden = self._remote.ctx_hidden(self.raw_id)
186 186 return hidden
187 187
188 188 @LazyProperty
189 189 def children(self):
190 190 """
191 191 Returns list of child commits.
192 192 """
193 193 children = self._remote.ctx_children(self.raw_id)
194 194 return self._make_commits(children)
195 195
196 196 def _fix_path(self, path):
197 197 """
198 198 Mercurial keeps filenodes as str so we need to encode from unicode
199 199 to str.
200 200 """
201 201 return safe_str(super(MercurialCommit, self)._fix_path(path))
202 202
203 203 def _get_kind(self, path):
204 204 path = self._fix_path(path)
205 205 if path in self._file_paths:
206 206 return NodeKind.FILE
207 207 elif path in self._dir_paths:
208 208 return NodeKind.DIR
209 209 else:
210 210 raise CommitError(
211 211 "Node does not exist at the given path '%s'" % (path, ))
212 212
213 213 def _get_filectx(self, path):
214 214 path = self._fix_path(path)
215 215 if self._get_kind(path) != NodeKind.FILE:
216 216 raise CommitError(
217 217 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
218 218 return path
219 219
220 220 def get_file_mode(self, path):
221 221 """
222 222 Returns stat mode of the file at the given ``path``.
223 223 """
224 224 path = self._get_filectx(path)
225 225 if 'x' in self._remote.fctx_flags(self.raw_id, path):
226 226 return base.FILEMODE_EXECUTABLE
227 227 else:
228 228 return base.FILEMODE_DEFAULT
229 229
230 230 def is_link(self, path):
231 231 path = self._get_filectx(path)
232 232 return 'l' in self._remote.fctx_flags(self.raw_id, path)
233 233
234 234 def get_file_content(self, path):
235 235 """
236 236 Returns content of the file at given ``path``.
237 237 """
238 238 path = self._get_filectx(path)
239 239 return self._remote.fctx_node_data(self.raw_id, path)
240 240
241 241 def get_file_size(self, path):
242 242 """
243 243 Returns size of the file at given ``path``.
244 244 """
245 245 path = self._get_filectx(path)
246 246 return self._remote.fctx_size(self.raw_id, path)
247 247
248 248 def get_path_history(self, path, limit=None, pre_load=None):
249 249 """
250 250 Returns history of file as reversed list of `MercurialCommit` objects
251 251 for which file at given ``path`` has been modified.
252 252 """
253 253 path = self._get_filectx(path)
254 254 hist = self._remote.node_history(self.raw_id, path, limit)
255 255 return [
256 256 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
257 257 for commit_id in hist]
258 258
259 259 def get_file_annotate(self, path, pre_load=None):
260 260 """
261 261 Returns a generator of four element tuples with
262 262 lineno, commit_id, commit lazy loader and line
263 263 """
264 264 result = self._remote.fctx_annotate(self.raw_id, path)
265 265
266 266 for ln_no, commit_id, content in result:
267 267 yield (
268 268 ln_no, commit_id,
269 269 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
270 270 content)
271 271
272 272 def get_nodes(self, path):
273 273 """
274 274 Returns combined ``DirNode`` and ``FileNode`` objects list representing
275 275 state of commit at the given ``path``. If node at the given ``path``
276 276 is not instance of ``DirNode``, CommitError would be raised.
277 277 """
278 278
279 279 if self._get_kind(path) != NodeKind.DIR:
280 280 raise CommitError(
281 281 "Directory does not exist for idx %s at '%s'" % (self.raw_id, path))
282 282 path = self._fix_path(path)
283 283
284 284 filenodes = [
285 285 FileNode(f, commit=self) for f in self._file_paths
286 286 if os.path.dirname(f) == path]
287 287 # TODO: johbo: Check if this can be done in a more obvious way
288 288 dirs = path == '' and '' or [
289 289 d for d in self._dir_paths
290 290 if d and vcspath.dirname(d) == path]
291 291 dirnodes = [
292 292 DirNode(d, commit=self) for d in dirs
293 293 if os.path.dirname(d) == path]
294 294
295 295 alias = self.repository.alias
296 296 for k, vals in self._submodules.iteritems():
297 297 if vcspath.dirname(k) == path:
298 298 loc = vals[0]
299 299 commit = vals[1]
300 300 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
301 301
302 302 nodes = dirnodes + filenodes
303 303 for node in nodes:
304 304 if node.path not in self.nodes:
305 305 self.nodes[node.path] = node
306 306 nodes.sort()
307 307
308 308 return nodes
309 309
310 310 def get_node(self, path, pre_load=None):
311 311 """
312 312 Returns `Node` object from the given `path`. If there is no node at
313 313 the given `path`, `NodeDoesNotExistError` would be raised.
314 314 """
315 315 path = self._fix_path(path)
316 316
317 317 if path not in self.nodes:
318 318 if path in self._file_paths:
319 319 node = FileNode(path, commit=self, pre_load=pre_load)
320 320 elif path in self._dir_paths:
321 321 if path == '':
322 322 node = RootNode(commit=self)
323 323 else:
324 324 node = DirNode(path, commit=self)
325 325 else:
326 326 raise self.no_node_at_path(path)
327 327
328 328 # cache node
329 329 self.nodes[path] = node
330 330 return self.nodes[path]
331 331
332 332 def get_largefile_node(self, path):
333
334 if self._remote.is_large_file(path):
333 pointer_spec = self._remote.is_large_file(path)
334 if pointer_spec:
335 335 # content of that file regular FileNode is the hash of largefile
336 336 file_id = self.get_file_content(path).strip()
337 337
338 338 if self._remote.in_largefiles_store(file_id):
339 339 lf_path = self._remote.store_path(file_id)
340 340 return LargeFileNode(lf_path, commit=self, org_path=path)
341 341 elif self._remote.in_user_cache(file_id):
342 342 lf_path = self._remote.store_path(file_id)
343 343 self._remote.link(file_id, path)
344 344 return LargeFileNode(lf_path, commit=self, org_path=path)
345 345
346 346 @LazyProperty
347 347 def _submodules(self):
348 348 """
349 349 Returns a dictionary with submodule information from substate file
350 350 of hg repository.
351 351 """
352 352 return self._remote.ctx_substate(self.raw_id)
353 353
354 354 @LazyProperty
355 355 def affected_files(self):
356 356 """
357 357 Gets a fast accessible file changes for given commit
358 358 """
359 359 return self._remote.ctx_files(self.raw_id)
360 360
361 361 @property
362 362 def added(self):
363 363 """
364 364 Returns list of added ``FileNode`` objects.
365 365 """
366 366 return AddedFileNodesGenerator([n for n in self.status[1]], self)
367 367
368 368 @property
369 369 def changed(self):
370 370 """
371 371 Returns list of modified ``FileNode`` objects.
372 372 """
373 373 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
374 374
375 375 @property
376 376 def removed(self):
377 377 """
378 378 Returns list of removed ``FileNode`` objects.
379 379 """
380 380 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,888 +1,894 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 os
22 22 import hashlib
23 23 import logging
24 24 from collections import namedtuple
25 25 from functools import wraps
26 26 import bleach
27 27
28 28 from rhodecode.lib import rc_cache
29 29 from rhodecode.lib.utils2 import (
30 30 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
31 31 from rhodecode.lib.vcs.backends import base
32 32 from rhodecode.model import BaseModel
33 33 from rhodecode.model.db import (
34 34 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting, CacheKey)
35 35 from rhodecode.model.meta import Session
36 36
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 UiSetting = namedtuple(
42 42 'UiSetting', ['section', 'key', 'value', 'active'])
43 43
44 44 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
45 45
46 46
47 47 class SettingNotFound(Exception):
48 48 def __init__(self, setting_id):
49 49 msg = 'Setting `{}` is not found'.format(setting_id)
50 50 super(SettingNotFound, self).__init__(msg)
51 51
52 52
53 53 class SettingsModel(BaseModel):
54 54 BUILTIN_HOOKS = (
55 55 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
56 56 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
57 57 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
58 58 RhodeCodeUi.HOOK_PUSH_KEY,)
59 59 HOOKS_SECTION = 'hooks'
60 60
61 61 def __init__(self, sa=None, repo=None):
62 62 self.repo = repo
63 63 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
64 64 self.SettingsDbModel = (
65 65 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
66 66 super(SettingsModel, self).__init__(sa)
67 67
68 68 def get_ui_by_key(self, key):
69 69 q = self.UiDbModel.query()
70 70 q = q.filter(self.UiDbModel.ui_key == key)
71 71 q = self._filter_by_repo(RepoRhodeCodeUi, q)
72 72 return q.scalar()
73 73
74 74 def get_ui_by_section(self, section):
75 75 q = self.UiDbModel.query()
76 76 q = q.filter(self.UiDbModel.ui_section == section)
77 77 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 78 return q.all()
79 79
80 80 def get_ui_by_section_and_key(self, section, key):
81 81 q = self.UiDbModel.query()
82 82 q = q.filter(self.UiDbModel.ui_section == section)
83 83 q = q.filter(self.UiDbModel.ui_key == key)
84 84 q = self._filter_by_repo(RepoRhodeCodeUi, q)
85 85 return q.scalar()
86 86
87 87 def get_ui(self, section=None, key=None):
88 88 q = self.UiDbModel.query()
89 89 q = self._filter_by_repo(RepoRhodeCodeUi, q)
90 90
91 91 if section:
92 92 q = q.filter(self.UiDbModel.ui_section == section)
93 93 if key:
94 94 q = q.filter(self.UiDbModel.ui_key == key)
95 95
96 96 # TODO: mikhail: add caching
97 97 result = [
98 98 UiSetting(
99 99 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
100 100 value=safe_str(r.ui_value), active=r.ui_active
101 101 )
102 102 for r in q.all()
103 103 ]
104 104 return result
105 105
106 106 def get_builtin_hooks(self):
107 107 q = self.UiDbModel.query()
108 108 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
109 109 return self._get_hooks(q)
110 110
111 111 def get_custom_hooks(self):
112 112 q = self.UiDbModel.query()
113 113 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
114 114 return self._get_hooks(q)
115 115
116 116 def create_ui_section_value(self, section, val, key=None, active=True):
117 117 new_ui = self.UiDbModel()
118 118 new_ui.ui_section = section
119 119 new_ui.ui_value = val
120 120 new_ui.ui_active = active
121 121
122 122 repository_id = ''
123 123 if self.repo:
124 124 repo = self._get_repo(self.repo)
125 125 repository_id = repo.repo_id
126 126 new_ui.repository_id = repository_id
127 127
128 128 if not key:
129 129 # keys are unique so they need appended info
130 130 if self.repo:
131 131 key = hashlib.sha1(
132 132 '{}{}{}'.format(section, val, repository_id)).hexdigest()
133 133 else:
134 134 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
135 135
136 136 new_ui.ui_key = key
137 137
138 138 Session().add(new_ui)
139 139 return new_ui
140 140
141 141 def create_or_update_hook(self, key, value):
142 142 ui = (
143 143 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
144 144 self.UiDbModel())
145 145 ui.ui_section = self.HOOKS_SECTION
146 146 ui.ui_active = True
147 147 ui.ui_key = key
148 148 ui.ui_value = value
149 149
150 150 if self.repo:
151 151 repo = self._get_repo(self.repo)
152 152 repository_id = repo.repo_id
153 153 ui.repository_id = repository_id
154 154
155 155 Session().add(ui)
156 156 return ui
157 157
158 158 def delete_ui(self, id_):
159 159 ui = self.UiDbModel.get(id_)
160 160 if not ui:
161 161 raise SettingNotFound(id_)
162 162 Session().delete(ui)
163 163
164 164 def get_setting_by_name(self, name):
165 165 q = self._get_settings_query()
166 166 q = q.filter(self.SettingsDbModel.app_settings_name == name)
167 167 return q.scalar()
168 168
169 169 def create_or_update_setting(
170 170 self, name, val=Optional(''), type_=Optional('unicode')):
171 171 """
172 172 Creates or updates RhodeCode setting. If updates is triggered it will
173 173 only update parameters that are explicityl set Optional instance will
174 174 be skipped
175 175
176 176 :param name:
177 177 :param val:
178 178 :param type_:
179 179 :return:
180 180 """
181 181
182 182 res = self.get_setting_by_name(name)
183 183 repo = self._get_repo(self.repo) if self.repo else None
184 184
185 185 if not res:
186 186 val = Optional.extract(val)
187 187 type_ = Optional.extract(type_)
188 188
189 189 args = (
190 190 (repo.repo_id, name, val, type_)
191 191 if repo else (name, val, type_))
192 192 res = self.SettingsDbModel(*args)
193 193
194 194 else:
195 195 if self.repo:
196 196 res.repository_id = repo.repo_id
197 197
198 198 res.app_settings_name = name
199 199 if not isinstance(type_, Optional):
200 200 # update if set
201 201 res.app_settings_type = type_
202 202 if not isinstance(val, Optional):
203 203 # update if set
204 204 res.app_settings_value = val
205 205
206 206 Session().add(res)
207 207 return res
208 208
209 209 def invalidate_settings_cache(self):
210 210 invalidation_namespace = CacheKey.SETTINGS_INVALIDATION_NAMESPACE
211 211 CacheKey.set_invalidate(invalidation_namespace)
212 212
213 213 def get_all_settings(self, cache=False):
214 214 region = rc_cache.get_or_create_region('sql_cache_short')
215 215 invalidation_namespace = CacheKey.SETTINGS_INVALIDATION_NAMESPACE
216 216
217 217 @region.conditional_cache_on_arguments(condition=cache)
218 218 def _get_all_settings(name, key):
219 219 q = self._get_settings_query()
220 220 if not q:
221 221 raise Exception('Could not get application settings !')
222 222
223 223 settings = {
224 224 'rhodecode_' + result.app_settings_name: result.app_settings_value
225 225 for result in q
226 226 }
227 227 return settings
228 228
229 229 repo = self._get_repo(self.repo) if self.repo else None
230 230 key = "settings_repo.{}".format(repo.repo_id) if repo else "settings_app"
231 231
232 232 inv_context_manager = rc_cache.InvalidationContext(
233 233 uid='cache_settings', invalidation_namespace=invalidation_namespace)
234 234 with inv_context_manager as invalidation_context:
235 235 # check for stored invalidation signal, and maybe purge the cache
236 236 # before computing it again
237 237 if invalidation_context.should_invalidate():
238 238 # NOTE:(marcink) we flush the whole sql_cache_short region, because it
239 239 # reads different settings etc. It's little too much but those caches
240 240 # are anyway very short lived and it's a safest way.
241 241 region = rc_cache.get_or_create_region('sql_cache_short')
242 242 region.invalidate()
243 243
244 244 result = _get_all_settings('rhodecode_settings', key)
245 245 log.debug('Fetching app settings for key: %s took: %.4fs', key,
246 246 inv_context_manager.compute_time)
247 247
248 248 return result
249 249
250 250 def get_auth_settings(self):
251 251 q = self._get_settings_query()
252 252 q = q.filter(
253 253 self.SettingsDbModel.app_settings_name.startswith('auth_'))
254 254 rows = q.all()
255 255 auth_settings = {
256 256 row.app_settings_name: row.app_settings_value for row in rows}
257 257 return auth_settings
258 258
259 259 def get_auth_plugins(self):
260 260 auth_plugins = self.get_setting_by_name("auth_plugins")
261 261 return auth_plugins.app_settings_value
262 262
263 263 def get_default_repo_settings(self, strip_prefix=False):
264 264 q = self._get_settings_query()
265 265 q = q.filter(
266 266 self.SettingsDbModel.app_settings_name.startswith('default_'))
267 267 rows = q.all()
268 268
269 269 result = {}
270 270 for row in rows:
271 271 key = row.app_settings_name
272 272 if strip_prefix:
273 273 key = remove_prefix(key, prefix='default_')
274 274 result.update({key: row.app_settings_value})
275 275 return result
276 276
277 277 def get_repo(self):
278 278 repo = self._get_repo(self.repo)
279 279 if not repo:
280 280 raise Exception(
281 281 'Repository `{}` cannot be found inside the database'.format(
282 282 self.repo))
283 283 return repo
284 284
285 285 def _filter_by_repo(self, model, query):
286 286 if self.repo:
287 287 repo = self.get_repo()
288 288 query = query.filter(model.repository_id == repo.repo_id)
289 289 return query
290 290
291 291 def _get_hooks(self, query):
292 292 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
293 293 query = self._filter_by_repo(RepoRhodeCodeUi, query)
294 294 return query.all()
295 295
296 296 def _get_settings_query(self):
297 297 q = self.SettingsDbModel.query()
298 298 return self._filter_by_repo(RepoRhodeCodeSetting, q)
299 299
300 300 def list_enabled_social_plugins(self, settings):
301 301 enabled = []
302 302 for plug in SOCIAL_PLUGINS_LIST:
303 303 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
304 304 )):
305 305 enabled.append(plug)
306 306 return enabled
307 307
308 308
309 309 def assert_repo_settings(func):
310 310 @wraps(func)
311 311 def _wrapper(self, *args, **kwargs):
312 312 if not self.repo_settings:
313 313 raise Exception('Repository is not specified')
314 314 return func(self, *args, **kwargs)
315 315 return _wrapper
316 316
317 317
318 318 class IssueTrackerSettingsModel(object):
319 319 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
320 320 SETTINGS_PREFIX = 'issuetracker_'
321 321
322 322 def __init__(self, sa=None, repo=None):
323 323 self.global_settings = SettingsModel(sa=sa)
324 324 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
325 325
326 326 @property
327 327 def inherit_global_settings(self):
328 328 if not self.repo_settings:
329 329 return True
330 330 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
331 331 return setting.app_settings_value if setting else True
332 332
333 333 @inherit_global_settings.setter
334 334 def inherit_global_settings(self, value):
335 335 if self.repo_settings:
336 336 settings = self.repo_settings.create_or_update_setting(
337 337 self.INHERIT_SETTINGS, value, type_='bool')
338 338 Session().add(settings)
339 339
340 340 def _get_keyname(self, key, uid, prefix=''):
341 341 return '{0}{1}{2}_{3}'.format(
342 342 prefix, self.SETTINGS_PREFIX, key, uid)
343 343
344 344 def _make_dict_for_settings(self, qs):
345 345 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
346 346
347 347 issuetracker_entries = {}
348 348 # create keys
349 349 for k, v in qs.items():
350 350 if k.startswith(prefix_match):
351 351 uid = k[len(prefix_match):]
352 352 issuetracker_entries[uid] = None
353 353
354 354 def url_cleaner(input_str):
355 355 input_str = input_str.replace('"', '').replace("'", '')
356 356 input_str = bleach.clean(input_str, strip=True)
357 357 return input_str
358 358
359 359 # populate
360 360 for uid in issuetracker_entries:
361 361 url_data = qs.get(self._get_keyname('url', uid, 'rhodecode_'))
362 362
363 363 issuetracker_entries[uid] = AttributeDict({
364 364 'pat': qs.get(
365 365 self._get_keyname('pat', uid, 'rhodecode_')),
366 366 'url': url_cleaner(
367 367 qs.get(self._get_keyname('url', uid, 'rhodecode_')) or ''),
368 368 'pref': bleach.clean(
369 369 qs.get(self._get_keyname('pref', uid, 'rhodecode_')) or ''),
370 370 'desc': qs.get(
371 371 self._get_keyname('desc', uid, 'rhodecode_')),
372 372 })
373 373
374 374 return issuetracker_entries
375 375
376 376 def get_global_settings(self, cache=False):
377 377 """
378 378 Returns list of global issue tracker settings
379 379 """
380 380 defaults = self.global_settings.get_all_settings(cache=cache)
381 381 settings = self._make_dict_for_settings(defaults)
382 382 return settings
383 383
384 384 def get_repo_settings(self, cache=False):
385 385 """
386 386 Returns list of issue tracker settings per repository
387 387 """
388 388 if not self.repo_settings:
389 389 raise Exception('Repository is not specified')
390 390 all_settings = self.repo_settings.get_all_settings(cache=cache)
391 391 settings = self._make_dict_for_settings(all_settings)
392 392 return settings
393 393
394 394 def get_settings(self, cache=False):
395 395 if self.inherit_global_settings:
396 396 return self.get_global_settings(cache=cache)
397 397 else:
398 398 return self.get_repo_settings(cache=cache)
399 399
400 400 def delete_entries(self, uid):
401 401 if self.repo_settings:
402 402 all_patterns = self.get_repo_settings()
403 403 settings_model = self.repo_settings
404 404 else:
405 405 all_patterns = self.get_global_settings()
406 406 settings_model = self.global_settings
407 407 entries = all_patterns.get(uid, [])
408 408
409 409 for del_key in entries:
410 410 setting_name = self._get_keyname(del_key, uid)
411 411 entry = settings_model.get_setting_by_name(setting_name)
412 412 if entry:
413 413 Session().delete(entry)
414 414
415 415 Session().commit()
416 416
417 417 def create_or_update_setting(
418 418 self, name, val=Optional(''), type_=Optional('unicode')):
419 419 if self.repo_settings:
420 420 setting = self.repo_settings.create_or_update_setting(
421 421 name, val, type_)
422 422 else:
423 423 setting = self.global_settings.create_or_update_setting(
424 424 name, val, type_)
425 425 return setting
426 426
427 427
428 428 class VcsSettingsModel(object):
429 429
430 430 INHERIT_SETTINGS = 'inherit_vcs_settings'
431 431 GENERAL_SETTINGS = (
432 432 'use_outdated_comments',
433 433 'pr_merge_enabled',
434 434 'hg_use_rebase_for_merging',
435 435 'hg_close_branch_before_merging',
436 436 'git_use_rebase_for_merging',
437 437 'git_close_branch_before_merging',
438 438 'diff_cache',
439 439 )
440 440
441 441 HOOKS_SETTINGS = (
442 442 ('hooks', 'changegroup.repo_size'),
443 443 ('hooks', 'changegroup.push_logger'),
444 444 ('hooks', 'outgoing.pull_logger'),
445 445 )
446 446 HG_SETTINGS = (
447 447 ('extensions', 'largefiles'),
448 448 ('phases', 'publish'),
449 449 ('extensions', 'evolve'),
450 450 ('extensions', 'topic'),
451 451 ('experimental', 'evolution'),
452 452 ('experimental', 'evolution.exchange'),
453 453 )
454 454 GIT_SETTINGS = (
455 455 ('vcs_git_lfs', 'enabled'),
456 456 )
457 457 GLOBAL_HG_SETTINGS = (
458 458 ('extensions', 'largefiles'),
459 459 ('largefiles', 'usercache'),
460 460 ('phases', 'publish'),
461 461 ('extensions', 'hgsubversion'),
462 462 ('extensions', 'evolve'),
463 463 ('extensions', 'topic'),
464 464 ('experimental', 'evolution'),
465 465 ('experimental', 'evolution.exchange'),
466 466 )
467 467
468 468 GLOBAL_GIT_SETTINGS = (
469 469 ('vcs_git_lfs', 'enabled'),
470 470 ('vcs_git_lfs', 'store_location')
471 471 )
472 472
473 473 GLOBAL_SVN_SETTINGS = (
474 474 ('vcs_svn_proxy', 'http_requests_enabled'),
475 475 ('vcs_svn_proxy', 'http_server_url')
476 476 )
477 477
478 478 SVN_BRANCH_SECTION = 'vcs_svn_branch'
479 479 SVN_TAG_SECTION = 'vcs_svn_tag'
480 480 SSL_SETTING = ('web', 'push_ssl')
481 481 PATH_SETTING = ('paths', '/')
482 482
483 483 def __init__(self, sa=None, repo=None):
484 484 self.global_settings = SettingsModel(sa=sa)
485 485 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
486 486 self._ui_settings = (
487 487 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
488 488 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
489 489
490 490 @property
491 491 @assert_repo_settings
492 492 def inherit_global_settings(self):
493 493 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
494 494 return setting.app_settings_value if setting else True
495 495
496 496 @inherit_global_settings.setter
497 497 @assert_repo_settings
498 498 def inherit_global_settings(self, value):
499 499 self.repo_settings.create_or_update_setting(
500 500 self.INHERIT_SETTINGS, value, type_='bool')
501 501
502 502 def get_global_svn_branch_patterns(self):
503 503 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
504 504
505 505 @assert_repo_settings
506 506 def get_repo_svn_branch_patterns(self):
507 507 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
508 508
509 509 def get_global_svn_tag_patterns(self):
510 510 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
511 511
512 512 @assert_repo_settings
513 513 def get_repo_svn_tag_patterns(self):
514 514 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
515 515
516 516 def get_global_settings(self):
517 517 return self._collect_all_settings(global_=True)
518 518
519 519 @assert_repo_settings
520 520 def get_repo_settings(self):
521 521 return self._collect_all_settings(global_=False)
522 522
523 523 @assert_repo_settings
524 def get_repo_settings_inherited(self):
525 global_settings = self.get_global_settings()
526 global_settings.update(self.get_repo_settings())
527 return global_settings
528
529 @assert_repo_settings
524 530 def create_or_update_repo_settings(
525 531 self, data, inherit_global_settings=False):
526 532 from rhodecode.model.scm import ScmModel
527 533
528 534 self.inherit_global_settings = inherit_global_settings
529 535
530 536 repo = self.repo_settings.get_repo()
531 537 if not inherit_global_settings:
532 538 if repo.repo_type == 'svn':
533 539 self.create_repo_svn_settings(data)
534 540 else:
535 541 self.create_or_update_repo_hook_settings(data)
536 542 self.create_or_update_repo_pr_settings(data)
537 543
538 544 if repo.repo_type == 'hg':
539 545 self.create_or_update_repo_hg_settings(data)
540 546
541 547 if repo.repo_type == 'git':
542 548 self.create_or_update_repo_git_settings(data)
543 549
544 550 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
545 551
546 552 @assert_repo_settings
547 553 def create_or_update_repo_hook_settings(self, data):
548 554 for section, key in self.HOOKS_SETTINGS:
549 555 data_key = self._get_form_ui_key(section, key)
550 556 if data_key not in data:
551 557 raise ValueError(
552 558 'The given data does not contain {} key'.format(data_key))
553 559
554 560 active = data.get(data_key)
555 561 repo_setting = self.repo_settings.get_ui_by_section_and_key(
556 562 section, key)
557 563 if not repo_setting:
558 564 global_setting = self.global_settings.\
559 565 get_ui_by_section_and_key(section, key)
560 566 self.repo_settings.create_ui_section_value(
561 567 section, global_setting.ui_value, key=key, active=active)
562 568 else:
563 569 repo_setting.ui_active = active
564 570 Session().add(repo_setting)
565 571
566 572 def update_global_hook_settings(self, data):
567 573 for section, key in self.HOOKS_SETTINGS:
568 574 data_key = self._get_form_ui_key(section, key)
569 575 if data_key not in data:
570 576 raise ValueError(
571 577 'The given data does not contain {} key'.format(data_key))
572 578 active = data.get(data_key)
573 579 repo_setting = self.global_settings.get_ui_by_section_and_key(
574 580 section, key)
575 581 repo_setting.ui_active = active
576 582 Session().add(repo_setting)
577 583
578 584 @assert_repo_settings
579 585 def create_or_update_repo_pr_settings(self, data):
580 586 return self._create_or_update_general_settings(
581 587 self.repo_settings, data)
582 588
583 589 def create_or_update_global_pr_settings(self, data):
584 590 return self._create_or_update_general_settings(
585 591 self.global_settings, data)
586 592
587 593 @assert_repo_settings
588 594 def create_repo_svn_settings(self, data):
589 595 return self._create_svn_settings(self.repo_settings, data)
590 596
591 597 def _set_evolution(self, settings, is_enabled):
592 598 if is_enabled:
593 599 # if evolve is active set evolution=all
594 600
595 601 self._create_or_update_ui(
596 602 settings, *('experimental', 'evolution'), value='all',
597 603 active=True)
598 604 self._create_or_update_ui(
599 605 settings, *('experimental', 'evolution.exchange'), value='yes',
600 606 active=True)
601 607 # if evolve is active set topics server support
602 608 self._create_or_update_ui(
603 609 settings, *('extensions', 'topic'), value='',
604 610 active=True)
605 611
606 612 else:
607 613 self._create_or_update_ui(
608 614 settings, *('experimental', 'evolution'), value='',
609 615 active=False)
610 616 self._create_or_update_ui(
611 617 settings, *('experimental', 'evolution.exchange'), value='no',
612 618 active=False)
613 619 self._create_or_update_ui(
614 620 settings, *('extensions', 'topic'), value='',
615 621 active=False)
616 622
617 623 @assert_repo_settings
618 624 def create_or_update_repo_hg_settings(self, data):
619 625 largefiles, phases, evolve = \
620 626 self.HG_SETTINGS[:3]
621 627 largefiles_key, phases_key, evolve_key = \
622 628 self._get_settings_keys(self.HG_SETTINGS[:3], data)
623 629
624 630 self._create_or_update_ui(
625 631 self.repo_settings, *largefiles, value='',
626 632 active=data[largefiles_key])
627 633 self._create_or_update_ui(
628 634 self.repo_settings, *evolve, value='',
629 635 active=data[evolve_key])
630 636 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
631 637
632 638 self._create_or_update_ui(
633 639 self.repo_settings, *phases, value=safe_str(data[phases_key]))
634 640
635 641 def create_or_update_global_hg_settings(self, data):
636 642 largefiles, largefiles_store, phases, hgsubversion, evolve \
637 643 = self.GLOBAL_HG_SETTINGS[:5]
638 644 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
639 645 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data)
640 646
641 647 self._create_or_update_ui(
642 648 self.global_settings, *largefiles, value='',
643 649 active=data[largefiles_key])
644 650 self._create_or_update_ui(
645 651 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
646 652 self._create_or_update_ui(
647 653 self.global_settings, *phases, value=safe_str(data[phases_key]))
648 654 self._create_or_update_ui(
649 655 self.global_settings, *hgsubversion, active=data[subversion_key])
650 656 self._create_or_update_ui(
651 657 self.global_settings, *evolve, value='',
652 658 active=data[evolve_key])
653 659 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
654 660
655 661 def create_or_update_repo_git_settings(self, data):
656 662 # NOTE(marcink): # comma makes unpack work properly
657 663 lfs_enabled, \
658 664 = self.GIT_SETTINGS
659 665
660 666 lfs_enabled_key, \
661 667 = self._get_settings_keys(self.GIT_SETTINGS, data)
662 668
663 669 self._create_or_update_ui(
664 670 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
665 671 active=data[lfs_enabled_key])
666 672
667 673 def create_or_update_global_git_settings(self, data):
668 674 lfs_enabled, lfs_store_location \
669 675 = self.GLOBAL_GIT_SETTINGS
670 676 lfs_enabled_key, lfs_store_location_key \
671 677 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
672 678
673 679 self._create_or_update_ui(
674 680 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
675 681 active=data[lfs_enabled_key])
676 682 self._create_or_update_ui(
677 683 self.global_settings, *lfs_store_location,
678 684 value=data[lfs_store_location_key])
679 685
680 686 def create_or_update_global_svn_settings(self, data):
681 687 # branch/tags patterns
682 688 self._create_svn_settings(self.global_settings, data)
683 689
684 690 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
685 691 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
686 692 self.GLOBAL_SVN_SETTINGS, data)
687 693
688 694 self._create_or_update_ui(
689 695 self.global_settings, *http_requests_enabled,
690 696 value=safe_str(data[http_requests_enabled_key]))
691 697 self._create_or_update_ui(
692 698 self.global_settings, *http_server_url,
693 699 value=data[http_server_url_key])
694 700
695 701 def update_global_ssl_setting(self, value):
696 702 self._create_or_update_ui(
697 703 self.global_settings, *self.SSL_SETTING, value=value)
698 704
699 705 def update_global_path_setting(self, value):
700 706 self._create_or_update_ui(
701 707 self.global_settings, *self.PATH_SETTING, value=value)
702 708
703 709 @assert_repo_settings
704 710 def delete_repo_svn_pattern(self, id_):
705 711 ui = self.repo_settings.UiDbModel.get(id_)
706 712 if ui and ui.repository.repo_name == self.repo_settings.repo:
707 713 # only delete if it's the same repo as initialized settings
708 714 self.repo_settings.delete_ui(id_)
709 715 else:
710 716 # raise error as if we wouldn't find this option
711 717 self.repo_settings.delete_ui(-1)
712 718
713 719 def delete_global_svn_pattern(self, id_):
714 720 self.global_settings.delete_ui(id_)
715 721
716 722 @assert_repo_settings
717 723 def get_repo_ui_settings(self, section=None, key=None):
718 724 global_uis = self.global_settings.get_ui(section, key)
719 725 repo_uis = self.repo_settings.get_ui(section, key)
720 726
721 727 filtered_repo_uis = self._filter_ui_settings(repo_uis)
722 728 filtered_repo_uis_keys = [
723 729 (s.section, s.key) for s in filtered_repo_uis]
724 730
725 731 def _is_global_ui_filtered(ui):
726 732 return (
727 733 (ui.section, ui.key) in filtered_repo_uis_keys
728 734 or ui.section in self._svn_sections)
729 735
730 736 filtered_global_uis = [
731 737 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
732 738
733 739 return filtered_global_uis + filtered_repo_uis
734 740
735 741 def get_global_ui_settings(self, section=None, key=None):
736 742 return self.global_settings.get_ui(section, key)
737 743
738 744 def get_ui_settings_as_config_obj(self, section=None, key=None):
739 745 config = base.Config()
740 746
741 747 ui_settings = self.get_ui_settings(section=section, key=key)
742 748
743 749 for entry in ui_settings:
744 750 config.set(entry.section, entry.key, entry.value)
745 751
746 752 return config
747 753
748 754 def get_ui_settings(self, section=None, key=None):
749 755 if not self.repo_settings or self.inherit_global_settings:
750 756 return self.get_global_ui_settings(section, key)
751 757 else:
752 758 return self.get_repo_ui_settings(section, key)
753 759
754 760 def get_svn_patterns(self, section=None):
755 761 if not self.repo_settings:
756 762 return self.get_global_ui_settings(section)
757 763 else:
758 764 return self.get_repo_ui_settings(section)
759 765
760 766 @assert_repo_settings
761 767 def get_repo_general_settings(self):
762 768 global_settings = self.global_settings.get_all_settings()
763 769 repo_settings = self.repo_settings.get_all_settings()
764 770 filtered_repo_settings = self._filter_general_settings(repo_settings)
765 771 global_settings.update(filtered_repo_settings)
766 772 return global_settings
767 773
768 774 def get_global_general_settings(self):
769 775 return self.global_settings.get_all_settings()
770 776
771 777 def get_general_settings(self):
772 778 if not self.repo_settings or self.inherit_global_settings:
773 779 return self.get_global_general_settings()
774 780 else:
775 781 return self.get_repo_general_settings()
776 782
777 783 def get_repos_location(self):
778 784 return self.global_settings.get_ui_by_key('/').ui_value
779 785
780 786 def _filter_ui_settings(self, settings):
781 787 filtered_settings = [
782 788 s for s in settings if self._should_keep_setting(s)]
783 789 return filtered_settings
784 790
785 791 def _should_keep_setting(self, setting):
786 792 keep = (
787 793 (setting.section, setting.key) in self._ui_settings or
788 794 setting.section in self._svn_sections)
789 795 return keep
790 796
791 797 def _filter_general_settings(self, settings):
792 798 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
793 799 return {
794 800 k: settings[k]
795 801 for k in settings if k in keys}
796 802
797 803 def _collect_all_settings(self, global_=False):
798 804 settings = self.global_settings if global_ else self.repo_settings
799 805 result = {}
800 806
801 807 for section, key in self._ui_settings:
802 808 ui = settings.get_ui_by_section_and_key(section, key)
803 809 result_key = self._get_form_ui_key(section, key)
804 810
805 811 if ui:
806 812 if section in ('hooks', 'extensions'):
807 813 result[result_key] = ui.ui_active
808 814 elif result_key in ['vcs_git_lfs_enabled']:
809 815 result[result_key] = ui.ui_active
810 816 else:
811 817 result[result_key] = ui.ui_value
812 818
813 819 for name in self.GENERAL_SETTINGS:
814 820 setting = settings.get_setting_by_name(name)
815 821 if setting:
816 822 result_key = 'rhodecode_{}'.format(name)
817 823 result[result_key] = setting.app_settings_value
818 824
819 825 return result
820 826
821 827 def _get_form_ui_key(self, section, key):
822 828 return '{section}_{key}'.format(
823 829 section=section, key=key.replace('.', '_'))
824 830
825 831 def _create_or_update_ui(
826 832 self, settings, section, key, value=None, active=None):
827 833 ui = settings.get_ui_by_section_and_key(section, key)
828 834 if not ui:
829 835 active = True if active is None else active
830 836 settings.create_ui_section_value(
831 837 section, value, key=key, active=active)
832 838 else:
833 839 if active is not None:
834 840 ui.ui_active = active
835 841 if value is not None:
836 842 ui.ui_value = value
837 843 Session().add(ui)
838 844
839 845 def _create_svn_settings(self, settings, data):
840 846 svn_settings = {
841 847 'new_svn_branch': self.SVN_BRANCH_SECTION,
842 848 'new_svn_tag': self.SVN_TAG_SECTION
843 849 }
844 850 for key in svn_settings:
845 851 if data.get(key):
846 852 settings.create_ui_section_value(svn_settings[key], data[key])
847 853
848 854 def _create_or_update_general_settings(self, settings, data):
849 855 for name in self.GENERAL_SETTINGS:
850 856 data_key = 'rhodecode_{}'.format(name)
851 857 if data_key not in data:
852 858 raise ValueError(
853 859 'The given data does not contain {} key'.format(data_key))
854 860 setting = settings.create_or_update_setting(
855 861 name, data[data_key], 'bool')
856 862 Session().add(setting)
857 863
858 864 def _get_settings_keys(self, settings, data):
859 865 data_keys = [self._get_form_ui_key(*s) for s in settings]
860 866 for data_key in data_keys:
861 867 if data_key not in data:
862 868 raise ValueError(
863 869 'The given data does not contain {} key'.format(data_key))
864 870 return data_keys
865 871
866 872 def create_largeobjects_dirs_if_needed(self, repo_store_path):
867 873 """
868 874 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
869 875 does a repository scan if enabled in the settings.
870 876 """
871 877
872 878 from rhodecode.lib.vcs.backends.hg import largefiles_store
873 879 from rhodecode.lib.vcs.backends.git import lfs_store
874 880
875 881 paths = [
876 882 largefiles_store(repo_store_path),
877 883 lfs_store(repo_store_path)]
878 884
879 885 for path in paths:
880 886 if os.path.isdir(path):
881 887 continue
882 888 if os.path.isfile(path):
883 889 continue
884 890 # not a file nor dir, we try to create it
885 891 try:
886 892 os.makedirs(path)
887 893 except Exception:
888 894 log.warning('Failed to create largefiles dir:%s', path)
General Comments 0
You need to be logged in to leave comments. Login now