##// END OF EJS Templates
added delay before doing an auto merge
added delay before doing an auto merge

File last commit:

r5651:bad147da default
r5660:cba707aa default
Show More
repo.py
1220 lines | 44.6 KiB | text/x-python | PythonLexer
# Copyright (C) 2010-2024 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import re
import shutil
import time
import logging
import traceback
import datetime
from pyramid.threadlocal import get_current_request
from sqlalchemy.orm import aliased
from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode import events
from rhodecode.lib.auth import HasUserGroupPermissionAny
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
from rhodecode.lib import hooks_base
from rhodecode.lib.str_utils import safe_bytes
from rhodecode.lib.user_log_filter import user_log_filter
from rhodecode.lib.utils import make_db_config
from rhodecode.lib.utils2 import (
safe_str, remove_prefix, obfuscate_url_pw,
get_current_rhodecode_user, safe_int, action_logger_generic)
from rhodecode.lib.vcs.backends import get_backend
from rhodecode.lib.vcs.nodes import NodeKind
from rhodecode.model import BaseModel
from rhodecode.model.db import (
_hash_key, func, case, joinedload, or_, in_filter_generator,
Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.settings import VcsSettingsModel
log = logging.getLogger(__name__)
class RepoModel(BaseModel):
cls = Repository
def _get_user_group(self, users_group):
return self._get_instance(UserGroup, users_group,
callback=UserGroup.get_by_group_name)
def _get_repo_group(self, repo_group):
return self._get_instance(RepoGroup, repo_group,
callback=RepoGroup.get_by_group_name)
def _create_default_perms(self, repository, private):
# create default permission
default = 'repository.read'
def_user = User.get_default_user()
for p in def_user.user_perms:
if p.permission.permission_name.startswith('repository.'):
default = p.permission.permission_name
break
default_perm = 'repository.none' if private else default
repo_to_perm = UserRepoToPerm()
repo_to_perm.permission = Permission.get_by_key(default_perm)
repo_to_perm.repository = repository
repo_to_perm.user = def_user
return repo_to_perm
def get(self, repo_id):
repo = self.sa.query(Repository) \
.filter(Repository.repo_id == repo_id)
return repo.scalar()
def get_repo(self, repository):
return self._get_repo(repository)
def get_by_repo_name(self, repo_name, cache=False):
repo = self.sa.query(Repository) \
.filter(Repository.repo_name == repo_name)
if cache:
name_key = _hash_key(repo_name)
repo = repo.options(
FromCache("sql_cache_short", f"get_repo_{name_key}"))
return repo.scalar()
def _extract_id_from_repo_name(self, repo_name):
if repo_name.startswith('/'):
repo_name = repo_name.lstrip('/')
by_id_match = re.match(r'^_(\d+)', repo_name)
if by_id_match:
return by_id_match.groups()[0]
def get_repo_by_id(self, repo_name):
"""
Extracts repo_name by id from special urls.
Example url is _11/repo_name
:param repo_name:
:return: repo object if matched else None
"""
_repo_id = None
try:
_repo_id = self._extract_id_from_repo_name(repo_name)
if _repo_id:
return self.get(_repo_id)
except Exception:
log.exception('Failed to extract repo_name from URL')
if _repo_id:
Session().rollback()
return None
def get_repos_for_root(self, root, traverse=False):
if traverse:
like_expression = u'{}%'.format(safe_str(root))
repos = Repository.query().filter(
Repository.repo_name.like(like_expression)).all()
else:
if root and not isinstance(root, RepoGroup):
raise ValueError(
'Root must be an instance '
'of RepoGroup, got:{} instead'.format(type(root)))
repos = Repository.query().filter(Repository.group == root).all()
return repos
def get_url(self, repo, request=None, permalink=False):
if not request:
request = get_current_request()
if not request:
return
if permalink:
return request.route_url(
'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
else:
return request.route_url(
'repo_summary', repo_name=safe_str(repo.repo_name))
def get_commit_url(self, repo, commit_id, request=None, permalink=False):
if not request:
request = get_current_request()
if not request:
return
if permalink:
return request.route_url(
'repo_commit', repo_name=safe_str(repo.repo_id),
commit_id=commit_id)
else:
return request.route_url(
'repo_commit', repo_name=safe_str(repo.repo_name),
commit_id=commit_id)
def get_repo_log(self, repo, filter_term):
repo_log = UserLog.query()\
.filter(or_(UserLog.repository_id == repo.repo_id,
UserLog.repository_name == repo.repo_name))\
.options(joinedload(UserLog.user))\
.options(joinedload(UserLog.repository))\
.order_by(UserLog.action_date.desc())
repo_log = user_log_filter(repo_log, filter_term)
return repo_log
@classmethod
def update_commit_cache(cls, repositories=None):
if not repositories:
repositories = Repository.getAll()
for repo in repositories:
repo.update_commit_cache()
def get_repos_as_dict(self, repo_list=None, admin=False,
super_user_actions=False, short_name=None):
_render = get_current_request().get_partial_renderer(
'rhodecode:templates/data_table/_dt_elements.mako')
c = _render.get_call_context()
h = _render.get_helpers()
def quick_menu(repo_name):
return _render('quick_menu', repo_name)
def repo_lnk(name, rtype, rstate, private, archived, fork_repo_name):
if short_name is not None:
short_name_var = short_name
else:
short_name_var = not admin
return _render('repo_name', name, rtype, rstate, private, archived, fork_repo_name,
short_name=short_name_var, admin=False)
def last_change(last_change):
if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
ts = time.time()
utc_offset = (datetime.datetime.fromtimestamp(ts)
- datetime.datetime.utcfromtimestamp(ts)).total_seconds()
last_change = last_change + datetime.timedelta(seconds=utc_offset)
return _render("last_change", last_change)
def rss_lnk(repo_name):
return _render("rss", repo_name)
def atom_lnk(repo_name):
return _render("atom", repo_name)
def last_rev(repo_name, cs_cache):
return _render('revision', repo_name, cs_cache.get('revision'),
cs_cache.get('raw_id'), cs_cache.get('author'),
cs_cache.get('message'), cs_cache.get('date'))
def desc(desc):
return _render('repo_desc', desc, c.visual.stylify_metatags)
def state(repo_state):
return _render("repo_state", repo_state)
def repo_actions(repo_name):
return _render('repo_actions', repo_name, super_user_actions)
def user_profile(username):
return _render('user_profile', username)
repos_data = []
for repo in repo_list:
# NOTE(marcink): because we use only raw column we need to load it like that
changeset_cache = Repository._load_changeset_cache(
repo.repo_id, repo._changeset_cache)
row = {
"menu": quick_menu(repo.repo_name),
"name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
repo.private, repo.archived, repo.fork_repo_name),
"desc": desc(h.escape(repo.description)),
"last_change": last_change(repo.updated_on),
"last_changeset": last_rev(repo.repo_name, changeset_cache),
"last_changeset_raw": changeset_cache.get('revision'),
"owner": user_profile(repo.owner_username),
"state": state(repo.repo_state),
"rss": rss_lnk(repo.repo_name),
"atom": atom_lnk(repo.repo_name),
}
if admin:
row.update({
"action": repo_actions(repo.repo_name),
})
repos_data.append(row)
return repos_data
def get_repos_data_table(
self, draw, start, limit,
search_q, order_by, order_dir,
auth_user, repo_group_id):
from rhodecode.model.scm import RepoList
_perms = ['repository.read', 'repository.write', 'repository.admin']
repos = Repository.query() \
.filter(Repository.group_id == repo_group_id) \
.all()
auth_repo_list = RepoList(
repos, perm_set=_perms,
extra_kwargs=dict(user=auth_user))
allowed_ids = [-1]
for repo in auth_repo_list:
allowed_ids.append(repo.repo_id)
repos_data_total_count = Repository.query() \
.filter(Repository.group_id == repo_group_id) \
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(Repository.repo_id, allowed_ids))
) \
.count()
RepoFork = aliased(Repository)
OwnerUser = aliased(User)
base_q = Session.query(
Repository.repo_id,
Repository.repo_name,
Repository.description,
Repository.repo_type,
Repository.repo_state,
Repository.private,
Repository.archived,
Repository.updated_on,
Repository._changeset_cache,
RepoFork.repo_name.label('fork_repo_name'),
OwnerUser.username.label('owner_username'),
) \
.filter(Repository.group_id == repo_group_id) \
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(Repository.repo_id, allowed_ids))
) \
.outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
.join(OwnerUser, Repository.user_id == OwnerUser.user_id)
repos_data_total_filtered_count = base_q.count()
sort_defined = False
if order_by == 'repo_name':
sort_col = func.lower(Repository.repo_name)
sort_defined = True
elif order_by == 'user_username':
sort_col = User.username
else:
sort_col = getattr(Repository, order_by, None)
if sort_defined or sort_col:
if order_dir == 'asc':
sort_col = sort_col.asc()
else:
sort_col = sort_col.desc()
base_q = base_q.order_by(sort_col)
base_q = base_q.offset(start).limit(limit)
repos_list = base_q.all()
repos_data = RepoModel().get_repos_as_dict(
repo_list=repos_list, admin=False)
data = ({
'draw': draw,
'data': repos_data,
'recordsTotal': repos_data_total_count,
'recordsFiltered': repos_data_total_filtered_count,
})
return data
def _get_defaults(self, repo_name):
"""
Gets information about repository, and returns a dict for
usage in forms
:param repo_name:
"""
repo_info = Repository.get_by_repo_name(repo_name)
if repo_info is None:
return None
defaults = repo_info.get_dict()
defaults['repo_name'] = repo_info.just_name
groups = repo_info.groups_with_parents
parent_group = groups[-1] if groups else None
# we use -1 as this is how in HTML, we mark an empty group
defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
keys_to_process = (
{'k': 'repo_type', 'strip': False},
{'k': 'repo_enable_downloads', 'strip': True},
{'k': 'repo_description', 'strip': True},
{'k': 'repo_enable_locking', 'strip': True},
{'k': 'repo_landing_rev', 'strip': True},
{'k': 'clone_uri', 'strip': False},
{'k': 'push_uri', 'strip': False},
{'k': 'repo_private', 'strip': True},
{'k': 'repo_enable_statistics', 'strip': True}
)
for item in keys_to_process:
attr = item['k']
if item['strip']:
attr = remove_prefix(item['k'], 'repo_')
val = defaults[attr]
if item['k'] == 'repo_landing_rev':
val = ':'.join(defaults[attr])
defaults[item['k']] = val
if item['k'] == 'clone_uri':
defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
if item['k'] == 'push_uri':
defaults['push_uri_hidden'] = repo_info.push_uri_hidden
# fill owner
if repo_info.user:
defaults.update({'user': repo_info.user.username})
else:
replacement_user = User.get_first_super_admin().username
defaults.update({'user': replacement_user})
return defaults
def update(self, repo, **kwargs):
try:
cur_repo = self._get_repo(repo)
source_repo_name = cur_repo.repo_name
affected_user_ids = []
if 'user' in kwargs:
old_owner_id = cur_repo.user.user_id
new_owner = User.get_by_username(kwargs['user'])
cur_repo.user = new_owner
if old_owner_id != new_owner.user_id:
affected_user_ids = [new_owner.user_id, old_owner_id]
if 'repo_group' in kwargs:
cur_repo.group = RepoGroup.get(kwargs['repo_group'])
log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
update_keys = [
(1, 'repo_description'),
(1, 'repo_landing_rev'),
(1, 'repo_private'),
(1, 'repo_enable_downloads'),
(1, 'repo_enable_locking'),
(1, 'repo_enable_statistics'),
(0, 'clone_uri'),
(0, 'push_uri'),
(0, 'fork_id')
]
for strip, k in update_keys:
if k in kwargs:
val = kwargs[k]
if strip:
k = remove_prefix(k, 'repo_')
setattr(cur_repo, k, val)
new_name = source_repo_name
if 'repo_name' in kwargs:
new_name = cur_repo.get_new_name(kwargs['repo_name'])
cur_repo.repo_name = new_name
if 'repo_private' in kwargs:
# if private flag is set to True, reset default permission to NONE
set_private_to = kwargs.get('repo_private')
if set_private_to:
EMPTY_PERM = 'repository.none'
RepoModel().grant_user_permission(
repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
)
if set_private_to != cur_repo.private:
# NOTE(dan): we change repo private mode we need to notify all USERS
# this is just by having this value set to a different value then it was before
affected_user_ids = User.get_all_user_ids()
if kwargs.get('repo_landing_rev'):
landing_rev_val = kwargs['repo_landing_rev']
RepoModel().set_landing_rev(cur_repo, landing_rev_val)
# handle extra fields
for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
k = RepositoryField.un_prefix_key(field)
ex_field = RepositoryField.get_by_key_name(
key=k, repo=cur_repo)
if ex_field:
ex_field.field_value = kwargs[field]
self.sa.add(ex_field)
self.sa.add(cur_repo)
if source_repo_name != new_name:
# rename repository
self._rename_filesystem_repo(
old=source_repo_name, new=new_name)
if affected_user_ids:
PermissionModel().trigger_permission_flush(affected_user_ids)
return cur_repo
except Exception:
log.error(traceback.format_exc())
raise
def _create_repo(self, repo_name, repo_type, description, owner,
private=False, clone_uri=None, repo_group=None,
landing_rev=None, fork_of=None,
copy_fork_permissions=False, enable_statistics=False,
enable_locking=False, enable_downloads=False,
copy_group_permissions=False,
state=Repository.STATE_PENDING):
"""
Create repository inside database with PENDING state, this should be
only executed by create() repo. With exception of importing existing
repos
"""
from rhodecode.model.scm import ScmModel
owner = self._get_user(owner)
fork_of = self._get_repo(fork_of)
repo_group = self._get_repo_group(safe_int(repo_group))
default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
landing_rev = landing_rev or default_landing_ref
try:
repo_name = safe_str(repo_name)
description = safe_str(description)
# repo name is just a name of repository
# while repo_name_full is a full qualified name that is combined
# with name and path of group
repo_name_full = repo_name
repo_name = repo_name.split(Repository.NAME_SEP)[-1]
new_repo = Repository()
new_repo.repo_state = state
new_repo.enable_statistics = False
new_repo.repo_name = repo_name_full
new_repo.repo_type = repo_type
new_repo.user = owner
new_repo.group = repo_group
new_repo.description = description or repo_name
new_repo.private = private
new_repo.archived = False
new_repo.clone_uri = clone_uri
new_repo.landing_rev = landing_rev
new_repo.enable_statistics = enable_statistics
new_repo.enable_locking = enable_locking
new_repo.enable_downloads = enable_downloads
if repo_group:
new_repo.enable_locking = repo_group.enable_locking
if fork_of:
parent_repo = fork_of
new_repo.fork = parent_repo
events.trigger(events.RepoPreCreateEvent(new_repo))
self.sa.add(new_repo)
EMPTY_PERM = 'repository.none'
if fork_of and copy_fork_permissions:
repo = fork_of
user_perms = UserRepoToPerm.query() \
.filter(UserRepoToPerm.repository == repo).all()
group_perms = UserGroupRepoToPerm.query() \
.filter(UserGroupRepoToPerm.repository == repo).all()
for perm in user_perms:
UserRepoToPerm.create(
perm.user, new_repo, perm.permission)
for perm in group_perms:
UserGroupRepoToPerm.create(
perm.users_group, new_repo, perm.permission)
# in case we copy permissions and also set this repo to private
# override the default user permission to make it a private repo
if private:
RepoModel(self.sa).grant_user_permission(
repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
elif repo_group and copy_group_permissions:
user_perms = UserRepoGroupToPerm.query() \
.filter(UserRepoGroupToPerm.group == repo_group).all()
group_perms = UserGroupRepoGroupToPerm.query() \
.filter(UserGroupRepoGroupToPerm.group == repo_group).all()
for perm in user_perms:
perm_name = perm.permission.permission_name.replace(
'group.', 'repository.')
perm_obj = Permission.get_by_key(perm_name)
UserRepoToPerm.create(perm.user, new_repo, perm_obj)
for perm in group_perms:
perm_name = perm.permission.permission_name.replace(
'group.', 'repository.')
perm_obj = Permission.get_by_key(perm_name)
UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
if private:
RepoModel(self.sa).grant_user_permission(
repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
else:
perm_obj = self._create_default_perms(new_repo, private)
self.sa.add(perm_obj)
# now automatically start following this repository as owner
ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
# we need to flush here, in order to check if database won't
# throw any exceptions, create filesystem dirs at the very end
self.sa.flush()
events.trigger(events.RepoCreateEvent(new_repo, actor=owner))
return new_repo
except Exception:
log.error(traceback.format_exc())
raise
def create(self, form_data, cur_user):
"""
Create repository using celery tasks
:param form_data:
:param cur_user:
"""
from rhodecode.lib.celerylib import tasks, run_task
return run_task(tasks.create_repo, form_data, cur_user)
def update_permissions(self, repo, perm_additions=None, perm_updates=None,
perm_deletions=None, check_perms=True,
cur_user=None):
if not perm_additions:
perm_additions = []
if not perm_updates:
perm_updates = []
if not perm_deletions:
perm_deletions = []
req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
changes = {
'added': [],
'updated': [],
'deleted': [],
'default_user_changed': None
}
repo = self._get_repo(repo)
# update permissions
for member_id, perm, member_type in perm_updates:
member_id = int(member_id)
if member_type == 'user':
member_name = User.get(member_id).username
if member_name == User.DEFAULT_USER:
# NOTE(dan): detect if we changed permissions for default user
perm_obj = self.sa.query(UserRepoToPerm) \
.filter(UserRepoToPerm.user_id == member_id) \
.filter(UserRepoToPerm.repository == repo) \
.scalar()
if perm_obj and perm_obj.permission.permission_name != perm:
changes['default_user_changed'] = True
# this updates also current one if found
self.grant_user_permission(
repo=repo, user=member_id, perm=perm)
elif member_type == 'user_group':
# check if we have permissions to alter this usergroup
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or HasUserGroupPermissionAny(
*req_perms)(member_name, user=cur_user):
self.grant_user_group_permission(
repo=repo, group_name=member_id, perm=perm)
else:
raise ValueError("member_type must be 'user' or 'user_group' "
"got {} instead".format(member_type))
changes['updated'].append({'type': member_type, 'id': member_id,
'name': member_name, 'new_perm': perm})
# set new permissions
for member_id, perm, member_type in perm_additions:
member_id = int(member_id)
if member_type == 'user':
member_name = User.get(member_id).username
self.grant_user_permission(
repo=repo, user=member_id, perm=perm)
elif member_type == 'user_group':
# check if we have permissions to alter this usergroup
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or HasUserGroupPermissionAny(
*req_perms)(member_name, user=cur_user):
self.grant_user_group_permission(
repo=repo, group_name=member_id, perm=perm)
else:
raise ValueError("member_type must be 'user' or 'user_group' "
"got {} instead".format(member_type))
changes['added'].append({'type': member_type, 'id': member_id,
'name': member_name, 'new_perm': perm})
# delete permissions
for member_id, perm, member_type in perm_deletions:
member_id = int(member_id)
if member_type == 'user':
member_name = User.get(member_id).username
self.revoke_user_permission(repo=repo, user=member_id)
elif member_type == 'user_group':
# check if we have permissions to alter this usergroup
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or HasUserGroupPermissionAny(
*req_perms)(member_name, user=cur_user):
self.revoke_user_group_permission(
repo=repo, group_name=member_id)
else:
raise ValueError("member_type must be 'user' or 'user_group' "
"got {} instead".format(member_type))
changes['deleted'].append({'type': member_type, 'id': member_id,
'name': member_name, 'new_perm': perm})
return changes
def create_fork(self, form_data, cur_user):
"""
Simple wrapper into executing celery task for fork creation
:param form_data:
:param cur_user:
"""
from rhodecode.lib.celerylib import tasks, run_task
return run_task(tasks.create_repo_fork, form_data, cur_user)
def archive(self, repo):
"""
Archive given repository. Set archive flag.
:param repo:
"""
repo = self._get_repo(repo)
if repo:
try:
repo.archived = True
self.sa.add(repo)
self.sa.commit()
except Exception:
log.error(traceback.format_exc())
raise
def delete(self, repo, forks=None, pull_requests=None, artifacts=None, fs_remove=True, cur_user=None, call_events=True):
"""
Delete given repository, forks parameter defines what do do with
attached forks. Throws AttachedForksError if deleted repo has attached
forks
:param repo:
:param forks: str 'delete' or 'detach'
:param pull_requests: str 'delete' or None
:param artifacts: str 'delete' or None
:param fs_remove: remove(archive) repo from filesystem
"""
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
repo = self._get_repo(repo)
if not repo:
return False
if forks == 'detach':
for r in repo.forks:
r.fork = None
self.sa.add(r)
elif forks == 'delete':
for r in repo.forks:
self.delete(r, forks='delete')
elif [f for f in repo.forks]:
raise AttachedForksError()
# check for pull requests
pr_sources = repo.pull_requests_source
pr_targets = repo.pull_requests_target
if pull_requests != 'delete' and (pr_sources or pr_targets):
raise AttachedPullRequestsError()
artifacts_objs = repo.artifacts
if artifacts == 'delete':
for a in artifacts_objs:
self.sa.delete(a)
elif [a for a in artifacts_objs]:
raise AttachedArtifactsError()
old_repo_dict = repo.get_dict()
if call_events:
events.trigger(events.RepoPreDeleteEvent(repo))
try:
self.sa.delete(repo)
if fs_remove:
self._delete_filesystem_repo(repo)
else:
log.debug('skipping removal from filesystem')
old_repo_dict.update({
'deleted_by': cur_user,
'deleted_on': time.time(),
})
if call_events:
hooks_base.delete_repository(**old_repo_dict)
events.trigger(events.RepoDeleteEvent(repo))
except Exception:
log.error(traceback.format_exc())
raise
return True
def grant_user_permission(self, repo, user, perm):
"""
Grant permission for user on given repository, or update existing one
if found
:param repo: Instance of Repository, repository_id, or repository name
:param user: Instance of User, user_id or username
:param perm: Instance of Permission, or permission_name
"""
user = self._get_user(user)
repo = self._get_repo(repo)
permission = self._get_perm(perm)
# check if we have that permission already
obj = self.sa.query(UserRepoToPerm) \
.filter(UserRepoToPerm.user == user) \
.filter(UserRepoToPerm.repository == repo) \
.scalar()
if obj is None:
# create new !
obj = UserRepoToPerm()
obj.repository = repo
obj.user = user
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s', perm, user, repo)
action_logger_generic(
'granted permission: {} to user: {} on repo: {}'.format(
perm, user, repo), namespace='security.repo')
return obj
def revoke_user_permission(self, repo, user):
"""
Revoke permission for user on given repository
:param repo: Instance of Repository, repository_id, or repository name
:param user: Instance of User, user_id or username
"""
user = self._get_user(user)
repo = self._get_repo(repo)
obj = self.sa.query(UserRepoToPerm) \
.filter(UserRepoToPerm.repository == repo) \
.filter(UserRepoToPerm.user == user) \
.scalar()
if obj:
self.sa.delete(obj)
log.debug('Revoked perm on %s on %s', repo, user)
action_logger_generic(
'revoked permission from user: {} on repo: {}'.format(
user, repo), namespace='security.repo')
def grant_user_group_permission(self, repo, group_name, perm):
"""
Grant permission for user group on given repository, or update
existing one if found
:param repo: Instance of Repository, repository_id, or repository name
:param group_name: Instance of UserGroup, users_group_id,
or user group name
:param perm: Instance of Permission, or permission_name
"""
repo = self._get_repo(repo)
group_name = self._get_user_group(group_name)
permission = self._get_perm(perm)
# check if we have that permission already
obj = self.sa.query(UserGroupRepoToPerm) \
.filter(UserGroupRepoToPerm.users_group == group_name) \
.filter(UserGroupRepoToPerm.repository == repo) \
.scalar()
if obj is None:
# create new
obj = UserGroupRepoToPerm()
obj.repository = repo
obj.users_group = group_name
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
action_logger_generic(
'granted permission: {} to usergroup: {} on repo: {}'.format(
perm, group_name, repo), namespace='security.repo')
return obj
def revoke_user_group_permission(self, repo, group_name):
"""
Revoke permission for user group on given repository
:param repo: Instance of Repository, repository_id, or repository name
:param group_name: Instance of UserGroup, users_group_id,
or user group name
"""
repo = self._get_repo(repo)
group_name = self._get_user_group(group_name)
obj = self.sa.query(UserGroupRepoToPerm) \
.filter(UserGroupRepoToPerm.repository == repo) \
.filter(UserGroupRepoToPerm.users_group == group_name) \
.scalar()
if obj:
self.sa.delete(obj)
log.debug('Revoked perm to %s on %s', repo, group_name)
action_logger_generic(
'revoked permission from usergroup: {} on repo: {}'.format(
group_name, repo), namespace='security.repo')
def delete_stats(self, repo_name):
"""
removes stats for given repo
:param repo_name:
"""
repo = self._get_repo(repo_name)
try:
obj = self.sa.query(Statistics) \
.filter(Statistics.repository == repo).scalar()
if obj:
self.sa.delete(obj)
except Exception:
log.error(traceback.format_exc())
raise
def add_repo_field(self, repo_name, field_key, field_label, field_value='',
field_type='str', field_desc=''):
repo = self._get_repo(repo_name)
new_field = RepositoryField()
new_field.repository = repo
new_field.field_key = field_key
new_field.field_type = field_type # python type
new_field.field_value = field_value
new_field.field_desc = field_desc
new_field.field_label = field_label
self.sa.add(new_field)
return new_field
def delete_repo_field(self, repo_name, field_key):
repo = self._get_repo(repo_name)
field = RepositoryField.get_by_key_name(field_key, repo)
if field:
self.sa.delete(field)
def set_landing_rev(self, repo, landing_rev_name):
if landing_rev_name.startswith('branch:'):
landing_rev_name = landing_rev_name.split('branch:')[-1]
scm_instance = repo.scm_instance()
if scm_instance:
return scm_instance._remote.set_head_ref(landing_rev_name)
def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
clone_uri=None, repo_store_location=None,
use_global_config=False, install_hooks=True):
"""
makes repository on filesystem. It's group aware means it'll create
a repository within a group, and alter the paths accordingly of
group location
:param repo_name:
:param alias:
:param parent:
:param clone_uri:
:param repo_store_location:
"""
from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
from rhodecode.model.scm import ScmModel
if Repository.NAME_SEP in repo_name:
raise ValueError(
'repo_name must not contain groups got `%s`' % repo_name)
if isinstance(repo_group, RepoGroup):
new_parent_path = os.sep.join(repo_group.full_path_splitted)
else:
new_parent_path = repo_group or ''
if repo_store_location:
_paths = [repo_store_location]
else:
_paths = [self.repos_path, new_parent_path, repo_name]
# we need to make it str for mercurial
repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
# check if this path is not a repository
if is_valid_repo(repo_path, self.repos_path):
raise Exception(f'This path {repo_path} is a valid repository')
# check if this path is a group
if is_valid_repo_group(repo_path, self.repos_path):
raise Exception(f'This path {repo_path} is a valid group')
log.info('creating repo %s in %s from url: `%s`',
repo_name, safe_str(repo_path),
obfuscate_url_pw(clone_uri))
backend = get_backend(repo_type)
config_repo = None if use_global_config else repo_name
if config_repo and new_parent_path:
config_repo = Repository.NAME_SEP.join(
(new_parent_path, config_repo))
config = make_db_config(clear_session=False, repo=config_repo)
config.set('extensions', 'largefiles', '')
# patch and reset hooks section of UI config to not run any
# hooks on creating remote repo
config.clear_section('hooks')
# TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
if repo_type == 'git':
repo = backend(
repo_path, config=config, create=True, src_url=clone_uri, bare=True,
with_wire={"cache": False})
else:
repo = backend(
repo_path, config=config, create=True, src_url=clone_uri,
with_wire={"cache": False})
if install_hooks:
repo.install_hooks()
log.debug('Created repo %s with %s backend',
safe_str(repo_name), safe_str(repo_type))
return repo
def _rename_filesystem_repo(self, old, new):
"""
renames repository on filesystem
:param old: old name
:param new: new name
"""
log.info('renaming repo from %s to %s', old, new)
old_path = os.path.join(self.repos_path, old)
new_path = os.path.join(self.repos_path, new)
if os.path.isdir(new_path):
raise Exception(
'Was trying to rename to already existing dir %s' % new_path
)
shutil.move(old_path, new_path)
def _delete_filesystem_repo(self, repo):
"""
removes repo from filesystem, the removal is actually made by
added rm__ prefix into dir, and rename internal .hg/.git dirs so this
repository is no longer valid for rhodecode, can be undeleted later on
by reverting the renames on this repository
:param repo: repo object
"""
rm_path = os.path.join(self.repos_path, repo.repo_name)
repo_group = repo.group
log.info("delete_filesystem_repo: removing repository %s", rm_path)
# disable hg/git internal that it doesn't get detected as repo
alias = repo.repo_type
config = make_db_config(clear_session=False)
config.set('extensions', 'largefiles', '')
bare = getattr(repo.scm_instance(config=config), 'bare', False)
# skip this for bare git repos
if not bare:
# disable VCS repo
vcs_path = os.path.join(rm_path, '.%s' % alias)
if os.path.exists(vcs_path):
shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
_now = datetime.datetime.now()
_ms = str(_now.microsecond).rjust(6, '0')
_d = 'rm__{}__{}'.format(_now.strftime('%Y%m%d_%H%M%S_' + _ms),
repo.just_name)
if repo_group:
# if repository is in group, prefix the removal path with the group
args = repo_group.full_path_splitted + [_d]
_d = os.path.join(*args)
if os.path.isdir(rm_path):
shutil.move(rm_path, os.path.join(self.repos_path, _d))
# finally cleanup diff-cache if it exists
cached_diffs_dir = repo.cached_diffs_dir
if os.path.isdir(cached_diffs_dir):
shutil.rmtree(cached_diffs_dir)
class ReadmeFinder:
"""
Utility which knows how to find a readme for a specific commit.
The main idea is that this is a configurable algorithm. When creating an
instance you can define parameters, currently only the `default_renderer`.
Based on this configuration the method :meth:`search` behaves slightly
different.
"""
readme_re = re.compile(br'^readme(\.[^.]+)?$', re.IGNORECASE)
path_re = re.compile(br'^docs?', re.IGNORECASE)
default_priorities = {
None: 0,
b'.rst': 1,
b'.md': 1,
b'.rest': 2,
b'.mkdn': 2,
b'.text': 2,
b'.txt': 3,
b'.mdown': 3,
b'.markdown': 4,
}
path_priority = {
b'doc': 0,
b'docs': 1,
}
FALLBACK_PRIORITY = 99
RENDERER_TO_EXTENSION = {
'rst': [b'.rst', b'.rest'],
'markdown': [b'.md', b'mkdn', b'.mdown', b'.markdown'],
}
def __init__(self, default_renderer=None):
self._default_renderer = default_renderer
self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(default_renderer, [])
def search(self, commit, path=b'/', nodes=None):
"""
Find a readme in the given `commit`.
"""
# firstly, check the PATH type if it is actually a DIR
bytes_path = safe_bytes(path)
if commit.get_node(bytes_path).kind != NodeKind.DIR:
return None
if not nodes:
nodes = commit.get_nodes(bytes_path)
matches = self._match_readmes(nodes)
matches = self._sort_according_to_priority(matches)
if matches:
return matches[0].node
paths = self._match_paths(nodes)
paths = self._sort_paths_according_to_priority(paths)
for bytes_path in paths:
match = self.search(commit, path=bytes_path)
if match:
return match
return None
def _match_readmes(self, nodes):
for node in nodes:
if not node.is_file():
continue
path = node.bytes_path.rsplit(b'/', 1)[-1]
match = self.readme_re.match(path)
if match:
extension = match.group(1)
yield ReadmeMatch(node, match, self._priority(extension))
def _match_paths(self, nodes):
for node in nodes:
if not node.is_dir():
continue
match = self.path_re.match(node.bytes_path)
if match:
yield node.bytes_path
def _priority(self, extension):
renderer_priority = 0 if extension in self._renderer_extensions else 1
extension_priority = self.default_priorities.get(extension, self.FALLBACK_PRIORITY)
return renderer_priority, extension_priority
def _sort_according_to_priority(self, matches):
def priority_and_path(match):
return match.priority, match.path
return sorted(matches, key=priority_and_path)
def _sort_paths_according_to_priority(self, paths):
def priority_and_path(path):
return self.path_priority.get(path, self.FALLBACK_PRIORITY), path
return sorted(paths, key=priority_and_path)
class ReadmeMatch:
def __init__(self, node, match, priority):
self.node = node
self._match = match
self.priority = priority
@property
def path(self):
return self.node.path
def __repr__(self):
return f'<ReadmeMatch {self.path} priority={self.priority}'