diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -219,10 +219,31 @@ def includeme(config): name='branches_home', pattern='/{repo_name:.*?[^/]}/branches', repo_route=True) + # Bookmarks config.add_route( name='bookmarks_home', pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True) + # Forks + config.add_route( + name='repo_fork_new', + pattern='/{repo_name:.*?[^/]}/fork', repo_route=True, + repo_accepted_types=['hg', 'git']) + + config.add_route( + name='repo_fork_create', + pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True, + repo_accepted_types=['hg', 'git']) + + config.add_route( + name='repo_forks_show_all', + pattern='/{repo_name:.*?[^/]}/forks', repo_route=True, + repo_accepted_types=['hg', 'git']) + config.add_route( + name='repo_forks_data', + pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True, + repo_accepted_types=['hg', 'git']) + # Pull Requests config.add_route( name='pullrequest_show', diff --git a/rhodecode/apps/repository/tests/test_repo_forks.py b/rhodecode/apps/repository/tests/test_repo_forks.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_repo_forks.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 . +# +# 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 pytest + +from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK + +from rhodecode.tests.fixture import Fixture +from rhodecode.lib import helpers as h + +from rhodecode.model.db import Repository +from rhodecode.model.repo import RepoModel +from rhodecode.model.user import UserModel +from rhodecode.model.meta import Session + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_summary': '/{repo_name}', + 'repo_creating_check': '/{repo_name}/repo_creating_check', + 'repo_fork_new': '/{repo_name}/fork', + 'repo_fork_create': '/{repo_name}/fork/create', + 'repo_forks_show_all': '/{repo_name}/forks', + 'repo_forks_data': '/{repo_name}/forks/data', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +FORK_NAME = { + 'hg': HG_FORK, + 'git': GIT_FORK +} + + +@pytest.mark.skip_backends('svn') +class TestRepoForkViewTests(TestController): + + def test_show_forks(self, backend, xhr_header): + self.log_user() + response = self.app.get( + route_path('repo_forks_data', repo_name=backend.repo_name), + extra_environ=xhr_header) + + assert response.json == {u'data': [], u'draw': None, + u'recordsFiltered': 0, u'recordsTotal': 0} + + def test_no_permissions_to_fork_page(self, backend, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') + + user_model = UserModel() + user_model.revoke_perm(user_id, 'hg.fork.repository') + user_model.grant_perm(user_id, 'hg.fork.none') + u = UserModel().get(user_id) + u.inherit_default_permissions = False + Session().commit() + # try create a fork + self.app.get( + route_path('repo_fork_new', repo_name=backend.repo_name), + status=404) + + def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') + + user_model = UserModel() + user_model.revoke_perm(user_id, 'hg.fork.repository') + user_model.grant_perm(user_id, 'hg.fork.none') + u = UserModel().get(user_id) + u.inherit_default_permissions = False + Session().commit() + # try create a fork + self.app.post( + route_path('repo_fork_create', repo_name=backend.repo_name), + {'csrf_token': csrf_token}, + status=404) + + def test_fork_missing_data(self, autologin_user, backend, csrf_token): + # try create a fork + response = self.app.post( + route_path('repo_fork_create', repo_name=backend.repo_name), + {'csrf_token': csrf_token}, + status=200) + # test if html fill works fine + response.mustcontain('Missing value') + + def test_create_fork_page(self, autologin_user, backend): + self.app.get( + route_path('repo_fork_new', repo_name=backend.repo_name), + status=200) + + def test_create_and_show_fork( + self, autologin_user, backend, csrf_token, xhr_header): + + # create a fork + fork_name = FORK_NAME[backend.alias] + description = 'fork of vcs test' + repo_name = backend.repo_name + source_repo = Repository.get_by_repo_name(repo_name) + creation_args = { + 'repo_name': fork_name, + 'repo_group': '', + 'fork_parent_id': source_repo.repo_id, + 'repo_type': backend.alias, + 'description': description, + 'private': 'False', + 'landing_rev': 'rev:tip', + 'csrf_token': csrf_token, + } + + self.app.post( + route_path('repo_fork_create', repo_name=repo_name), creation_args) + + response = self.app.get( + route_path('repo_forks_data', repo_name=repo_name), + extra_environ=xhr_header) + + assert response.json['data'][0]['fork_name'] == \ + """%s""" % (fork_name, fork_name) + + # remove this fork + fixture.destroy_repo(fork_name) + + def test_fork_create(self, autologin_user, backend, csrf_token): + fork_name = FORK_NAME[backend.alias] + description = 'fork of vcs test' + repo_name = backend.repo_name + source_repo = Repository.get_by_repo_name(repo_name) + creation_args = { + 'repo_name': fork_name, + 'repo_group': '', + 'fork_parent_id': source_repo.repo_id, + 'repo_type': backend.alias, + 'description': description, + 'private': 'False', + 'landing_rev': 'rev:tip', + 'csrf_token': csrf_token, + } + self.app.post( + route_path('repo_fork_create', repo_name=repo_name), creation_args) + repo = Repository.get_by_repo_name(FORK_NAME[backend.alias]) + assert repo.fork.repo_name == backend.repo_name + + # run the check page that triggers the flash message + response = self.app.get( + route_path('repo_creating_check', repo_name=fork_name)) + # test if we have a message that fork is ok + assert_session_flash(response, + 'Forked repository %s as %s' + % (repo_name, fork_name, fork_name)) + + # test if the fork was created in the database + fork_repo = Session().query(Repository)\ + .filter(Repository.repo_name == fork_name).one() + + assert fork_repo.repo_name == fork_name + assert fork_repo.fork.repo_name == repo_name + + # test if the repository is visible in the list ? + response = self.app.get( + h.route_path('repo_summary', repo_name=fork_name)) + response.mustcontain(fork_name) + response.mustcontain(backend.alias) + response.mustcontain('Fork of') + response.mustcontain('%s' % (repo_name, repo_name)) + + def test_fork_create_into_group(self, autologin_user, backend, csrf_token): + group = fixture.create_repo_group('vc') + group_id = group.group_id + fork_name = FORK_NAME[backend.alias] + fork_name_full = 'vc/%s' % fork_name + description = 'fork of vcs test' + repo_name = backend.repo_name + source_repo = Repository.get_by_repo_name(repo_name) + creation_args = { + 'repo_name': fork_name, + 'repo_group': group_id, + 'fork_parent_id': source_repo.repo_id, + 'repo_type': backend.alias, + 'description': description, + 'private': 'False', + 'landing_rev': 'rev:tip', + 'csrf_token': csrf_token, + } + self.app.post( + route_path('repo_fork_create', repo_name=repo_name), creation_args) + repo = Repository.get_by_repo_name(fork_name_full) + assert repo.fork.repo_name == backend.repo_name + + # run the check page that triggers the flash message + response = self.app.get( + route_path('repo_creating_check', repo_name=fork_name_full)) + # test if we have a message that fork is ok + assert_session_flash(response, + 'Forked repository %s as %s' + % (repo_name, fork_name_full, fork_name_full)) + + # test if the fork was created in the database + fork_repo = Session().query(Repository)\ + .filter(Repository.repo_name == fork_name_full).one() + + assert fork_repo.repo_name == fork_name_full + assert fork_repo.fork.repo_name == repo_name + + # test if the repository is visible in the list ? + response = self.app.get( + h.route_path('repo_summary', repo_name=fork_name_full)) + response.mustcontain(fork_name_full) + response.mustcontain(backend.alias) + + response.mustcontain('Fork of') + response.mustcontain('%s' % (repo_name, repo_name)) + + fixture.destroy_repo(fork_name_full) + fixture.destroy_repo_group(group_id) + + def test_fork_read_permission(self, backend, xhr_header, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') + + # create a fake fork + fork = user_util.create_repo(repo_type=backend.alias) + source = user_util.create_repo(repo_type=backend.alias) + repo_name = source.repo_name + + fork.fork_id = source.repo_id + fork_name = fork.repo_name + Session().commit() + + forks = Repository.query()\ + .filter(Repository.repo_type == backend.alias)\ + .filter(Repository.fork_id == source.repo_id).all() + assert 1 == len(forks) + + # set read permissions for this + RepoModel().grant_user_permission( + repo=forks[0], user=user_id, perm='repository.read') + Session().commit() + + response = self.app.get( + route_path('repo_forks_data', repo_name=repo_name), + extra_environ=xhr_header) + + assert response.json['data'][0]['fork_name'] == \ + """%s""" % (fork_name, fork_name) + + def test_fork_none_permission(self, backend, xhr_header, user_util): + user = user_util.create_user(password='qweqwe') + user_id = user.user_id + self.log_user(user.username, 'qweqwe') + + # create a fake fork + fork = user_util.create_repo(repo_type=backend.alias) + source = user_util.create_repo(repo_type=backend.alias) + repo_name = source.repo_name + + fork.fork_id = source.repo_id + + Session().commit() + + forks = Repository.query()\ + .filter(Repository.repo_type == backend.alias)\ + .filter(Repository.fork_id == source.repo_id).all() + assert 1 == len(forks) + + # set none + RepoModel().grant_user_permission( + repo=forks[0], user=user_id, perm='repository.none') + Session().commit() + + # fork shouldn't be there + response = self.app.get( + route_path('repo_forks_data', repo_name=repo_name), + extra_environ=xhr_header) + + assert response.json == {u'data': [], u'draw': None, + u'recordsFiltered': 0, u'recordsTotal': 0} + + +class TestSVNFork(TestController): + @pytest.mark.parametrize('route_name', [ + 'repo_fork_create', 'repo_fork_new' + ]) + def test_fork_redirects(self, autologin_user, backend_svn, route_name): + + self.app.get(route_path( + route_name, repo_name=backend_svn.repo_name), + status=404) diff --git a/rhodecode/apps/repository/views/repo_forks.py b/rhodecode/apps/repository/views/repo_forks.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_forks.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 . +# +# 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 logging +import datetime +import formencode +from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound +from pyramid.view import view_config +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.apps._base import RepoAppView, DataGridAppView + +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, + HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired) +import rhodecode.lib.helpers as h +from rhodecode.model.db import ( + coalesce, or_, Repository, RepoGroup, UserFollowing, User) +from rhodecode.model.repo import RepoModel +from rhodecode.model.forms import RepoForkForm +from rhodecode.model.scm import ScmModel, RepoGroupList +from rhodecode.lib.utils2 import safe_int, safe_unicode + +log = logging.getLogger(__name__) + + +class RepoForksView(RepoAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context(include_app_defaults=True) + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + c.rhodecode_repo = self.rhodecode_vcs_repo + + acl_groups = RepoGroupList( + RepoGroup.query().all(), + perm_set=['group.write', 'group.admin']) + c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) + c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups) + choices, c.landing_revs = ScmModel().get_repo_landing_revs() + c.landing_revs_choices = choices + c.personal_repo_group = c.rhodecode_user.personal_repo_group + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_forks_show_all', request_method='GET', + renderer='rhodecode:templates/forks/forks.mako') + def repo_forks_show_all(self): + c = self.load_default_context() + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_forks_data', request_method='GET', + renderer='json_ext', xhr=True) + def repo_forks_data(self): + _ = self.request.translate + column_map = { + 'fork_name': 'repo_name', + 'fork_date': 'created_on', + 'last_activity': 'updated_on' + } + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) + + acl_check = HasRepoPermissionAny( + 'repository.read', 'repository.write', 'repository.admin') + repo_id = self.db_repo.repo_id + allowed_ids = [] + for f in Repository.query().filter(Repository.fork_id == repo_id): + if acl_check(f.repo_name, 'get forks check'): + allowed_ids.append(f.repo_id) + + forks_data_total_count = Repository.query()\ + .filter(Repository.fork_id == repo_id)\ + .filter(Repository.repo_id.in_(allowed_ids))\ + .count() + + # json generate + base_q = Repository.query()\ + .filter(Repository.fork_id == repo_id)\ + .filter(Repository.repo_id.in_(allowed_ids))\ + + if search_q: + like_expression = u'%{}%'.format(safe_unicode(search_q)) + base_q = base_q.filter(or_( + Repository.repo_name.ilike(like_expression), + Repository.description.ilike(like_expression), + )) + + forks_data_total_filtered_count = base_q.count() + + sort_col = getattr(Repository, order_by, None) + if sort_col: + if order_dir == 'asc': + # handle null values properly to order by NULL last + if order_by in ['last_activity']: + sort_col = coalesce(sort_col, datetime.date.max) + sort_col = sort_col.asc() + else: + # handle null values properly to order by NULL last + if order_by in ['last_activity']: + sort_col = coalesce(sort_col, datetime.date.min) + sort_col = sort_col.desc() + + base_q = base_q.order_by(sort_col) + base_q = base_q.offset(start).limit(limit) + + fork_list = base_q.all() + + def fork_actions(fork): + url_link = h.route_path( + 'repo_compare', + repo_name=fork.repo_name, + source_ref_type=self.db_repo.landing_rev[0], + source_ref=self.db_repo.landing_rev[1], + target_ref_type=self.db_repo.landing_rev[0], + target_ref=self.db_repo.landing_rev[1], + _query=dict(merge=1, target_repo=f.repo_name)) + return h.link_to(_('Compare fork'), url_link, class_='btn-link') + + def fork_name(fork): + return h.link_to(fork.repo_name, + h.route_path('repo_summary', repo_name=fork.repo_name)) + + forks_data = [] + for fork in fork_list: + forks_data.append({ + "username": h.gravatar_with_user(self.request, fork.user.username), + "fork_name": fork_name(fork), + "description": fork.description, + "fork_date": h.age_component(fork.created_on, time_is_local=True), + "last_activity": h.format_date(fork.updated_on), + "action": fork_actions(fork), + }) + + data = ({ + 'draw': draw, + 'data': forks_data, + 'recordsTotal': forks_data_total_count, + 'recordsFiltered': forks_data_total_filtered_count, + }) + + return data + + @LoginRequired() + @NotAnonymous() + @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_fork_new', request_method='GET', + renderer='rhodecode:templates/forks/forks.mako') + def repo_fork_new(self): + c = self.load_default_context() + + defaults = RepoModel()._get_defaults(self.db_repo_name) + # alter the description to indicate a fork + defaults['description'] = ( + 'fork of repository: %s \n%s' % ( + defaults['repo_name'], defaults['description'])) + # add suffix to fork + defaults['repo_name'] = '%s-fork' % defaults['repo_name'] + + data = render('rhodecode:templates/forks/fork.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=defaults, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + + @LoginRequired() + @NotAnonymous() + @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @CSRFRequired() + @view_config( + route_name='repo_fork_create', request_method='POST', + renderer='rhodecode:templates/forks/fork.mako') + def repo_fork_create(self): + _ = self.request.translate + c = self.load_default_context() + + _form = RepoForkForm(old_data={'repo_type': self.db_repo.repo_type}, + repo_groups=c.repo_groups_choices, + landing_revs=c.landing_revs_choices)() + form_result = {} + task_id = None + try: + form_result = _form.to_python(dict(self.request.POST)) + # create fork is done sometimes async on celery, db transaction + # management is handled there. + task = RepoModel().create_fork( + form_result, c.rhodecode_user.user_id) + from celery.result import BaseAsyncResult + if isinstance(task, BaseAsyncResult): + task_id = task.task_id + except formencode.Invalid as errors: + c.repo_info = self.db_repo + + data = render('rhodecode:templates/forks/fork.mako', + self._get_template_context(c), self.request) + html = formencode.htmlfill.render( + data, + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8", + force_defaults=False + ) + return Response(html) + except Exception: + log.exception( + u'Exception while trying to fork the repository %s', + self.db_repo_name) + msg = ( + _('An error occurred during repository forking %s') % ( + self.db_repo_name, )) + h.flash(msg, category='error') + + repo_name = form_result.get('repo_name_full', self.db_repo_name) + raise HTTPFound( + h.route_path('repo_creating', + repo_name=repo_name, + _query=dict(task_id=task_id))) diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -489,20 +489,4 @@ def make_map(config): conditions={'method': ['GET', 'POST'], 'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('repo_fork_create_home', '/{repo_name}/fork', - controller='forks', action='fork_create', - conditions={'function': check_repo, 'method': ['POST']}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('repo_fork_home', '/{repo_name}/fork', - controller='forks', action='fork', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('repo_forks_home', '/{repo_name}/forks', - controller='forks', action='forks', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - return rmap diff --git a/rhodecode/controllers/forks.py b/rhodecode/controllers/forks.py deleted file mode 100644 --- a/rhodecode/controllers/forks.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2011-2017 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 . -# -# 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/ - -""" -forks controller for rhodecode -""" - -import formencode -import logging -from formencode import htmlfill - -from pylons import tmpl_context as c, request, url -from pylons.controllers.util import redirect -from pylons.i18n.translation import _ - -from pyramid.httpexceptions import HTTPFound -import rhodecode.lib.helpers as h - -from rhodecode.lib import auth -from rhodecode.lib.helpers import Page -from rhodecode.lib.auth import ( - LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, - HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType) -from rhodecode.lib.base import BaseRepoController, render -from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User -from rhodecode.model.repo import RepoModel -from rhodecode.model.forms import RepoForkForm -from rhodecode.model.scm import ScmModel, RepoGroupList -from rhodecode.lib.utils2 import safe_int - -log = logging.getLogger(__name__) - - -class ForksController(BaseRepoController): - - def __before__(self): - super(ForksController, self).__before__() - - def __load_defaults(self): - acl_groups = RepoGroupList( - RepoGroup.query().all(), - perm_set=['group.write', 'group.admin']) - c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) - c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) - choices, c.landing_revs = ScmModel().get_repo_landing_revs() - c.landing_revs_choices = choices - c.personal_repo_group = c.rhodecode_user.personal_repo_group - - def __load_data(self, repo_name=None): - """ - Load defaults settings for edit, and update - - :param repo_name: - """ - self.__load_defaults() - - c.repo_info = Repository.get_by_repo_name(repo_name) - repo = c.repo_info.scm_instance() - - if c.repo_info is None: - h.not_mapped_error(repo_name) - return redirect(url('repos')) - - c.default_user_id = User.get_default_user().user_id - c.in_public_journal = UserFollowing.query()\ - .filter(UserFollowing.user_id == c.default_user_id)\ - .filter(UserFollowing.follows_repository == c.repo_info).scalar() - - if c.repo_info.stats: - last_rev = c.repo_info.stats.stat_on_revision+1 - else: - last_rev = 0 - c.stats_revision = last_rev - - c.repo_last_rev = repo.count() - - if last_rev == 0 or c.repo_last_rev == 0: - c.stats_percentage = 0 - else: - c.stats_percentage = '%.2f' % ((float((last_rev)) / - c.repo_last_rev) * 100) - - defaults = RepoModel()._get_defaults(repo_name) - # alter the description to indicate a fork - defaults['description'] = ('fork of repository: %s \n%s' - % (defaults['repo_name'], - defaults['description'])) - # add suffix to fork - defaults['repo_name'] = '%s-fork' % defaults['repo_name'] - - return defaults - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @HasAcceptedRepoType('git', 'hg') - def forks(self, repo_name): - p = safe_int(request.GET.get('page', 1), 1) - repo_id = c.rhodecode_db_repo.repo_id - d = [] - for r in Repository.get_repo_forks(repo_id): - if not HasRepoPermissionAny( - 'repository.read', 'repository.write', 'repository.admin' - )(r.repo_name, 'get forks check'): - continue - d.append(r) - c.forks_pager = Page(d, page=p, items_per_page=20) - - c.forks_data = render('/forks/forks_data.mako') - - if request.environ.get('HTTP_X_PJAX'): - return c.forks_data - - return render('/forks/forks.mako') - - @LoginRequired() - @NotAnonymous() - @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @HasAcceptedRepoType('git', 'hg') - def fork(self, repo_name): - c.repo_info = Repository.get_by_repo_name(repo_name) - if not c.repo_info: - h.not_mapped_error(repo_name) - return redirect(h.route_path('home')) - - defaults = self.__load_data(repo_name) - - return htmlfill.render( - render('forks/fork.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False - ) - - @LoginRequired() - @NotAnonymous() - @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @HasAcceptedRepoType('git', 'hg') - @auth.CSRFRequired() - def fork_create(self, repo_name): - self.__load_defaults() - c.repo_info = Repository.get_by_repo_name(repo_name) - _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type}, - repo_groups=c.repo_groups_choices, - landing_revs=c.landing_revs_choices)() - form_result = {} - task_id = None - try: - form_result = _form.to_python(dict(request.POST)) - # create fork is done sometimes async on celery, db transaction - # management is handled there. - task = RepoModel().create_fork( - form_result, c.rhodecode_user.user_id) - from celery.result import BaseAsyncResult - if isinstance(task, BaseAsyncResult): - task_id = task.task_id - except formencode.Invalid as errors: - c.new_repo = errors.value['repo_name'] - return htmlfill.render( - render('forks/fork.mako'), - defaults=errors.value, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - except Exception: - log.exception( - u'Exception while trying to fork the repository %s', repo_name) - msg = ( - _('An error occurred during repository forking %s') % - (repo_name, )) - h.flash(msg, category='error') - - raise HTTPFound( - h.route_path('repo_creating', - repo_name=form_result['repo_name_full'], - _query=dict(task_id=task_id))) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1340,39 +1340,6 @@ class XHRRequired(object): return func(*fargs, **fkwargs) -class HasAcceptedRepoType(object): - """ - Check if requested repo is within given repo type aliases - """ - - # TODO(marcink): remove this in favor of the predicates in pyramid routes - - def __init__(self, *repo_type_list): - self.repo_type_list = set(repo_type_list) - - def __call__(self, func): - return get_cython_compat_decorator(self.__wrapper, func) - - def __wrapper(self, func, *fargs, **fkwargs): - import rhodecode.lib.helpers as h - cls = fargs[0] - rhodecode_repo = cls.rhodecode_repo - - log.debug('%s checking repo type for %s in %s', - self.__class__.__name__, - rhodecode_repo.alias, self.repo_type_list) - - if rhodecode_repo.alias in self.repo_type_list: - return func(*fargs, **fkwargs) - else: - h.flash(h.literal( - _('Action not supported for %s.' % rhodecode_repo.alias)), - category='warning') - raise HTTPFound( - h.route_path('repo_summary', - repo_name=cls.rhodecode_db_repo.repo_name)) - - class PermsDecorator(object): """ Base class for controller decorators, we extract the current user from diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -145,6 +145,10 @@ function registerRCRoutes() { pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']); pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']); pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']); + pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']); + pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']); + pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']); + pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']); pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']); diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -275,7 +275,7 @@ %endif %if c.rhodecode_user.username != h.DEFAULT_USER: %if c.rhodecode_db_repo.repo_type in ['git','hg']: -
  • ${_('Fork')}
  • +
  • ${_('Fork')}
  • ${_('Create Pull Request')}
  • %endif %endif diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -24,7 +24,7 @@
  • - + ${_('Fork')}
  • diff --git a/rhodecode/templates/forks/fork.mako b/rhodecode/templates/forks/fork.mako --- a/rhodecode/templates/forks/fork.mako +++ b/rhodecode/templates/forks/fork.mako @@ -27,7 +27,7 @@ ${self.breadcrumbs()} - ${h.secure_form(h.url('repo_fork_create_home',repo_name=c.repo_info.repo_name))} + ${h.secure_form(h.route_path('repo_fork_create',repo_name=c.repo_info.repo_name), method='POST', request=request)}
    diff --git a/rhodecode/templates/forks/forks.mako b/rhodecode/templates/forks/forks.mako --- a/rhodecode/templates/forks/forks.mako +++ b/rhodecode/templates/forks/forks.mako @@ -26,20 +26,94 @@ ${self.repo_page_title(c.rhodecode_db_repo)}
    -
    -
    - ${c.forks_data} -
    + +
    +
    + + + + diff --git a/rhodecode/templates/forks/forks_data.mako b/rhodecode/templates/forks/forks_data.mako deleted file mode 100644 --- a/rhodecode/templates/forks/forks_data.mako +++ /dev/null @@ -1,56 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace name="base" file="/base/base.mako"/> - -% if c.forks_pager: - - - - - - - - - % for f in c.forks_pager: - - - - - - - - % endfor -
    ${_('Owner')}${_('Fork')}${_('Description')}${_('Forked')}
    - ${base.gravatar_with_user(f.user.email, 16)} - - ${h.link_to(f.repo_name,h.route_path('repo_summary',repo_name=f.repo_name))} - -
    ${f.description}
    -
    - ${h.age_component(f.created_on, time_is_local=True)} - - - ${_('Compare fork')} - -
    -
    - - ${c.forks_pager.pager('$link_previous ~2~ $link_next')} -
    -% else: - ${_('There are no forks yet')} -% endif diff --git a/rhodecode/templates/summary/components.mako b/rhodecode/templates/summary/components.mako --- a/rhodecode/templates/summary/components.mako +++ b/rhodecode/templates/summary/components.mako @@ -108,7 +108,7 @@ % endif ## forks - + ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}, ## repo size diff --git a/rhodecode/tests/functional/test_forks.py b/rhodecode/tests/functional/test_forks.py deleted file mode 100644 --- a/rhodecode/tests/functional/test_forks.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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 . -# -# 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 pytest - -from rhodecode.tests import * -from rhodecode.tests.fixture import Fixture -from rhodecode.lib import helpers as h - -from rhodecode.model.db import Repository -from rhodecode.model.repo import RepoModel -from rhodecode.model.user import UserModel -from rhodecode.model.meta import Session - -fixture = Fixture() - - -def route_path(name, params=None, **kwargs): - import urllib - - base_url = { - 'repo_summary': '/{repo_name}', - 'repo_creating_check': '/{repo_name}/repo_creating_check', - }[name].format(**kwargs) - - if params: - base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) - return base_url - - -class _BaseTest(TestController): - - REPO = None - REPO_TYPE = None - NEW_REPO = None - REPO_FORK = None - - @pytest.fixture(autouse=True) - def prepare(self, request, pylonsapp): - self.username = u'forkuser' - self.password = u'qweqwe' - self.u1 = fixture.create_user(self.username, password=self.password, - email=u'fork_king@rhodecode.org') - Session().commit() - self.u1id = self.u1.user_id - request.addfinalizer(self.cleanup) - - def cleanup(self): - u1 = UserModel().get(self.u1id) - Session().delete(u1) - Session().commit() - - def test_index(self): - self.log_user() - repo_name = self.REPO - response = self.app.get(url(controller='forks', action='forks', - repo_name=repo_name)) - - response.mustcontain("""There are no forks yet""") - - def test_no_permissions_to_fork(self): - usr = self.log_user(TEST_USER_REGULAR_LOGIN, - TEST_USER_REGULAR_PASS)['user_id'] - user_model = UserModel() - user_model.revoke_perm(usr, 'hg.fork.repository') - user_model.grant_perm(usr, 'hg.fork.none') - u = UserModel().get(usr) - u.inherit_default_permissions = False - Session().commit() - # try create a fork - repo_name = self.REPO - self.app.post( - url(controller='forks', action='fork_create', repo_name=repo_name), - {'csrf_token': self.csrf_token}, status=404) - - def test_index_with_fork(self): - self.log_user() - - # create a fork - fork_name = self.REPO_FORK - description = 'fork of vcs test' - repo_name = self.REPO - source_repo = Repository.get_by_repo_name(repo_name) - creation_args = { - 'repo_name': fork_name, - 'repo_group': '', - 'fork_parent_id': source_repo.repo_id, - 'repo_type': self.REPO_TYPE, - 'description': description, - 'private': 'False', - 'landing_rev': 'rev:tip', - 'csrf_token': self.csrf_token, - } - - self.app.post(url(controller='forks', action='fork_create', - repo_name=repo_name), creation_args) - - response = self.app.get(url(controller='forks', action='forks', - repo_name=repo_name)) - - response.mustcontain( - """%s""" % (fork_name, fork_name) - ) - - # remove this fork - fixture.destroy_repo(fork_name) - - def test_fork_create_into_group(self): - self.log_user() - group = fixture.create_repo_group('vc') - group_id = group.group_id - fork_name = self.REPO_FORK - fork_name_full = 'vc/%s' % fork_name - description = 'fork of vcs test' - repo_name = self.REPO - source_repo = Repository.get_by_repo_name(repo_name) - creation_args = { - 'repo_name': fork_name, - 'repo_group': group_id, - 'fork_parent_id': source_repo.repo_id, - 'repo_type': self.REPO_TYPE, - 'description': description, - 'private': 'False', - 'landing_rev': 'rev:tip', - 'csrf_token': self.csrf_token, - } - self.app.post(url(controller='forks', action='fork_create', - repo_name=repo_name), creation_args) - repo = Repository.get_by_repo_name(fork_name_full) - assert repo.fork.repo_name == self.REPO - - # run the check page that triggers the flash message - response = self.app.get( - route_path('repo_creating_check', repo_name=fork_name_full)) - # test if we have a message that fork is ok - assert_session_flash(response, - 'Forked repository %s as %s' - % (repo_name, fork_name_full, fork_name_full)) - - # test if the fork was created in the database - fork_repo = Session().query(Repository)\ - .filter(Repository.repo_name == fork_name_full).one() - - assert fork_repo.repo_name == fork_name_full - assert fork_repo.fork.repo_name == repo_name - - # test if the repository is visible in the list ? - response = self.app.get(h.route_path('repo_summary', repo_name=fork_name_full)) - response.mustcontain(fork_name_full) - response.mustcontain(self.REPO_TYPE) - - response.mustcontain('Fork of') - response.mustcontain('%s' % (repo_name, repo_name)) - - fixture.destroy_repo(fork_name_full) - fixture.destroy_repo_group(group_id) - - def test_z_fork_create(self): - self.log_user() - fork_name = self.REPO_FORK - description = 'fork of vcs test' - repo_name = self.REPO - source_repo = Repository.get_by_repo_name(repo_name) - creation_args = { - 'repo_name': fork_name, - 'repo_group': '', - 'fork_parent_id': source_repo.repo_id, - 'repo_type': self.REPO_TYPE, - 'description': description, - 'private': 'False', - 'landing_rev': 'rev:tip', - 'csrf_token': self.csrf_token, - } - self.app.post(url(controller='forks', action='fork_create', - repo_name=repo_name), creation_args) - repo = Repository.get_by_repo_name(self.REPO_FORK) - assert repo.fork.repo_name == self.REPO - - # run the check page that triggers the flash message - response = self.app.get( - route_path('repo_creating_check', repo_name=fork_name)) - # test if we have a message that fork is ok - assert_session_flash(response, - 'Forked repository %s as %s' - % (repo_name, fork_name, fork_name)) - - # test if the fork was created in the database - fork_repo = Session().query(Repository)\ - .filter(Repository.repo_name == fork_name).one() - - assert fork_repo.repo_name == fork_name - assert fork_repo.fork.repo_name == repo_name - - # test if the repository is visible in the list ? - response = self.app.get(h.route_path('repo_summary', repo_name=fork_name)) - response.mustcontain(fork_name) - response.mustcontain(self.REPO_TYPE) - response.mustcontain('Fork of') - response.mustcontain('%s' % (repo_name, repo_name)) - - def test_zz_fork_permission_page(self): - usr = self.log_user(self.username, self.password)['user_id'] - repo_name = self.REPO - - forks = Repository.query()\ - .filter(Repository.repo_type == self.REPO_TYPE)\ - .filter(Repository.fork_id != None).all() - assert 1 == len(forks) - - # set read permissions for this - RepoModel().grant_user_permission(repo=forks[0], - user=usr, - perm='repository.read') - Session().commit() - - response = self.app.get(url(controller='forks', action='forks', - repo_name=repo_name)) - - response.mustcontain('fork of vcs test') - - def test_zzz_fork_permission_page(self): - usr = self.log_user(self.username, self.password)['user_id'] - repo_name = self.REPO - - forks = Repository.query()\ - .filter(Repository.repo_type == self.REPO_TYPE)\ - .filter(Repository.fork_id != None).all() - assert 1 == len(forks) - - # set none - RepoModel().grant_user_permission(repo=forks[0], - user=usr, perm='repository.none') - Session().commit() - # fork shouldn't be there - response = self.app.get(url(controller='forks', action='forks', - repo_name=repo_name)) - response.mustcontain('There are no forks yet') - - -class TestGIT(_BaseTest): - REPO = GIT_REPO - NEW_REPO = NEW_GIT_REPO - REPO_TYPE = 'git' - REPO_FORK = GIT_FORK - - -class TestHG(_BaseTest): - REPO = HG_REPO - NEW_REPO = NEW_HG_REPO - REPO_TYPE = 'hg' - REPO_FORK = HG_FORK - - -@pytest.mark.usefixtures('app', 'autologin_user') -@pytest.mark.skip_backends('git','hg') -class TestSVNFork(object): - - def test_fork_redirects(self, backend): - denied_actions = ['fork','fork_create'] - for action in denied_actions: - response = self.app.get(url( - controller='forks', action=action, - repo_name=backend.repo_name)) - assert response.status_int == 302 - - # Not allowed, redirect to the summary - redirected = response.follow() - summary_url = h.route_path('repo_summary', repo_name=backend.repo_name) - - # URL adds leading slash and path doesn't have it - assert redirected.request.path == summary_url diff --git a/rhodecode/tests/functional/test_integrations.py b/rhodecode/tests/functional/test_integrations.py --- a/rhodecode/tests/functional/test_integrations.py +++ b/rhodecode/tests/functional/test_integrations.py @@ -18,15 +18,10 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -import mock import pytest -from webob.exc import HTTPNotFound -import rhodecode from rhodecode.model.db import Integration from rhodecode.model.meta import Session -from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN -from rhodecode.tests.utils import AssertResponse from rhodecode.integrations import integration_type_registry from rhodecode.config.routing import ADMIN_PREFIX