scm.py
767 lines
| 27.8 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4187 | # -*- coding: utf-8 -*- | ||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# 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 General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
kallithea.model.scm | ||||
~~~~~~~~~~~~~~~~~~~ | ||||
Bradley M. Kuhn
|
r4212 | Scm model for Kallithea | ||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4211 | This file was forked by the Kallithea project in July 2014. | ||
Original author and date, and relevant copyright and licensing information is below: | ||||
Bradley M. Kuhn
|
r4187 | :created_on: Apr 9, 2010 | ||
:author: marcink | ||||
Bradley M. Kuhn
|
r4211 | :copyright: (c) 2013 RhodeCode GmbH, and others. | ||
Bradley M. Kuhn
|
r4208 | :license: GPLv3, see LICENSE.md for more details. | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r7718 | import logging | ||
Bradley M. Kuhn
|
r4187 | import os | ||
domruf
|
r5716 | import posixpath | ||
Bradley M. Kuhn
|
r4187 | import re | ||
Mads Kiilerich
|
r7718 | import sys | ||
Bradley M. Kuhn
|
r4187 | import traceback | ||
Mads Kiilerich
|
r7718 | |||
Bradley M. Kuhn
|
r4187 | import pkg_resources | ||
Mads Kiilerich
|
r6508 | from tg.i18n import ugettext as _ | ||
Bradley M. Kuhn
|
r4187 | |||
import kallithea | ||||
from kallithea import BACKENDS | ||||
Mads Kiilerich
|
r7718 | from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, HasUserGroupPermissionLevel | ||
from kallithea.lib.exceptions import IMCCommitError, NonRelativePathError | ||||
from kallithea.lib.hooks import process_pushed_raw_ids | ||||
from kallithea.lib.utils import action_logger, get_filesystem_repos, make_ui | ||||
Mads Kiilerich
|
r8076 | from kallithea.lib.utils2 import safe_bytes, set_hook_environment | ||
Mads Kiilerich
|
r7718 | from kallithea.lib.vcs import get_backend | ||
from kallithea.lib.vcs.backends.base import EmptyChangeset | ||||
from kallithea.lib.vcs.exceptions import RepositoryError | ||||
from kallithea.lib.vcs.nodes import FileNode | ||||
from kallithea.lib.vcs.utils.lazy import LazyProperty | ||||
Mads Kiilerich
|
r7828 | from kallithea.model.db import PullRequest, RepoGroup, Repository, Session, Ui, User, UserFollowing, UserLog | ||
Mads Kiilerich
|
r7718 | |||
Bradley M. Kuhn
|
r4187 | |||
log = logging.getLogger(__name__) | ||||
class UserTemp(object): | ||||
def __init__(self, user_id): | ||||
self.user_id = user_id | ||||
def __repr__(self): | ||||
return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id) | ||||
class RepoTemp(object): | ||||
def __init__(self, repo_id): | ||||
self.repo_id = repo_id | ||||
def __repr__(self): | ||||
return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id) | ||||
class _PermCheckIterator(object): | ||||
def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None): | ||||
""" | ||||
Creates iterator from given list of objects, additionally | ||||
checking permission for them from perm_set var | ||||
:param obj_list: list of db objects | ||||
:param obj_attr: attribute of object to pass into perm_checker | ||||
:param perm_set: list of permissions to check | ||||
:param perm_checker: callable to check permissions against | ||||
""" | ||||
self.obj_list = obj_list | ||||
self.obj_attr = obj_attr | ||||
self.perm_set = perm_set | ||||
self.perm_checker = perm_checker | ||||
self.extra_kwargs = extra_kwargs or {} | ||||
def __len__(self): | ||||
return len(self.obj_list) | ||||
def __repr__(self): | ||||
return '<%s (%s)>' % (self.__class__.__name__, self.__len__()) | ||||
def __iter__(self): | ||||
for db_obj in self.obj_list: | ||||
# check permission at this level | ||||
name = getattr(db_obj, self.obj_attr, None) | ||||
if not self.perm_checker(*self.perm_set)( | ||||
name, self.__class__.__name__, **self.extra_kwargs): | ||||
continue | ||||
yield db_obj | ||||
class RepoList(_PermCheckIterator): | ||||
Søren Løvborg
|
r6471 | def __init__(self, db_repo_list, perm_level, extra_kwargs=None): | ||
Bradley M. Kuhn
|
r4187 | super(RepoList, self).__init__(obj_list=db_repo_list, | ||
Søren Løvborg
|
r6471 | obj_attr='repo_name', perm_set=[perm_level], | ||
perm_checker=HasRepoPermissionLevel, | ||||
Bradley M. Kuhn
|
r4187 | extra_kwargs=extra_kwargs) | ||
class RepoGroupList(_PermCheckIterator): | ||||
Søren Løvborg
|
r6472 | def __init__(self, db_repo_group_list, perm_level, extra_kwargs=None): | ||
Bradley M. Kuhn
|
r4187 | super(RepoGroupList, self).__init__(obj_list=db_repo_group_list, | ||
Søren Løvborg
|
r6472 | obj_attr='group_name', perm_set=[perm_level], | ||
perm_checker=HasRepoGroupPermissionLevel, | ||||
Bradley M. Kuhn
|
r4187 | extra_kwargs=extra_kwargs) | ||
class UserGroupList(_PermCheckIterator): | ||||
Søren Løvborg
|
r6473 | def __init__(self, db_user_group_list, perm_level, extra_kwargs=None): | ||
Bradley M. Kuhn
|
r4187 | super(UserGroupList, self).__init__(obj_list=db_user_group_list, | ||
Søren Løvborg
|
r6473 | obj_attr='users_group_name', perm_set=[perm_level], | ||
perm_checker=HasUserGroupPermissionLevel, | ||||
Bradley M. Kuhn
|
r4187 | extra_kwargs=extra_kwargs) | ||
Søren Løvborg
|
r6483 | class ScmModel(object): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Generic Scm Model | ||||
""" | ||||
def __get_repo(self, instance): | ||||
cls = Repository | ||||
if isinstance(instance, cls): | ||||
return instance | ||||
Mads Kiilerich
|
r7908 | elif isinstance(instance, int): | ||
Bradley M. Kuhn
|
r4187 | return cls.get(instance) | ||
Mads Kiilerich
|
r8064 | elif isinstance(instance, str): | ||
Mads Kiilerich
|
r7908 | if instance.isdigit(): | ||
return cls.get(int(instance)) | ||||
Bradley M. Kuhn
|
r4187 | return cls.get_by_repo_name(instance) | ||
Mads Kiilerich
|
r5306 | elif instance is not None: | ||
Bradley M. Kuhn
|
r4187 | raise Exception('given object must be int, basestr or Instance' | ||
' of %s got %s' % (type(cls), type(instance))) | ||||
@LazyProperty | ||||
def repos_path(self): | ||||
""" | ||||
Gets the repositories root path from database | ||||
""" | ||||
Søren Løvborg
|
r6483 | q = Ui.query().filter(Ui.ui_key == '/').one() | ||
Bradley M. Kuhn
|
r4187 | |||
return q.ui_value | ||||
def repo_scan(self, repos_path=None): | ||||
""" | ||||
Listing of repositories in given path. This path should not be a | ||||
Mads Kiilerich
|
r7946 | repository itself. Return a dictionary of repository objects mapping to | ||
vcs instances. | ||||
Bradley M. Kuhn
|
r4187 | |||
:param repos_path: path to directory containing repositories | ||||
""" | ||||
if repos_path is None: | ||||
repos_path = self.repos_path | ||||
Mads Kiilerich
|
r5375 | log.info('scanning for repositories in %s', repos_path) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7580 | baseui = make_ui() | ||
Bradley M. Kuhn
|
r4187 | repos = {} | ||
Mads Kiilerich
|
r6014 | for name, path in get_filesystem_repos(repos_path): | ||
Bradley M. Kuhn
|
r4187 | # name need to be decomposed and put back together using the / | ||
Bradley M. Kuhn
|
r4212 | # since this is internal storage separator for kallithea | ||
Bradley M. Kuhn
|
r4187 | name = Repository.normalize_repo_name(name) | ||
try: | ||||
if name in repos: | ||||
raise RepositoryError('Duplicate repository name %s ' | ||||
'found in %s' % (name, path)) | ||||
else: | ||||
klass = get_backend(path[0]) | ||||
Mads Kiilerich
|
r7906 | if path[0] == 'hg' and path[0] in BACKENDS: | ||
Mads Kiilerich
|
r8076 | repos[name] = klass(path[1], baseui=baseui) | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7906 | if path[0] == 'git' and path[0] in BACKENDS: | ||
Bradley M. Kuhn
|
r4187 | repos[name] = klass(path[1]) | ||
except OSError: | ||||
continue | ||||
Mads Kiilerich
|
r5375 | log.debug('found %s paths with repositories', len(repos)) | ||
Bradley M. Kuhn
|
r4187 | return repos | ||
Mads Kiilerich
|
r5734 | def get_repos(self, repos): | ||
"""Return the repos the user has access to""" | ||||
Søren Løvborg
|
r6471 | return RepoList(repos, perm_level='read') | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5734 | def get_repo_groups(self, groups=None): | ||
"""Return the repo groups the user has access to | ||||
If no groups are specified, use top level groups. | ||||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r5734 | if groups is None: | ||
groups = RepoGroup.query() \ | ||||
Søren Løvborg
|
r6280 | .filter(RepoGroup.parent_group_id == None).all() | ||
Søren Løvborg
|
r6472 | return RepoGroupList(groups, perm_level='read') | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5533 | def mark_for_invalidation(self, repo_name): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mark caches of this repo invalid in the database. | ||||
:param repo_name: the repo for which caches should be marked invalid | ||||
""" | ||||
Mads Kiilerich
|
r7635 | log.debug("Marking %s as invalidated and update cache", repo_name) | ||
Bradley M. Kuhn
|
r4187 | repo = Repository.get_by_repo_name(repo_name) | ||
Mads Kiilerich
|
r5306 | if repo is not None: | ||
Mads Kiilerich
|
r7828 | repo.set_invalidate() | ||
Bradley M. Kuhn
|
r4187 | repo.update_changeset_cache() | ||
def toggle_following_repo(self, follow_repo_id, user_id): | ||||
Søren Løvborg
|
r6483 | f = UserFollowing.query() \ | ||
Søren Løvborg
|
r6281 | .filter(UserFollowing.follows_repository_id == follow_repo_id) \ | ||
Bradley M. Kuhn
|
r4187 | .filter(UserFollowing.user_id == user_id).scalar() | ||
if f is not None: | ||||
try: | ||||
Søren Løvborg
|
r6483 | Session().delete(f) | ||
Bradley M. Kuhn
|
r4187 | action_logger(UserTemp(user_id), | ||
'stopped_following_repo', | ||||
RepoTemp(follow_repo_id)) | ||||
return | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
try: | ||||
f = UserFollowing() | ||||
f.user_id = user_id | ||||
Søren Løvborg
|
r6281 | f.follows_repository_id = follow_repo_id | ||
Søren Løvborg
|
r6483 | Session().add(f) | ||
Bradley M. Kuhn
|
r4187 | |||
action_logger(UserTemp(user_id), | ||||
'started_following_repo', | ||||
RepoTemp(follow_repo_id)) | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
def toggle_following_user(self, follow_user_id, user_id): | ||||
Søren Løvborg
|
r6483 | f = UserFollowing.query() \ | ||
Mads Kiilerich
|
r5585 | .filter(UserFollowing.follows_user_id == follow_user_id) \ | ||
Bradley M. Kuhn
|
r4187 | .filter(UserFollowing.user_id == user_id).scalar() | ||
if f is not None: | ||||
try: | ||||
Søren Løvborg
|
r6483 | Session().delete(f) | ||
Bradley M. Kuhn
|
r4187 | return | ||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
try: | ||||
f = UserFollowing() | ||||
f.user_id = user_id | ||||
f.follows_user_id = follow_user_id | ||||
Søren Løvborg
|
r6483 | Session().add(f) | ||
Bradley M. Kuhn
|
r4187 | except Exception: | ||
log.error(traceback.format_exc()) | ||||
raise | ||||
Mads Kiilerich
|
r7829 | def is_following_repo(self, repo_name, user_id): | ||
Søren Løvborg
|
r6483 | r = Repository.query() \ | ||
Bradley M. Kuhn
|
r4187 | .filter(Repository.repo_name == repo_name).scalar() | ||
Søren Løvborg
|
r6483 | f = UserFollowing.query() \ | ||
Mads Kiilerich
|
r5585 | .filter(UserFollowing.follows_repository == r) \ | ||
Bradley M. Kuhn
|
r4187 | .filter(UserFollowing.user_id == user_id).scalar() | ||
return f is not None | ||||
Mads Kiilerich
|
r7829 | def is_following_user(self, username, user_id): | ||
Bradley M. Kuhn
|
r4187 | u = User.get_by_username(username) | ||
Søren Løvborg
|
r6483 | f = UserFollowing.query() \ | ||
Mads Kiilerich
|
r5585 | .filter(UserFollowing.follows_user == u) \ | ||
Bradley M. Kuhn
|
r4187 | .filter(UserFollowing.user_id == user_id).scalar() | ||
return f is not None | ||||
def get_followers(self, repo): | ||||
Søren Løvborg
|
r6424 | repo = Repository.guess_instance(repo) | ||
Bradley M. Kuhn
|
r4187 | |||
Søren Løvborg
|
r6483 | return UserFollowing.query() \ | ||
Bradley M. Kuhn
|
r4187 | .filter(UserFollowing.follows_repository == repo).count() | ||
def get_forks(self, repo): | ||||
Søren Løvborg
|
r6424 | repo = Repository.guess_instance(repo) | ||
Søren Løvborg
|
r6483 | return Repository.query() \ | ||
Bradley M. Kuhn
|
r4187 | .filter(Repository.fork == repo).count() | ||
def get_pull_requests(self, repo): | ||||
Søren Løvborg
|
r6424 | repo = Repository.guess_instance(repo) | ||
Søren Løvborg
|
r6483 | return PullRequest.query() \ | ||
Mads Kiilerich
|
r5585 | .filter(PullRequest.other_repo == repo) \ | ||
Bradley M. Kuhn
|
r4187 | .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count() | ||
def mark_as_fork(self, repo, fork, user): | ||||
repo = self.__get_repo(repo) | ||||
fork = self.__get_repo(fork) | ||||
if fork and repo.repo_id == fork.repo_id: | ||||
raise Exception("Cannot set repository as fork of itself") | ||||
if fork and repo.repo_type != fork.repo_type: | ||||
raise RepositoryError("Cannot set repository as fork of repository with other type") | ||||
repo.fork = fork | ||||
return repo | ||||
Mads Kiilerich
|
r7632 | def _handle_push(self, repo, username, ip_addr, action, repo_name, revisions): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Mads Kiilerich
|
r7581 | Handle that the repository has changed. | ||
Adds an action log entry with the new revisions, and the head revision | ||||
cache and in-memory caches are invalidated/updated. | ||||
Bradley M. Kuhn
|
r4187 | |||
:param username: username who pushes | ||||
Thomas De Schampheleire
|
r4919 | :param action: push/push_local/push_remote | ||
Bradley M. Kuhn
|
r4187 | :param repo_name: name of repo | ||
:param revisions: list of revisions that we pushed | ||||
""" | ||||
Mads Kiilerich
|
r7634 | set_hook_environment(username, ip_addr, repo_name, repo_alias=repo.alias, action=action) | ||
Mads Kiilerich
|
r7581 | process_pushed_raw_ids(revisions) # also calls mark_for_invalidation | ||
Bradley M. Kuhn
|
r4187 | |||
def _get_IMC_module(self, scm_type): | ||||
""" | ||||
Returns InMemoryCommit class based on scm_type | ||||
:param scm_type: | ||||
""" | ||||
if scm_type == 'hg': | ||||
from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset | ||||
return MercurialInMemoryChangeset | ||||
if scm_type == 'git': | ||||
from kallithea.lib.vcs.backends.git import GitInMemoryChangeset | ||||
return GitInMemoryChangeset | ||||
raise Exception('Invalid scm_type, must be one of hg,git got %s' | ||||
% (scm_type,)) | ||||
Mads Kiilerich
|
r7632 | def pull_changes(self, repo, username, ip_addr, clone_uri=None): | ||
Mads Kiilerich
|
r5415 | """ | ||
domruf
|
r6603 | Pull from "clone URL" or fork origin. | ||
Mads Kiilerich
|
r5415 | """ | ||
Bradley M. Kuhn
|
r4187 | dbrepo = self.__get_repo(repo) | ||
Thomas De Schampheleire
|
r7053 | if clone_uri is None: | ||
clone_uri = dbrepo.clone_uri or dbrepo.fork and dbrepo.fork.repo_full_path | ||||
Bradley M. Kuhn
|
r4187 | if not clone_uri: | ||
raise Exception("This repository doesn't have a clone uri") | ||||
repo = dbrepo.scm_instance | ||||
repo_name = dbrepo.repo_name | ||||
try: | ||||
if repo.alias == 'git': | ||||
repo.fetch(clone_uri) | ||||
# git doesn't really have something like post-fetch action | ||||
Mads Kiilerich
|
r7581 | # we fake that now. | ||
# TODO: extract fetched revisions ... somehow ... | ||||
Bradley M. Kuhn
|
r4187 | self._handle_push(repo, | ||
username=username, | ||||
Mads Kiilerich
|
r7632 | ip_addr=ip_addr, | ||
Bradley M. Kuhn
|
r4187 | action='push_remote', | ||
repo_name=repo_name, | ||||
revisions=[]) | ||||
else: | ||||
Mads Kiilerich
|
r7634 | set_hook_environment(username, ip_addr, dbrepo.repo_name, | ||
Bradley M. Kuhn
|
r4187 | repo.alias, action='push_remote') | ||
repo.pull(clone_uri) | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
Mads Kiilerich
|
r7632 | def commit_change(self, repo, repo_name, cs, user, ip_addr, author, message, | ||
Bradley M. Kuhn
|
r4187 | content, f_path): | ||
""" | ||||
Mads Kiilerich
|
r5415 | Commit a change to a single file | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5415 | :param repo: a db_repo.scm_instance | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Søren Løvborg
|
r6423 | user = User.guess_instance(user) | ||
Bradley M. Kuhn
|
r4187 | IMC = self._get_IMC_module(repo.alias) | ||
imc = IMC(repo) | ||||
Mads Kiilerich
|
r8076 | imc.change(FileNode(f_path, content, mode=cs.get_file_mode(f_path))) | ||
Bradley M. Kuhn
|
r4187 | try: | ||
tip = imc.commit(message=message, author=author, | ||||
parents=[cs], branch=cs.branch) | ||||
Mads Kiilerich
|
r5374 | except Exception as e: | ||
Bradley M. Kuhn
|
r4187 | log.error(traceback.format_exc()) | ||
Mads Kiilerich
|
r7635 | # clear caches - we also want a fresh object if commit fails | ||
self.mark_for_invalidation(repo_name) | ||||
Bradley M. Kuhn
|
r4187 | raise IMCCommitError(str(e)) | ||
self._handle_push(repo, | ||||
username=user.username, | ||||
Mads Kiilerich
|
r7632 | ip_addr=ip_addr, | ||
Bradley M. Kuhn
|
r4187 | action='push_local', | ||
repo_name=repo_name, | ||||
revisions=[tip.raw_id]) | ||||
return tip | ||||
def _sanitize_path(self, f_path): | ||||
if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path: | ||||
raise NonRelativePathError('%s is not an relative path' % f_path) | ||||
if f_path: | ||||
domruf
|
r5716 | f_path = posixpath.normpath(f_path) | ||
Bradley M. Kuhn
|
r4187 | return f_path | ||
def get_nodes(self, repo_name, revision, root_path='/', flat=True): | ||||
""" | ||||
Na'Tosha Bard
|
r4526 | Recursively walk root dir and return a set of all paths found. | ||
Bradley M. Kuhn
|
r4187 | |||
:param repo_name: name of repository | ||||
:param revision: revision for which to list nodes | ||||
:param root_path: root path to list | ||||
Na'Tosha Bard
|
r4525 | :param flat: return as a list, if False returns a dict with description | ||
Bradley M. Kuhn
|
r4187 | |||
""" | ||||
_files = list() | ||||
_dirs = list() | ||||
try: | ||||
_repo = self.__get_repo(repo_name) | ||||
changeset = _repo.scm_instance.get_changeset(revision) | ||||
root_path = root_path.lstrip('/') | ||||
for topnode, dirs, files in changeset.walk(root_path): | ||||
for f in files: | ||||
_files.append(f.path if flat else {"name": f.path, | ||||
"type": "file"}) | ||||
for d in dirs: | ||||
_dirs.append(d.path if flat else {"name": d.path, | ||||
"type": "dir"}) | ||||
except RepositoryError: | ||||
log.debug(traceback.format_exc()) | ||||
raise | ||||
return _dirs, _files | ||||
Mads Kiilerich
|
r7632 | def create_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None, | ||
Bradley M. Kuhn
|
r4187 | author=None, trigger_push_hook=True): | ||
""" | ||||
Na'Tosha Bard
|
r4526 | Commits specified nodes to repo. | ||
Bradley M. Kuhn
|
r4187 | |||
Thomas De Schampheleire
|
r4919 | :param user: Kallithea User object or user_id, the committer | ||
Bradley M. Kuhn
|
r4212 | :param repo: Kallithea Repository object | ||
Bradley M. Kuhn
|
r4187 | :param message: commit message | ||
:param nodes: mapping {filename:{'content':content},...} | ||||
:param parent_cs: parent changeset, can be empty than it's initial commit | ||||
Thomas De Schampheleire
|
r4919 | :param author: author of commit, cna be different that committer only for git | ||
Bradley M. Kuhn
|
r4187 | :param trigger_push_hook: trigger push hooks | ||
Thomas De Schampheleire
|
r4919 | :returns: new committed changeset | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Søren Løvborg
|
r6423 | user = User.guess_instance(user) | ||
Bradley M. Kuhn
|
r4187 | scm_instance = repo.scm_instance_no_cache() | ||
processed_nodes = [] | ||||
for f_path in nodes: | ||||
domruf
|
r5712 | content = nodes[f_path]['content'] | ||
Bradley M. Kuhn
|
r4187 | f_path = self._sanitize_path(f_path) | ||
Mads Kiilerich
|
r8076 | if not isinstance(content, str) and not isinstance(content, bytes): | ||
Bradley M. Kuhn
|
r4187 | content = content.read() | ||
processed_nodes.append((f_path, content)) | ||||
Mads Kiilerich
|
r8075 | message = message | ||
Thomas De Schampheleire
|
r4919 | committer = user.full_contact | ||
Mads Kiilerich
|
r8075 | if not author: | ||
author = committer | ||||
Bradley M. Kuhn
|
r4187 | |||
IMC = self._get_IMC_module(scm_instance.alias) | ||||
imc = IMC(scm_instance) | ||||
if not parent_cs: | ||||
parent_cs = EmptyChangeset(alias=scm_instance.alias) | ||||
if isinstance(parent_cs, EmptyChangeset): | ||||
# EmptyChangeset means we we're editing empty repository | ||||
parents = None | ||||
else: | ||||
parents = [parent_cs] | ||||
# add multiple nodes | ||||
for path, content in processed_nodes: | ||||
imc.add(FileNode(path, content=content)) | ||||
tip = imc.commit(message=message, | ||||
author=author, | ||||
parents=parents, | ||||
branch=parent_cs.branch) | ||||
if trigger_push_hook: | ||||
self._handle_push(scm_instance, | ||||
username=user.username, | ||||
Mads Kiilerich
|
r7632 | ip_addr=ip_addr, | ||
Bradley M. Kuhn
|
r4187 | action='push_local', | ||
repo_name=repo.repo_name, | ||||
revisions=[tip.raw_id]) | ||||
Mads Kiilerich
|
r7635 | else: | ||
self.mark_for_invalidation(repo.repo_name) | ||||
Bradley M. Kuhn
|
r4187 | return tip | ||
Mads Kiilerich
|
r7632 | def update_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None, | ||
Bradley M. Kuhn
|
r4187 | author=None, trigger_push_hook=True): | ||
Mads Kiilerich
|
r5415 | """ | ||
Commits specified nodes to repo. Again. | ||||
""" | ||||
Søren Løvborg
|
r6423 | user = User.guess_instance(user) | ||
Bradley M. Kuhn
|
r4187 | scm_instance = repo.scm_instance_no_cache() | ||
Mads Kiilerich
|
r8075 | message = message | ||
Thomas De Schampheleire
|
r4919 | committer = user.full_contact | ||
Mads Kiilerich
|
r8075 | if not author: | ||
author = committer | ||||
Bradley M. Kuhn
|
r4187 | |||
imc_class = self._get_IMC_module(scm_instance.alias) | ||||
imc = imc_class(scm_instance) | ||||
if not parent_cs: | ||||
parent_cs = EmptyChangeset(alias=scm_instance.alias) | ||||
if isinstance(parent_cs, EmptyChangeset): | ||||
# EmptyChangeset means we we're editing empty repository | ||||
parents = None | ||||
else: | ||||
parents = [parent_cs] | ||||
# add multiple nodes | ||||
for _filename, data in nodes.items(): | ||||
# new filename, can be renamed from the old one | ||||
filename = self._sanitize_path(data['filename']) | ||||
old_filename = self._sanitize_path(_filename) | ||||
content = data['content'] | ||||
filenode = FileNode(old_filename, content=content) | ||||
op = data['op'] | ||||
if op == 'add': | ||||
imc.add(filenode) | ||||
elif op == 'del': | ||||
imc.remove(filenode) | ||||
elif op == 'mod': | ||||
if filename != old_filename: | ||||
Lars Kruse
|
r6789 | # TODO: handle renames, needs vcs lib changes | ||
Bradley M. Kuhn
|
r4187 | imc.remove(filenode) | ||
imc.add(FileNode(filename, content=content)) | ||||
else: | ||||
imc.change(filenode) | ||||
# commit changes | ||||
tip = imc.commit(message=message, | ||||
author=author, | ||||
parents=parents, | ||||
branch=parent_cs.branch) | ||||
if trigger_push_hook: | ||||
self._handle_push(scm_instance, | ||||
username=user.username, | ||||
Mads Kiilerich
|
r7632 | ip_addr=ip_addr, | ||
Bradley M. Kuhn
|
r4187 | action='push_local', | ||
repo_name=repo.repo_name, | ||||
revisions=[tip.raw_id]) | ||||
Mads Kiilerich
|
r7635 | else: | ||
self.mark_for_invalidation(repo.repo_name) | ||||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7632 | def delete_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None, | ||
Bradley M. Kuhn
|
r4187 | author=None, trigger_push_hook=True): | ||
""" | ||||
Na'Tosha Bard
|
r4526 | Deletes specified nodes from repo. | ||
Bradley M. Kuhn
|
r4187 | |||
Thomas De Schampheleire
|
r4919 | :param user: Kallithea User object or user_id, the committer | ||
Bradley M. Kuhn
|
r4212 | :param repo: Kallithea Repository object | ||
Bradley M. Kuhn
|
r4187 | :param message: commit message | ||
:param nodes: mapping {filename:{'content':content},...} | ||||
:param parent_cs: parent changeset, can be empty than it's initial commit | ||||
Thomas De Schampheleire
|
r4919 | :param author: author of commit, cna be different that committer only for git | ||
Bradley M. Kuhn
|
r4187 | :param trigger_push_hook: trigger push hooks | ||
Thomas De Schampheleire
|
r4919 | :returns: new committed changeset after deletion | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Søren Løvborg
|
r6423 | user = User.guess_instance(user) | ||
Bradley M. Kuhn
|
r4187 | scm_instance = repo.scm_instance_no_cache() | ||
processed_nodes = [] | ||||
for f_path in nodes: | ||||
f_path = self._sanitize_path(f_path) | ||||
Na'Tosha Bard
|
r4525 | # content can be empty but for compatibility it allows same dicts | ||
Bradley M. Kuhn
|
r4187 | # structure as add_nodes | ||
content = nodes[f_path].get('content') | ||||
processed_nodes.append((f_path, content)) | ||||
Mads Kiilerich
|
r8075 | message = message | ||
Thomas De Schampheleire
|
r4919 | committer = user.full_contact | ||
Mads Kiilerich
|
r8075 | if not author: | ||
author = committer | ||||
Bradley M. Kuhn
|
r4187 | |||
IMC = self._get_IMC_module(scm_instance.alias) | ||||
imc = IMC(scm_instance) | ||||
if not parent_cs: | ||||
parent_cs = EmptyChangeset(alias=scm_instance.alias) | ||||
if isinstance(parent_cs, EmptyChangeset): | ||||
# EmptyChangeset means we we're editing empty repository | ||||
parents = None | ||||
else: | ||||
parents = [parent_cs] | ||||
# add multiple nodes | ||||
for path, content in processed_nodes: | ||||
imc.remove(FileNode(path, content=content)) | ||||
tip = imc.commit(message=message, | ||||
author=author, | ||||
parents=parents, | ||||
branch=parent_cs.branch) | ||||
if trigger_push_hook: | ||||
self._handle_push(scm_instance, | ||||
username=user.username, | ||||
Mads Kiilerich
|
r7632 | ip_addr=ip_addr, | ||
Bradley M. Kuhn
|
r4187 | action='push_local', | ||
repo_name=repo.repo_name, | ||||
revisions=[tip.raw_id]) | ||||
Mads Kiilerich
|
r7635 | else: | ||
self.mark_for_invalidation(repo.repo_name) | ||||
Bradley M. Kuhn
|
r4187 | return tip | ||
def get_unread_journal(self): | ||||
Søren Løvborg
|
r6483 | return UserLog.query().count() | ||
Bradley M. Kuhn
|
r4187 | |||
def get_repo_landing_revs(self, repo=None): | ||||
""" | ||||
Generates select option with tags branches and bookmarks (for hg only) | ||||
grouped by type | ||||
:param repo: | ||||
""" | ||||
hist_l = [] | ||||
choices = [] | ||||
repo = self.__get_repo(repo) | ||||
Mads Kiilerich
|
r7740 | hist_l.append(('rev:tip', _('latest tip'))) | ||
Bradley M. Kuhn
|
r4187 | choices.append('rev:tip') | ||
Mads Kiilerich
|
r5306 | if repo is None: | ||
Bradley M. Kuhn
|
r4187 | return choices, hist_l | ||
repo = repo.scm_instance | ||||
branches_group = ([(u'branch:%s' % k, k) for k, v in | ||||
Mads Kiilerich
|
r8059 | repo.branches.items()], _("Branches")) | ||
Bradley M. Kuhn
|
r4187 | hist_l.append(branches_group) | ||
choices.extend([x[0] for x in branches_group[0]]) | ||||
if repo.alias == 'hg': | ||||
bookmarks_group = ([(u'book:%s' % k, k) for k, v in | ||||
Mads Kiilerich
|
r8059 | repo.bookmarks.items()], _("Bookmarks")) | ||
Bradley M. Kuhn
|
r4187 | hist_l.append(bookmarks_group) | ||
choices.extend([x[0] for x in bookmarks_group[0]]) | ||||
tags_group = ([(u'tag:%s' % k, k) for k, v in | ||||
Mads Kiilerich
|
r8059 | repo.tags.items()], _("Tags")) | ||
Bradley M. Kuhn
|
r4187 | hist_l.append(tags_group) | ||
choices.extend([x[0] for x in tags_group[0]]) | ||||
return choices, hist_l | ||||
Thomas De Schampheleire
|
r7536 | def _get_git_hook_interpreter(self): | ||
"""Return a suitable interpreter for Git hooks. | ||||
Return a suitable string to be written in the POSIX #! shebang line for | ||||
Git hook scripts so they invoke Kallithea code with the right Python | ||||
interpreter and in the right environment. | ||||
""" | ||||
Thomas De Schampheleire
|
r7538 | # Note: sys.executable might not point at a usable Python interpreter. For | ||
# example, when using uwsgi, it will point at the uwsgi program itself. | ||||
Thomas De Schampheleire
|
r7536 | # FIXME This may not work on Windows and may need a shell wrapper script. | ||
Thomas De Schampheleire
|
r7538 | return (kallithea.CONFIG.get('git_hook_interpreter') | ||
or sys.executable | ||||
Mads Kiilerich
|
r8053 | or '/usr/bin/env python3') | ||
Thomas De Schampheleire
|
r7536 | |||
Branko Majic
|
r5452 | def install_git_hooks(self, repo, force_create=False): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Bradley M. Kuhn
|
r4212 | Creates a kallithea hook inside a git repository | ||
Bradley M. Kuhn
|
r4187 | |||
:param repo: Instance of VCS repo | ||||
:param force_create: Create even if same name hook exists | ||||
""" | ||||
domruf
|
r5960 | loc = os.path.join(repo.path, 'hooks') | ||
Bradley M. Kuhn
|
r4187 | if not repo.bare: | ||
domruf
|
r5960 | loc = os.path.join(repo.path, '.git', 'hooks') | ||
Bradley M. Kuhn
|
r4187 | if not os.path.isdir(loc): | ||
os.makedirs(loc) | ||||
Mads Kiilerich
|
r7992 | tmpl_post = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter()) | ||
domruf
|
r5927 | tmpl_post += pkg_resources.resource_string( | ||
domruf
|
r5960 | 'kallithea', os.path.join('config', 'post_receive_tmpl.py') | ||
Bradley M. Kuhn
|
r4187 | ) | ||
Mads Kiilerich
|
r7992 | tmpl_pre = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter()) | ||
domruf
|
r5927 | tmpl_pre += pkg_resources.resource_string( | ||
domruf
|
r5960 | 'kallithea', os.path.join('config', 'pre_receive_tmpl.py') | ||
Bradley M. Kuhn
|
r4187 | ) | ||
for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]: | ||||
domruf
|
r5960 | _hook_file = os.path.join(loc, '%s-receive' % h_type) | ||
Bradley M. Kuhn
|
r4205 | has_hook = False | ||
Mads Kiilerich
|
r5375 | log.debug('Installing git hook in repo %s', repo) | ||
Bradley M. Kuhn
|
r4187 | if os.path.exists(_hook_file): | ||
Bradley M. Kuhn
|
r4212 | # let's take a look at this hook, maybe it's kallithea ? | ||
log.debug('hook exists, checking if it is from kallithea') | ||||
Bradley M. Kuhn
|
r4187 | with open(_hook_file, 'rb') as f: | ||
data = f.read() | ||||
Mads Kiilerich
|
r7952 | matches = re.search(br'^KALLITHEA_HOOK_VER\s*=\s*(.*)$', data, flags=re.MULTILINE) | ||
Bradley M. Kuhn
|
r4187 | if matches: | ||
try: | ||||
ver = matches.groups()[0] | ||||
Mads Kiilerich
|
r7952 | log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver) | ||
Bradley M. Kuhn
|
r4205 | has_hook = True | ||
Bradley M. Kuhn
|
r4187 | except Exception: | ||
log.error(traceback.format_exc()) | ||||
else: | ||||
# there is no hook in this dir, so we want to create one | ||||
Bradley M. Kuhn
|
r4205 | has_hook = True | ||
Bradley M. Kuhn
|
r4187 | |||
Bradley M. Kuhn
|
r4205 | if has_hook or force_create: | ||
Mads Kiilerich
|
r5375 | log.debug('writing %s hook file !', h_type) | ||
Bradley M. Kuhn
|
r4187 | try: | ||
with open(_hook_file, 'wb') as f: | ||||
Mads Kiilerich
|
r7992 | tmpl = tmpl.replace(b'_TMPL_', safe_bytes(kallithea.__version__)) | ||
Bradley M. Kuhn
|
r4187 | f.write(tmpl) | ||
Mads Kiilerich
|
r7890 | os.chmod(_hook_file, 0o755) | ||
Mads Kiilerich
|
r5374 | except IOError as e: | ||
Mads Kiilerich
|
r5375 | log.error('error writing %s: %s', _hook_file, e) | ||
Bradley M. Kuhn
|
r4187 | else: | ||
log.debug('skipping writing hook file') | ||||
Mads Kiilerich
|
r5289 | |||
Lars Kruse
|
r6789 | |||
Søren Løvborg
|
r6472 | def AvailableRepoGroupChoices(top_perms, repo_group_perm_level, extras=()): | ||
Mads Kiilerich
|
r5289 | """Return group_id,string tuples with choices for all the repo groups where | ||
the user has the necessary permissions. | ||||
Top level is -1. | ||||
""" | ||||
groups = RepoGroup.query().all() | ||||
Søren Løvborg
|
r6026 | if HasPermissionAny('hg.admin')('available repo groups'): | ||
Mads Kiilerich
|
r5289 | groups.append(None) | ||
else: | ||||
Søren Løvborg
|
r6472 | groups = list(RepoGroupList(groups, perm_level=repo_group_perm_level)) | ||
Mads Kiilerich
|
r5289 | if top_perms and HasPermissionAny(*top_perms)('available repo groups'): | ||
groups.append(None) | ||||
for extra in extras: | ||||
if not any(rg == extra for rg in groups): | ||||
groups.append(extra) | ||||
Mads Kiilerich
|
r5293 | return RepoGroup.groups_choices(groups=groups) | ||