diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -51,3 +51,4 @@ 14502561d22e6b70613674cd675ae9a604b7989f 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2 797744642eca86640ed20bef2cd77445780abaec v4.16.0 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1 +5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2 diff --git a/docs/release-notes/release-notes-4.16.2.rst b/docs/release-notes/release-notes-4.16.2.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-4.16.2.rst @@ -0,0 +1,41 @@ +|RCE| 4.16.2 |RNS| +------------------ + +Release Date +^^^^^^^^^^^^ + +- 2019-04-02 + + +New Features +^^^^^^^^^^^^ + + + +General +^^^^^^^ + + + +Security +^^^^^^^^ + + + +Performance +^^^^^^^^^^^ + + + +Fixes +^^^^^ + +- Integrations: fixed missing template variable for fork reference checks. +- Permissions: fixed server error when showing permissions for user groups. +- Pull requests: fixed a bug in removal of multiple reviewers at once. + + +Upgrade notes +^^^^^^^^^^^^^ + +- Scheduled release addressing problems in 4.16.X releases. diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,7 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-4.16.2.rst release-notes-4.16.1.rst release-notes-4.16.0.rst release-notes-4.15.2.rst diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -5,7 +5,7 @@ self: super: { "alembic" = super.buildPythonPackage { - name = "alembic-1.0.8"; + name = "alembic-1.0.9"; doCheck = false; propagatedBuildInputs = [ self."sqlalchemy" @@ -14,8 +14,8 @@ self: super: { self."python-dateutil" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/d6/bb/ec1e21f2e303689ad2170eb47fc67df9ad4199ade6759a99474c4d3535c8/alembic-1.0.8.tar.gz"; - sha256 = "1s34i1j0dsxbflxligwhnkf37a5hvcshsv8ibkcfdjf03ph42pah"; + url = "https://files.pythonhosted.org/packages/fc/42/8729e2491fa9b8eae160d1cbb429f61712bfc2d779816488c25cfdabf7b8/alembic-1.0.9.tar.gz"; + sha256 = "0a88rwp7fp0y8ykczj82ivr4ww1kiflcvb882lgfl9azm8csdfa0"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -310,11 +310,11 @@ self: super: { }; }; "coverage" = super.buildPythonPackage { - name = "coverage-4.5.1"; + name = "coverage-4.5.3"; doCheck = false; src = fetchurl { - url = "https://files.pythonhosted.org/packages/35/fe/e7df7289d717426093c68d156e0fd9117c8f4872b6588e8a8928a0f68424/coverage-4.5.1.tar.gz"; - sha256 = "1wbrzpxka3xd4nmmkc6q0ir343d91kymwsm8pbmwa0d2a7q4ir2n"; + url = "https://files.pythonhosted.org/packages/82/70/2280b5b29a0352519bb95ab0ef1ea942d40466ca71c53a2085bdeff7b0eb/coverage-4.5.3.tar.gz"; + sha256 = "02f6m073qdispn96rc616hg0rnmw1pgqzw3bgxwiwza4zf9hirlx"; }; meta = { license = [ pkgs.lib.licenses.asl20 ]; @@ -1118,14 +1118,14 @@ self: super: { }; }; "pexpect" = super.buildPythonPackage { - name = "pexpect-4.6.0"; + name = "pexpect-4.7.0"; doCheck = false; propagatedBuildInputs = [ self."ptyprocess" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/89/43/07d07654ee3e25235d8cea4164cdee0ec39d1fda8e9203156ebe403ffda4/pexpect-4.6.0.tar.gz"; - sha256 = "1fla85g47iaxxpjhp9vkxdnv4pgc7rplfy6ja491smrrk0jqi3ia"; + url = "https://files.pythonhosted.org/packages/1c/b1/362a0d4235496cb42c33d1d8732b5e2c607b0129ad5fdd76f5a583b9fcb3/pexpect-4.7.0.tar.gz"; + sha256 = "1sv2rri15zwhds85a4kamwh9pj49qcxv7m4miyr4jfpfwv81yb4y"; }; meta = { license = [ pkgs.lib.licenses.isc { fullName = "ISC License (ISCL)"; } ]; @@ -1160,15 +1160,15 @@ self: super: { }; }; "plaster-pastedeploy" = super.buildPythonPackage { - name = "plaster-pastedeploy-0.6"; + name = "plaster-pastedeploy-0.7"; doCheck = false; propagatedBuildInputs = [ self."pastedeploy" self."plaster" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/3f/e7/6a6833158d2038ec40085433308a1e164fd1dac595513f6dd556d5669bb8/plaster_pastedeploy-0.6.tar.gz"; - sha256 = "1bkggk18f4z2bmsmxyxabvf62znvjwbivzh880419r3ap0616cf2"; + url = "https://files.pythonhosted.org/packages/99/69/2d3bc33091249266a1bd3cf24499e40ab31d54dffb4a7d76fe647950b98c/plaster_pastedeploy-0.7.tar.gz"; + sha256 = "1zg7gcsvc1kzay1ry5p699rg2qavfsxqwl17mqxzr0gzw6j9679r"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1186,15 +1186,15 @@ self: super: { }; }; "prompt-toolkit" = super.buildPythonPackage { - name = "prompt-toolkit-1.0.15"; + name = "prompt-toolkit-1.0.16"; doCheck = false; propagatedBuildInputs = [ self."six" self."wcwidth" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz"; - sha256 = "05v9h5nydljwpj5nm8n804ms0glajwfy1zagrzqrg91wk3qqi1c5"; + url = "https://files.pythonhosted.org/packages/f1/03/bb36771dc9fa7553ac4bdc639a9ecdf6fda0ff4176faf940d97e3c16e41d/prompt_toolkit-1.0.16.tar.gz"; + sha256 = "1d65hm6nf0cbq0q0121m60zzy4s1fpg9fn761s1yxf08dridvkn1"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1212,11 +1212,11 @@ self: super: { }; }; "psycopg2" = super.buildPythonPackage { - name = "psycopg2-2.7.7"; + name = "psycopg2-2.8.2"; doCheck = false; src = fetchurl { - url = "https://files.pythonhosted.org/packages/63/54/c039eb0f46f9a9406b59a638415c2012ad7be9b4b97bfddb1f48c280df3a/psycopg2-2.7.7.tar.gz"; - sha256 = "0zjbabb4qjx9dm07imhf8y5a9rpa06d5zah80myiimgdi83nslpl"; + url = "https://files.pythonhosted.org/packages/23/7e/93c325482c328619870b6cd09370f6dbe1148283daca65115cd63642e60f/psycopg2-2.8.2.tar.gz"; + sha256 = "122mn2z3r0zgs8jyq682jjjr6vq5690qmxqf22gj6g41dwdz5b2w"; }; meta = { license = [ pkgs.lib.licenses.zpl21 { fullName = "GNU Library or Lesser General Public License (LGPL)"; } { fullName = "LGPL with exceptions or ZPL"; } ]; @@ -1373,7 +1373,7 @@ self: super: { }; }; "pyramid" = super.buildPythonPackage { - name = "pyramid-1.10.2"; + name = "pyramid-1.10.4"; doCheck = false; propagatedBuildInputs = [ self."hupper" @@ -1388,8 +1388,8 @@ self: super: { self."repoze.lru" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/bc/0e/73de9b189ff00a963beeedaff90e27b134eedf2806279a1a3fe122fd65b6/pyramid-1.10.2.tar.gz"; - sha256 = "0gn6sw6ml67ir150ffivc0ad5hd448p43p9z2bkyp12jh2n9n2p7"; + url = "https://files.pythonhosted.org/packages/c2/43/1ae701c9c6bb3a434358e678a5e72c96e8aa55cf4cb1d2fa2041b5dd38b7/pyramid-1.10.4.tar.gz"; + sha256 = "0rkxs1ajycg2zh1c94xlmls56mx5m161sn8112skj0amza6cn36q"; }; meta = { license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -1931,11 +1931,11 @@ self: super: { }; }; "setuptools" = super.buildPythonPackage { - name = "setuptools-40.8.0"; + name = "setuptools-41.0.0"; doCheck = false; src = fetchurl { - url = "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip"; - sha256 = "0k9hifpgahnw2a26w3cr346iy733k6d3nwh3f7g9m13y6f8fqkkf"; + url = "https://files.pythonhosted.org/packages/ed/69/c805067de1feedbb98c53174b0f2df44cc05e0e9ee73bb85eebc59e508c6/setuptools-41.0.0.zip"; + sha256 = "1cfwy2g23qj3262ivj0b1182lgwz7bqqbka35rkqwypynra05lvr"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -2012,14 +2012,14 @@ self: super: { }; }; "supervisor" = super.buildPythonPackage { - name = "supervisor-3.3.5"; + name = "supervisor-4.0.1"; doCheck = false; propagatedBuildInputs = [ self."meld3" ]; src = fetchurl { - url = "https://files.pythonhosted.org/packages/ba/65/92575a8757ed576beaee59251f64a3287bde82bdc03964b89df9e1d29e1b/supervisor-3.3.5.tar.gz"; - sha256 = "1w3ahridzbc6rxfpbyx8lij6pjlcgf2ymzyg53llkjqxalp6sk8v"; + url = "https://files.pythonhosted.org/packages/96/ec/f8190beeb0c6d29a30aea10389c11d0164b6ff221931ee84093315ecde6a/supervisor-4.0.1.tar.gz"; + sha256 = "10l3z7v6v1fyv7m5zbazzxciwvli2n9a41pxi27p4kixgsfp0s1j"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ pyparsing==2.3.0 pyramid-beaker==0.8 pyramid-debugtoolbar==4.5.0 pyramid-mako==1.0.2 -pyramid==1.10.2 +pyramid==1.10.4 pyramid_mailer==0.15.1 python-dateutil python-ldap==3.1.0 @@ -67,7 +67,7 @@ six==1.11.0 sqlalchemy==1.1.18 sshpubkeys==3.1.0 subprocess32==3.5.3 -supervisor==3.3.5 +supervisor==4.0.1 translationstring==1.3 urllib3==1.24.1 urlobject==2.4.3 @@ -87,7 +87,7 @@ zope.interface==4.6.0 mysql-python==1.2.5 pymysql==0.8.1 pysqlite==2.8.3 -psycopg2==2.7.7 +psycopg2==2.8.2 # IPYTHON RENDERING # entrypoints backport, pypi version doesn't support egg installs @@ -97,7 +97,7 @@ nbformat==4.4.0 jupyter_client==5.0.0 ## cli tools -alembic==1.0.8 +alembic==1.0.9 invoke==0.13.0 bumpversion==0.5.3 diff --git a/requirements_test.txt b/requirements_test.txt --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ gprof2dot==2017.9.19 mock==1.0.1 cov-core==1.15.0 -coverage==4.5.1 +coverage==4.5.3 webtest==2.0.33 beautifulsoup4==4.6.3 diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -45,7 +45,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 95 # defines current db version for migrations +__dbversion__ = 97 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -424,6 +424,10 @@ def admin_routes(config): pattern='/repo_groups') config.add_route( + name='repo_groups_data', + pattern='/repo_groups_data') + + config.add_route( name='repo_group_new', pattern='/repo_group/new') diff --git a/rhodecode/apps/admin/tests/test_admin_repository_groups.py b/rhodecode/apps/admin/tests/test_admin_repository_groups.py --- a/rhodecode/apps/admin/tests/test_admin_repository_groups.py +++ b/rhodecode/apps/admin/tests/test_admin_repository_groups.py @@ -23,11 +23,11 @@ import pytest from rhodecode.apps._base import ADMIN_PREFIX from rhodecode.lib import helpers as h -from rhodecode.model.db import Repository, UserRepoToPerm, User +from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup from rhodecode.model.meta import Session from rhodecode.model.repo_group import RepoGroupModel from rhodecode.tests import ( - assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController) + assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH) from rhodecode.tests.fixture import Fixture fixture = Fixture() @@ -38,6 +38,7 @@ def route_path(name, params=None, **kwar base_url = { 'repo_groups': ADMIN_PREFIX + '/repo_groups', + 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data', 'repo_group_new': ADMIN_PREFIX + '/repo_group/new', 'repo_group_create': ADMIN_PREFIX + '/repo_group/create', @@ -59,13 +60,30 @@ def _get_permission_for_user(user, repo) @pytest.mark.usefixtures("app") class TestAdminRepositoryGroups(object): + def test_show_repo_groups(self, autologin_user): - response = self.app.get(route_path('repo_groups')) - response.mustcontain('data: []') + self.app.get(route_path('repo_groups')) + + def test_show_repo_groups_data(self, autologin_user, xhr_header): + response = self.app.get(route_path( + 'repo_groups_data'), extra_environ=xhr_header) + + all_repo_groups = RepoGroup.query().count() + assert response.json['recordsTotal'] == all_repo_groups - def test_show_repo_groups_after_creating_group(self, autologin_user): + def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header): + response = self.app.get(route_path( + 'repo_groups_data', params={'search[value]': 'empty_search'}), + extra_environ=xhr_header) + + all_repo_groups = RepoGroup.query().count() + assert response.json['recordsTotal'] == all_repo_groups + assert response.json['recordsFiltered'] == 0 + + def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header): fixture.create_repo_group('test_repo_group') - response = self.app.get(route_path('repo_groups')) + response = self.app.get(route_path( + 'repo_groups_data'), extra_environ=xhr_header) response.mustcontain('"name_raw": "test_repo_group"') fixture.destroy_repo_group('test_repo_group') diff --git a/rhodecode/apps/admin/views/repo_groups.py b/rhodecode/apps/admin/views/repo_groups.py --- a/rhodecode/apps/admin/views/repo_groups.py +++ b/rhodecode/apps/admin/views/repo_groups.py @@ -17,7 +17,7 @@ # 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 datetime import logging import formencode import formencode.htmlfill @@ -30,16 +30,16 @@ from pyramid.response import Response from rhodecode import events from rhodecode.apps._base import BaseAppView, DataGridAppView -from rhodecode.lib.ext_json import json from rhodecode.lib.auth import ( LoginRequired, CSRFRequired, NotAnonymous, HasPermissionAny, HasRepoGroupPermissionAny) from rhodecode.lib import helpers as h, audit_logger -from rhodecode.lib.utils2 import safe_int, safe_unicode +from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time from rhodecode.model.forms import RepoGroupForm from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.scm import RepoGroupList -from rhodecode.model.db import Session, RepoGroup +from rhodecode.model.db import ( + or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository) log = logging.getLogger(__name__) @@ -88,22 +88,168 @@ class AdminRepoGroupsView(BaseAppView, D return False return False + # permission check in data loading of + # `repo_group_list_data` via RepoGroupList @LoginRequired() @NotAnonymous() - # perms check inside @view_config( route_name='repo_groups', request_method='GET', renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako') def repo_group_list(self): c = self.load_default_context() + return self._get_template_context(c) - repo_group_list = RepoGroup.get_all_repo_groups() - repo_group_list_acl = RepoGroupList( - repo_group_list, perm_set=['group.admin']) - repo_group_data = RepoGroupModel().get_repo_groups_as_dict( - repo_group_list=repo_group_list_acl, admin=True) - c.data = json.dumps(repo_group_data) - return self._get_template_context(c) + # permission check inside + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='repo_groups_data', request_method='GET', + renderer='json_ext', xhr=True) + def repo_group_list_data(self): + self.load_default_context() + column_map = { + 'name_raw': 'group_name_hash', + 'desc': 'group_description', + 'last_change_raw': 'updated_on', + 'top_level_repos': 'repos_total', + 'owner': 'user_username', + } + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering( + self.request, column_map=column_map) + + _render = self.request.get_partial_renderer( + 'rhodecode:templates/data_table/_dt_elements.mako') + c = _render.get_call_context() + + def quick_menu(repo_group_name): + return _render('quick_repo_group_menu', repo_group_name) + + def repo_group_lnk(repo_group_name): + return _render('repo_group_name', repo_group_name) + + def last_change(last_change): + if isinstance(last_change, datetime.datetime) and not last_change.tzinfo: + delta = datetime.timedelta( + seconds=(datetime.datetime.now() - datetime.datetime.utcnow()).seconds) + last_change = last_change + delta + return _render("last_change", last_change) + + def desc(desc, personal): + return _render( + 'repo_group_desc', desc, personal, c.visual.stylify_metatags) + + def repo_group_actions(repo_group_id, repo_group_name, gr_count): + return _render( + 'repo_group_actions', repo_group_id, repo_group_name, gr_count) + + def user_profile(username): + return _render('user_profile', username) + + auth_repo_group_list = RepoGroupList( + RepoGroup.query().all(), perm_set=['group.admin']) + + allowed_ids = [-1] + for repo_group in auth_repo_group_list: + allowed_ids.append(repo_group.group_id) + + repo_groups_data_total_count = RepoGroup.query()\ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(RepoGroup.group_id, allowed_ids) + )) \ + .count() + + repo_groups_data_total_inactive_count = RepoGroup.query()\ + .filter(RepoGroup.group_id.in_(allowed_ids))\ + .count() + + repo_count = count(Repository.repo_id) + base_q = Session.query( + RepoGroup.group_name, + RepoGroup.group_name_hash, + RepoGroup.group_description, + RepoGroup.group_id, + RepoGroup.personal, + RepoGroup.updated_on, + User, + repo_count.label('repos_count') + ) \ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(RepoGroup.group_id, allowed_ids) + )) \ + .outerjoin(Repository) \ + .join(User, User.user_id == RepoGroup.user_id) \ + .group_by(RepoGroup, User) + + if search_q: + like_expression = u'%{}%'.format(safe_unicode(search_q)) + base_q = base_q.filter(or_( + RepoGroup.group_name.ilike(like_expression), + )) + + repo_groups_data_total_filtered_count = base_q.count() + # the inactive isn't really used, but we still make it same as other data grids + # which use inactive (users,user groups) + repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count + + sort_defined = False + if order_by == 'group_name': + sort_col = func.lower(RepoGroup.group_name) + sort_defined = True + elif order_by == 'repos_total': + sort_col = repo_count + sort_defined = True + elif order_by == 'user_username': + sort_col = User.username + else: + sort_col = getattr(RepoGroup, order_by, None) + + if sort_defined or sort_col: + if order_dir == 'asc': + sort_col = sort_col.asc() + else: + sort_col = sort_col.desc() + + base_q = base_q.order_by(sort_col) + base_q = base_q.offset(start).limit(limit) + + # authenticated access to user groups + auth_repo_group_list = base_q.all() + + repo_groups_data = [] + for repo_gr in auth_repo_group_list: + row = { + "menu": quick_menu(repo_gr.group_name), + "name": repo_group_lnk(repo_gr.group_name), + "name_raw": repo_gr.group_name, + "last_change": last_change(repo_gr.updated_on), + "last_change_raw": datetime_to_time(repo_gr.updated_on), + + "last_changeset": "", + "last_changeset_raw": "", + + "desc": desc(repo_gr.group_description, repo_gr.personal), + "owner": user_profile(repo_gr.User.username), + "top_level_repos": repo_gr.repos_count, + "action": repo_group_actions( + repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count), + + } + + repo_groups_data.append(row) + + data = ({ + 'draw': draw, + 'data': repo_groups_data, + 'recordsTotal': repo_groups_data_total_count, + 'recordsTotalInactive': repo_groups_data_total_inactive_count, + 'recordsFiltered': repo_groups_data_total_filtered_count, + 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count, + }) + + return data @LoginRequired() @NotAnonymous() diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py --- a/rhodecode/apps/admin/views/user_groups.py +++ b/rhodecode/apps/admin/views/user_groups.py @@ -39,7 +39,7 @@ from rhodecode.model.forms import UserGr from rhodecode.model.permission import PermissionModel from rhodecode.model.scm import UserGroupList from rhodecode.model.db import ( - or_, count, User, UserGroup, UserGroupMember) + or_, count, User, UserGroup, UserGroupMember, in_filter_generator) from rhodecode.model.meta import Session from rhodecode.model.user_group import UserGroupModel from rhodecode.model.db import true @@ -107,11 +107,17 @@ class AdminUserGroupsView(BaseAppView, D allowed_ids.append(user_group.users_group_id) user_groups_data_total_count = UserGroup.query()\ - .filter(UserGroup.users_group_id.in_(allowed_ids))\ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(UserGroup.users_group_id, allowed_ids) + ))\ .count() user_groups_data_total_inactive_count = UserGroup.query()\ - .filter(UserGroup.users_group_id.in_(allowed_ids))\ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(UserGroup.users_group_id, allowed_ids) + ))\ .filter(UserGroup.users_group_active != true()).count() member_count = count(UserGroupMember.user_id) @@ -123,11 +129,14 @@ class AdminUserGroupsView(BaseAppView, D UserGroup.group_data, User, member_count.label('member_count') - ) \ - .filter(UserGroup.users_group_id.in_(allowed_ids)) \ - .outerjoin(UserGroupMember) \ - .join(User, User.user_id == UserGroup.user_id) \ - .group_by(UserGroup, User) + ) \ + .filter(or_( + # generate multiple IN to fix limitation problems + *in_filter_generator(UserGroup.users_group_id, allowed_ids) + )) \ + .outerjoin(UserGroupMember) \ + .join(User, User.user_id == UserGroup.user_id) \ + .group_by(UserGroup, User) base_q_inactive = base_q.filter(UserGroup.users_group_active != true()) @@ -141,14 +150,16 @@ class AdminUserGroupsView(BaseAppView, D user_groups_data_total_filtered_count = base_q.count() user_groups_data_total_filtered_inactive_count = base_q_inactive.count() + sort_defined = False if order_by == 'members_total': sort_col = member_count + sort_defined = True elif order_by == 'user_username': sort_col = User.username else: sort_col = getattr(UserGroup, order_by, None) - if isinstance(sort_col, count) or sort_col: + if sort_defined or sort_col: if order_dir == 'asc': sort_col = sort_col.asc() else: @@ -162,7 +173,7 @@ class AdminUserGroupsView(BaseAppView, D user_groups_data = [] for user_gr in auth_user_group_list: - user_groups_data.append({ + row = { "users_group_name": user_group_name(user_gr.users_group_name), "name_raw": h.escape(user_gr.users_group_name), "description": h.escape(user_gr.user_group_description), @@ -175,7 +186,8 @@ class AdminUserGroupsView(BaseAppView, D "owner": user_profile(user_gr.User.username), "action": user_group_actions( user_gr.users_group_id, user_gr.users_group_name) - }) + } + user_groups_data.append(row) data = ({ 'draw': draw, diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py --- a/rhodecode/apps/repository/views/repo_settings_advanced.py +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -67,12 +67,6 @@ class RepoSettingsView(RepoAppView): .filter(UserFollowing.user_id == c.default_user_id) \ .filter(UserFollowing.follows_repository == self.db_repo).scalar() - c.has_origin_repo_read_perm = False - if self.db_repo.fork: - c.has_origin_repo_read_perm = h.HasRepoPermissionAny( - 'repository.write', 'repository.read', 'repository.admin')( - self.db_repo.fork.repo_name, 'repo set as fork page') - c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info() return self._get_template_context(c) diff --git a/rhodecode/apps/ssh_support/lib/backends/hg.py b/rhodecode/apps/ssh_support/lib/backends/hg.py --- a/rhodecode/apps/ssh_support/lib/backends/hg.py +++ b/rhodecode/apps/ssh_support/lib/backends/hg.py @@ -20,12 +20,13 @@ import os import sys -import shutil import logging import tempfile import textwrap - +import collections from .base import VcsServer +from rhodecode.model.db import RhodeCodeUi +from rhodecode.model.settings import VcsSettingsModel log = logging.getLogger(__name__) @@ -37,62 +38,46 @@ class MercurialTunnelWrapper(object): self.server = server self.stdin = sys.stdin self.stdout = sys.stdout - self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp() - self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp() + self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_') def create_hooks_env(self): + repo_name = self.server.repo_name + hg_flags = self.config_to_hgrc(repo_name) content = textwrap.dedent( ''' - # SSH hooks version=1.0.0 - [hooks] - pretxnchangegroup.ssh_auth=python:vcsserver.hooks.pre_push_ssh_auth - pretxnchangegroup.ssh=python:vcsserver.hooks.pre_push_ssh - changegroup.ssh=python:vcsserver.hooks.post_push_ssh - - preoutgoing.ssh=python:vcsserver.hooks.pre_pull_ssh - outgoing.ssh=python:vcsserver.hooks.post_pull_ssh + # RhodeCode SSH hooks version=2.0.0 + {custom} + ''' + ).format(custom='\n'.join(hg_flags)) - ''' - ) + root = self.server.get_root_store() + hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode') + hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc') + # cleanup custom hgrc file + if os.path.isfile(hgrc_custom): + with open(hgrc_custom, 'wb') as f: + f.write('') + log.debug('Cleanup custom hgrc file under %s', hgrc_custom) + + # write temp with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file: hooks_env_file.write(content) - root = self.server.get_root_store() - hgrc_custom = os.path.join( - root, self.server.repo_name, '.hg', 'hgrc_rhodecode') - log.debug('Wrote custom hgrc file under %s', hgrc_custom) - shutil.move( - self.hooks_env_path, hgrc_custom) - - hgrc_main = os.path.join( - root, self.server.repo_name, '.hg', 'hgrc') - include_marker = '%include hgrc_rhodecode' + return self.hooks_env_path - if not os.path.isfile(hgrc_main): - os.mknod(hgrc_main) - - with open(hgrc_main, 'rb') as f: - data = f.read() - has_marker = include_marker in data + def remove_configs(self): + os.remove(self.hooks_env_path) - if not has_marker: - log.debug('Adding include marker for hooks') - with open(hgrc_main, 'wa') as f: - f.write(textwrap.dedent(''' - # added by RhodeCode - {} - '''.format(include_marker))) - - def command(self): + def command(self, hgrc_path): root = self.server.get_root_store() command = ( - "cd {root}; {hg_path} -R {root}{repo_name} " + "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} " "serve --stdio".format( root=root, hg_path=self.server.hg_path, - repo_name=self.server.repo_name)) + repo_name=self.server.repo_name, hgrc=hgrc_path)) log.debug("Final CMD: %s", command) return command @@ -102,22 +87,61 @@ class MercurialTunnelWrapper(object): action = '?' # permissions are check via `pre_push_ssh_auth` hook self.server.update_environment(action=action, extras=extras) - self.create_hooks_env() - return os.system(self.command()) + custom_hgrc_file = self.create_hooks_env() + + try: + return os.system(self.command(custom_hgrc_file)) + finally: + self.remove_configs() class MercurialServer(VcsServer): backend = 'hg' + cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks'] - def __init__(self, store, ini_path, repo_name, - user, user_permissions, config, env): - super(MercurialServer, self).\ - __init__(user, user_permissions, config, env) + def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env): + super(MercurialServer, self).__init__(user, user_permissions, config, env) self.store = store self.ini_path = ini_path self.repo_name = repo_name - self._path = self.hg_path = config.get( - 'app:main', 'ssh.executable.hg') + self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg') + self.tunnel = MercurialTunnelWrapper(server=self) + + def config_to_hgrc(self, repo_name): + ui_sections = collections.defaultdict(list) + ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None) + + # write default hooks + default_hooks = [ + ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'), + ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'), + ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'), + + ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'), + ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'), + ] + + for k, v in default_hooks: + ui_sections['hooks'].append((k, v)) - self.tunnel = MercurialTunnelWrapper(server=self) + for entry in ui: + if not entry.active: + continue + sec = entry.section + key = entry.key + + if sec in self.cli_flags: + # we want only custom hooks, so we skip builtins + if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN: + continue + + ui_sections[sec].append([key, entry.value]) + + flags = [] + for _sec, key_val in ui_sections.items(): + flags.append(' ') + flags.append('[{}]'.format(_sec)) + for key, val in key_val: + flags.append('{}= {}'.format(key, val)) + return flags diff --git a/rhodecode/apps/ssh_support/tests/test_server_hg.py b/rhodecode/apps/ssh_support/tests/test_server_hg.py --- a/rhodecode/apps/ssh_support/tests/test_server_hg.py +++ b/rhodecode/apps/ssh_support/tests/test_server_hg.py @@ -18,6 +18,7 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import os import mock import pytest @@ -68,14 +69,16 @@ def hg_server(app): class TestMercurialServer(object): - def test_command(self, hg_server): + def test_command(self, hg_server, tmpdir): server = hg_server.create() + custom_hgrc = os.path.join(str(tmpdir), 'hgrc') expected_command = ( - 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'.format( - root=hg_server.root, hg_path=hg_server.hg_path, + 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format( + root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path, repo_name=hg_server.repo_name) ) - assert expected_command == server.tunnel.command() + server_command = server.tunnel.command(custom_hgrc) + assert expected_command == server_command @pytest.mark.parametrize('permissions, action, code', [ ({}, 'pull', -2), diff --git a/rhodecode/integrations/views.py b/rhodecode/integrations/views.py --- a/rhodecode/integrations/views.py +++ b/rhodecode/integrations/views.py @@ -114,7 +114,6 @@ class IntegrationSettingsViewBase(BaseAp _ = self.request.translate c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context( include_app_defaults=include_app_defaults) - c.active = 'integrations' return c @@ -404,6 +403,11 @@ class RepoIntegrationsView(IntegrationSe c.repo_name = self.db_repo.repo_name c.repository_pull_requests = ScmModel().get_pull_requests(self.repo) + c.has_origin_repo_read_perm = False + if self.db_repo.fork: + c.has_origin_repo_read_perm = h.HasRepoPermissionAny( + 'repository.write', 'repository.read', 'repository.admin')( + self.db_repo.fork.repo_name, 'summary fork link') return c @LoginRequired() diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -375,6 +375,27 @@ class DbManage(object): hgevolve.ui_active = False self.sa.add(hgevolve) + hgevolve = RhodeCodeUi() + hgevolve.ui_section = 'experimental' + hgevolve.ui_key = 'evolution' + hgevolve.ui_value = '' + hgevolve.ui_active = False + self.sa.add(hgevolve) + + hgevolve = RhodeCodeUi() + hgevolve.ui_section = 'experimental' + hgevolve.ui_key = 'evolution.exchange' + hgevolve.ui_value = '' + hgevolve.ui_active = False + self.sa.add(hgevolve) + + hgevolve = RhodeCodeUi() + hgevolve.ui_section = 'extensions' + hgevolve.ui_key = 'topic' + hgevolve.ui_value = '' + hgevolve.ui_active = False + self.sa.add(hgevolve) + # enable hggit disabled by default hggit = RhodeCodeUi() hggit.ui_section = 'extensions' diff --git a/rhodecode/lib/dbmigrate/versions/096_version_4_17_0.py b/rhodecode/lib/dbmigrate/versions/096_version_4_17_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/096_version_4_17_0.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import logging + +from alembic.migration import MigrationContext +from alembic.operations import Operations +from sqlalchemy import String, Column +from sqlalchemy.sql import text + +from rhodecode.lib.dbmigrate.versions import _reset_base +from rhodecode.model import meta, init_model_encryption +from rhodecode.model.db import RepoGroup + + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + from rhodecode.lib.dbmigrate.schema import db_4_16_0_2 + + init_model_encryption(db_4_16_0_2) + + context = MigrationContext.configure(migrate_engine.connect()) + op = Operations(context) + + repo_group = db_4_16_0_2.RepoGroup.__table__ + + with op.batch_alter_table(repo_group.name) as batch_op: + batch_op.add_column( + Column("repo_group_name_hash", String(1024), nullable=True, unique=False)) + + _generate_repo_group_name_hashes(db_4_16_0_2, op, meta.Session) + + +def downgrade(migrate_engine): + pass + + +def _generate_repo_group_name_hashes(models, op, session): + repo_groups = models.RepoGroup.get_all() + for repo_group in repo_groups: + print(repo_group.group_name) + hash_ = RepoGroup.hash_repo_group_name(repo_group.group_name) + params = {'hash': hash_, 'id': repo_group.group_id} + query = text( + 'UPDATE groups SET repo_group_name_hash = :hash' + ' WHERE group_id = :id').bindparams(**params) + op.execute(query) + session().commit() diff --git a/rhodecode/lib/dbmigrate/versions/097_version_4_17_0.py b/rhodecode/lib/dbmigrate/versions/097_version_4_17_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/097_version_4_17_0.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import logging + +from alembic.migration import MigrationContext +from alembic.operations import Operations + +from rhodecode.lib.dbmigrate.versions import _reset_base +from rhodecode.model import init_model_encryption + + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + from rhodecode.lib.dbmigrate.schema import db_4_16_0_2 + + init_model_encryption(db_4_16_0_2) + + context = MigrationContext.configure(migrate_engine.connect()) + op = Operations(context) + + repo_group = db_4_16_0_2.RepoGroup.__table__ + + with op.batch_alter_table(repo_group.name) as batch_op: + batch_op.alter_column("repo_group_name_hash", nullable=False) + + +def downgrade(migrate_engine): + pass + + +def _generate_repo_group_name_hashes(models, op, session): + pass diff --git a/rhodecode/lib/hooks_utils.py b/rhodecode/lib/hooks_utils.py --- a/rhodecode/lib/hooks_utils.py +++ b/rhodecode/lib/hooks_utils.py @@ -80,7 +80,7 @@ def trigger_log_create_pull_request_hook extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'create_pull_request') events.trigger(events.PullRequestCreateEvent(pull_request)) - extras.update(pull_request.get_api_data()) + extras.update(pull_request.get_api_data(with_merge_state=False)) hooks_base.log_create_pull_request(**extras) diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -371,7 +371,8 @@ def config_data_from_db(clear_session=Tr config.append(( safe_str(setting.section), safe_str(setting.key), False)) log.debug( - 'settings ui from db: %s', + 'settings ui from db@repo[%s]: %s', + repo, ','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data))) if clear_session: meta.Session.remove() diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -161,7 +161,7 @@ class MergeResponse(object): u'This pull request cannot be merged because the source contains ' u'more branches than the target.'), MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext( - u'This pull request cannot be merged because the target ' + u'This pull request cannot be merged because the target `{target_ref.name}` ' u'has multiple heads: `{heads}`.'), MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext( u'This pull request cannot be merged because the target repository is ' @@ -309,6 +309,9 @@ class BaseRepository(object): def _remote(self): raise NotImplementedError + def _heads(self, branch=None): + return [] + @LazyProperty def EMPTY_COMMIT(self): return EmptyCommit(self.EMPTY_COMMIT_ID) diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -715,11 +715,16 @@ class MercurialRepository(BaseRepository try: if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1: - heads = ','.join(self._heads(target_ref.name)) + heads = '\n,'.join(self._heads(target_ref.name)) + metadata = { + 'target_ref': target_ref, + 'source_ref': source_ref, + 'heads': heads + } return MergeResponse( False, False, None, MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, - metadata={'heads': heads}) + metadata=metadata) except CommitDoesNotExistError: log.exception('Failure when looking up branch heads on hg target') return MergeResponse( diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -25,6 +25,7 @@ Database Models for RhodeCode Enterprise import re import os import time +import string import hashlib import logging import datetime @@ -50,6 +51,7 @@ from sqlalchemy.dialects.mysql import LO from zope.cachedescriptors.property import Lazy as LazyProperty from pyramid import compat from pyramid.threadlocal import get_current_request +from webhelpers.text import collapse, remove_formatting from rhodecode.translation import _ from rhodecode.lib.vcs import get_vcs_instance @@ -409,6 +411,15 @@ class RhodeCodeUi(Base, BaseModel): HOOK_PUSH = 'changegroup.push_logger' HOOK_PUSH_KEY = 'pushkey.key_push' + HOOKS_BUILTIN = [ + HOOK_PRE_PULL, + HOOK_PULL, + HOOK_PRE_PUSH, + HOOK_PRETX_PUSH, + HOOK_PUSH, + HOOK_PUSH_KEY, + ] + # TODO: johbo: Unify way how hooks are configured for git and hg, # git part is currently hardcoded. @@ -2469,7 +2480,8 @@ class RepoGroup(Base, BaseModel): CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) - group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) + _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) + group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False) group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None) enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) @@ -2492,6 +2504,15 @@ class RepoGroup(Base, BaseModel): return u"<%s('id:%s:%s')>" % ( self.__class__.__name__, self.group_id, self.group_name) + @hybrid_property + def group_name(self): + return self._group_name + + @group_name.setter + def group_name(self, value): + self._group_name = value + self.group_name_hash = self.hash_repo_group_name(value) + @validates('group_parent_id') def validate_group_parent_id(self, key, val): """ @@ -2508,6 +2529,18 @@ class RepoGroup(Base, BaseModel): return h.escape(self.group_description) @classmethod + def hash_repo_group_name(cls, repo_group_name): + val = remove_formatting(repo_group_name) + val = safe_str(val).lower() + chars = [] + for c in val: + if c not in string.ascii_letters: + c = str(ord(c)) + chars.append(c) + + return ''.join(chars) + + @classmethod def _generate_choice(cls, repo_group): from webhelpers.html import literal as _literal _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k)) @@ -2770,6 +2803,13 @@ class RepoGroup(Base, BaseModel): } return data + def get_dict(self): + # Since we transformed `group_name` to a hybrid property, we need to + # keep compatibility with the code which uses `group_name` field. + result = super(RepoGroup, self).get_dict() + result['group_name'] = result.pop('_group_name', None) + return result + class Permission(Base, BaseModel): __tablename__ = 'permissions' diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -1020,6 +1020,9 @@ class PullRequestModel(BaseModel): log.debug("Adding %s reviewers", ids_to_add) log.debug("Removing %s reviewers", ids_to_remove) changed = False + added_audit_reviewers = [] + removed_audit_reviewers = [] + for uid in ids_to_add: changed = True _usr = self._get_user(uid) @@ -1030,29 +1033,37 @@ class PullRequestModel(BaseModel): # NOTE(marcink): mandatory shouldn't be changed now # reviewer.mandatory = reviewers[uid]['reasons'] Session().add(reviewer) - self._log_audit_action( - 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()}, - user, pull_request) + added_audit_reviewers.append(reviewer.get_dict()) for uid in ids_to_remove: changed = True + # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case + # that prevents and fixes cases that we added the same reviewer twice. + # this CAN happen due to the lack of DB checks reviewers = PullRequestReviewers.query()\ .filter(PullRequestReviewers.user_id == uid, PullRequestReviewers.pull_request == pull_request)\ .all() - # use .all() in case we accidentally added the same person twice - # this CAN happen due to the lack of DB checks + for obj in reviewers: - old_data = obj.get_dict() + added_audit_reviewers.append(obj.get_dict()) Session().delete(obj) - self._log_audit_action( - 'repo.pull_request.reviewer.delete', - {'old_data': old_data}, user, pull_request) if changed: + Session().expire_all() pull_request.updated_on = datetime.datetime.now() Session().add(pull_request) + # finally store audit logs + for user_data in added_audit_reviewers: + self._log_audit_action( + 'repo.pull_request.reviewer.add', {'data': user_data}, + user, pull_request) + for user_data in removed_audit_reviewers: + self._log_audit_action( + 'repo.pull_request.reviewer.delete', {'old_data': user_data}, + user, pull_request) + self.notify_reviewers(pull_request, ids_to_add) return ids_to_add, ids_to_remove @@ -1306,8 +1317,16 @@ class PullRequestModel(BaseModel): possible = pull_request.last_merge_status == MergeFailureReason.NONE metadata = { 'target_ref': pull_request.target_ref_parts, - 'source_ref': pull_request.source_ref_parts + 'source_ref': pull_request.source_ref_parts, } + if not possible and target_ref.type == 'branch': + # NOTE(marcink): case for mercurial multiple heads on branch + heads = target_vcs._heads(target_ref.name) + if len(heads) != 1: + heads = '\n,'.join(target_vcs._heads(target_ref.name)) + metadata.update({ + 'heads': heads + }) merge_state = MergeResponse( possible, False, None, pull_request.last_merge_status, metadata=metadata) diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -119,6 +119,7 @@ class SettingsModel(BaseModel): new_ui.ui_value = val new_ui.ui_active = active + repository_id = '' if self.repo: repo = self._get_repo(self.repo) repository_id = repo.repo_id @@ -440,26 +441,39 @@ class VcsSettingsModel(object): HOOKS_SETTINGS = ( ('hooks', 'changegroup.repo_size'), ('hooks', 'changegroup.push_logger'), - ('hooks', 'outgoing.pull_logger'),) + ('hooks', 'outgoing.pull_logger'), + ) HG_SETTINGS = ( ('extensions', 'largefiles'), ('phases', 'publish'), - ('extensions', 'evolve'),) + ('extensions', 'evolve'), + ('extensions', 'topic'), + ('experimental', 'evolution'), + ('experimental', 'evolution.exchange'), + ) GIT_SETTINGS = ( - ('vcs_git_lfs', 'enabled'),) + ('vcs_git_lfs', 'enabled'), + ) GLOBAL_HG_SETTINGS = ( ('extensions', 'largefiles'), ('largefiles', 'usercache'), ('phases', 'publish'), ('extensions', 'hgsubversion'), - ('extensions', 'evolve'),) + ('extensions', 'evolve'), + ('extensions', 'topic'), + ('experimental', 'evolution'), + ('experimental', 'evolution.exchange'), + ) + GLOBAL_GIT_SETTINGS = ( ('vcs_git_lfs', 'enabled'), - ('vcs_git_lfs', 'store_location')) + ('vcs_git_lfs', 'store_location') + ) GLOBAL_SVN_SETTINGS = ( ('vcs_svn_proxy', 'http_requests_enabled'), - ('vcs_svn_proxy', 'http_server_url')) + ('vcs_svn_proxy', 'http_server_url') + ) SVN_BRANCH_SECTION = 'vcs_svn_branch' SVN_TAG_SECTION = 'vcs_svn_tag' @@ -574,12 +588,38 @@ class VcsSettingsModel(object): def create_repo_svn_settings(self, data): return self._create_svn_settings(self.repo_settings, data) + def _set_evolution(self, settings, is_enabled): + if is_enabled: + # if evolve is active set evolution=all + + self._create_or_update_ui( + settings, *('experimental', 'evolution'), value='all', + active=True) + self._create_or_update_ui( + settings, *('experimental', 'evolution.exchange'), value='yes', + active=True) + # if evolve is active set topics server support + self._create_or_update_ui( + settings, *('extensions', 'topic'), value='', + active=True) + + else: + self._create_or_update_ui( + settings, *('experimental', 'evolution'), value='', + active=False) + self._create_or_update_ui( + settings, *('experimental', 'evolution.exchange'), value='no', + active=False) + self._create_or_update_ui( + settings, *('extensions', 'topic'), value='', + active=False) + @assert_repo_settings def create_or_update_repo_hg_settings(self, data): largefiles, phases, evolve = \ - self.HG_SETTINGS + self.HG_SETTINGS[:3] largefiles_key, phases_key, evolve_key = \ - self._get_settings_keys(self.HG_SETTINGS, data) + self._get_settings_keys(self.HG_SETTINGS[:3], data) self._create_or_update_ui( self.repo_settings, *largefiles, value='', @@ -587,21 +627,22 @@ class VcsSettingsModel(object): self._create_or_update_ui( self.repo_settings, *evolve, value='', active=data[evolve_key]) + self._set_evolution(self.repo_settings, is_enabled=data[evolve_key]) + self._create_or_update_ui( self.repo_settings, *phases, value=safe_str(data[phases_key])) def create_or_update_global_hg_settings(self, data): largefiles, largefiles_store, phases, hgsubversion, evolve \ - = self.GLOBAL_HG_SETTINGS + = self.GLOBAL_HG_SETTINGS[:5] largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \ - = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data) + = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data) self._create_or_update_ui( self.global_settings, *largefiles, value='', active=data[largefiles_key]) self._create_or_update_ui( - self.global_settings, *largefiles_store, - value=data[largefiles_store_key]) + self.global_settings, *largefiles_store, value=data[largefiles_store_key]) self._create_or_update_ui( self.global_settings, *phases, value=safe_str(data[phases_key])) self._create_or_update_ui( @@ -609,9 +650,10 @@ class VcsSettingsModel(object): self._create_or_update_ui( self.global_settings, *evolve, value='', active=data[evolve_key]) + self._set_evolution(self.global_settings, is_enabled=data[evolve_key]) def create_or_update_repo_git_settings(self, data): - # NOTE(marcink): # comma make unpack work properly + # NOTE(marcink): # comma makes unpack work properly lfs_enabled, \ = self.GIT_SETTINGS @@ -675,6 +717,7 @@ class VcsSettingsModel(object): def get_repo_ui_settings(self, section=None, key=None): global_uis = self.global_settings.get_ui(section, key) repo_uis = self.repo_settings.get_ui(section, key) + filtered_repo_uis = self._filter_ui_settings(repo_uis) filtered_repo_uis_keys = [ (s.section, s.key) for s in filtered_repo_uis] diff --git a/rhodecode/templates/admin/repo_groups/repo_groups.mako b/rhodecode/templates/admin/repo_groups/repo_groups.mako --- a/rhodecode/templates/admin/repo_groups/repo_groups.mako +++ b/rhodecode/templates/admin/repo_groups/repo_groups.mako @@ -40,15 +40,32 @@ - // refilter table if page load via back button - $("#q_filter").trigger('keyup'); -}); - diff --git a/rhodecode/templates/admin/repos/repos.mako b/rhodecode/templates/admin/repos/repos.mako --- a/rhodecode/templates/admin/repos/repos.mako +++ b/rhodecode/templates/admin/repos/repos.mako @@ -70,7 +70,7 @@ { data: {"_": "state", "sort": "state"}, title: "${_('State')}", className: "td-tags td-state" }, { data: {"_": "action", - "sort": "action"}, title: "${_('Action')}", className: "td-action" } + "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false } ], language: { paginate: DEFAULT_GRID_PAGINATION, diff --git a/rhodecode/templates/admin/user_groups/user_groups.mako b/rhodecode/templates/admin/user_groups/user_groups.mako --- a/rhodecode/templates/admin/user_groups/user_groups.mako +++ b/rhodecode/templates/admin/user_groups/user_groups.mako @@ -105,7 +105,7 @@ // filter $('#q_filter').on('keyup', $.debounce(250, function() { - $('#user_group_list_table').DataTable().search( + $userGroupsListTable.DataTable().search( $('#q_filter').val() ).draw(); }) diff --git a/rhodecode/templates/base/vcs_settings.mako b/rhodecode/templates/base/vcs_settings.mako --- a/rhodecode/templates/base/vcs_settings.mako +++ b/rhodecode/templates/base/vcs_settings.mako @@ -133,13 +133,13 @@
${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)} - +
% if display_globals: - ${_('Enable evolve extension for all repositories.')} + ${_('Enable Evolve and Topic extensions for all repositories.')} % else: - ${_('Enable evolve extension for this repository.')} + ${_('Enable Evolve and Topic extensions for this repository.')} % endif
diff --git a/rhodecode/tests/functional/test_delegated_admin.py b/rhodecode/tests/functional/test_delegated_admin.py --- a/rhodecode/tests/functional/test_delegated_admin.py +++ b/rhodecode/tests/functional/test_delegated_admin.py @@ -35,6 +35,8 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/repos', 'repo_groups': ADMIN_PREFIX + '/repo_groups', + 'repo_groups_data': + ADMIN_PREFIX + '/repo_groups_data', 'user_groups': ADMIN_PREFIX + '/user_groups', 'user_groups_data': @@ -63,8 +65,9 @@ class TestAdminDelegatedUser(TestControl response = self.app.get(route_path('repos'), status=200) response.mustcontain('data: []') - response = self.app.get(route_path('repo_groups'), status=200) - response.mustcontain('data: []') + response = self.app.get(route_path('repo_groups_data'), + status=200, extra_environ=xhr_header) + assert response.json['data'] == [] response = self.app.get(route_path('user_groups_data'), status=200, extra_environ=xhr_header) @@ -97,7 +100,8 @@ class TestAdminDelegatedUser(TestControl response = self.app.get(route_path('repos'), status=200) response.mustcontain('"name_raw": "{}"'.format(repo_name)) - response = self.app.get(route_path('repo_groups'), status=200) + response = self.app.get(route_path('repo_groups_data'), + extra_environ=xhr_header, status=200) response.mustcontain('"name_raw": "{}"'.format(repo_group_name)) response = self.app.get(route_path('user_groups_data'), @@ -139,7 +143,8 @@ class TestAdminDelegatedUser(TestControl response = self.app.get(route_path('repos'), status=200) response.mustcontain('"name_raw": "{}"'.format(repo_name)) - response = self.app.get(route_path('repo_groups'), status=200) + response = self.app.get(route_path('repo_groups_data'), + extra_environ=xhr_header, status=200) response.mustcontain('"name_raw": "{}"'.format(repo_group_name)) response = self.app.get(route_path('user_groups_data'), diff --git a/rhodecode/tests/models/settings/test_vcs_settings.py b/rhodecode/tests/models/settings/test_vcs_settings.py --- a/rhodecode/tests/models/settings/test_vcs_settings.py +++ b/rhodecode/tests/models/settings/test_vcs_settings.py @@ -501,8 +501,8 @@ class TestCreateOrUpdateUi(object): def test_update(self, repo_stub, settings_util): model = VcsSettingsModel(repo=repo_stub.repo_name) - - largefiles, phases, evolve = model.HG_SETTINGS + # care about only 3 first settings + largefiles, phases, evolve = model.HG_SETTINGS[:3] section = 'test-section' key = 'test-key' @@ -531,10 +531,11 @@ class TestCreateOrUpdateRepoHgSettings(o with mock.patch.object(model, '_create_or_update_ui') as create_mock: model.create_or_update_repo_hg_settings(self.FORM_DATA) expected_calls = [ - mock.call(model.repo_settings, 'extensions', 'largefiles', - active=False, value=''), - mock.call(model.repo_settings, 'extensions', 'evolve', - active=False, value=''), + mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''), + mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''), + mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''), + mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'), + mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''), mock.call(model.repo_settings, 'phases', 'publish', value='False'), ] assert expected_calls == create_mock.call_args_list @@ -589,17 +590,16 @@ class TestCreateOrUpdateGlobalHgSettings with mock.patch.object(model, '_create_or_update_ui') as create_mock: model.create_or_update_global_hg_settings(self.FORM_DATA) expected_calls = [ - mock.call(model.global_settings, 'extensions', 'largefiles', - active=False, value=''), - mock.call(model.global_settings, 'largefiles', 'usercache', - value='/example/largefiles-store'), - mock.call(model.global_settings, 'phases', 'publish', - value='False'), - mock.call(model.global_settings, 'extensions', 'hgsubversion', - active=False), - mock.call(model.global_settings, 'extensions', 'evolve', - active=False, value='') + mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''), + mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'), + mock.call(model.global_settings, 'phases', 'publish', value='False'), + mock.call(model.global_settings, 'extensions', 'hgsubversion', active=False), + mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''), + mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''), + mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'), + mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''), ] + assert expected_calls == create_mock.call_args_list @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys()) @@ -625,10 +625,8 @@ class TestCreateOrUpdateGlobalGitSetting with mock.patch.object(model, '_create_or_update_ui') as create_mock: model.create_or_update_global_git_settings(self.FORM_DATA) expected_calls = [ - mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', - active=False, value=False), - mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', - value='/example/lfs-store'), + mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False), + mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'), ] assert expected_calls == create_mock.call_args_list diff --git a/rhodecode/tests/models/test_pullrequest.py b/rhodecode/tests/models/test_pullrequest.py --- a/rhodecode/tests/models/test_pullrequest.py +++ b/rhodecode/tests/models/test_pullrequest.py @@ -512,7 +512,7 @@ def test_outdated_comments( (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES, 'This pull request cannot be merged because the source contains more branches than the target.'), (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS, - 'This pull request cannot be merged because the target has multiple heads: `a,b,c`.'), + 'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'), (MergeFailureReason.TARGET_IS_LOCKED, 'This pull request cannot be merged because the target repository is locked by user:123.'), (MergeFailureReason.MISSING_TARGET_REF,