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}
-
+
+
+
+
+
+
%def>
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:
-
-
- ${_('Owner')} |
- ${_('Fork')} |
- ${_('Description')} |
- ${_('Forked')} |
- |
-
- % for f in c.forks_pager:
-
-
- ${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')}
-
- |
-
- % endfor
-
-
-% 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