##// END OF EJS Templates
merge: merged default changes into new-ui
marcink -
r3638:c1c37b0b merge new-ui
parent child Browse files
Show More
@@ -0,0 +1,41 b''
1 |RCE| 4.16.2 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2019-04-02
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30 Fixes
31 ^^^^^
32
33 - Integrations: fixed missing template variable for fork reference checks.
34 - Permissions: fixed server error when showing permissions for user groups.
35 - Pull requests: fixed a bug in removal of multiple reviewers at once.
36
37
38 Upgrade notes
39 ^^^^^^^^^^^^^
40
41 - Scheduled release addressing problems in 4.16.X releases.
@@ -0,0 +1,54 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4
5 from alembic.migration import MigrationContext
6 from alembic.operations import Operations
7 from sqlalchemy import String, Column
8 from sqlalchemy.sql import text
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import meta, init_model_encryption
12 from rhodecode.model.db import RepoGroup
13
14
15 log = logging.getLogger(__name__)
16
17
18 def upgrade(migrate_engine):
19 """
20 Upgrade operations go here.
21 Don't create your own engine; bind migrate_engine to your metadata
22 """
23 _reset_base(migrate_engine)
24 from rhodecode.lib.dbmigrate.schema import db_4_16_0_2
25
26 init_model_encryption(db_4_16_0_2)
27
28 context = MigrationContext.configure(migrate_engine.connect())
29 op = Operations(context)
30
31 repo_group = db_4_16_0_2.RepoGroup.__table__
32
33 with op.batch_alter_table(repo_group.name) as batch_op:
34 batch_op.add_column(
35 Column("repo_group_name_hash", String(1024), nullable=True, unique=False))
36
37 _generate_repo_group_name_hashes(db_4_16_0_2, op, meta.Session)
38
39
40 def downgrade(migrate_engine):
41 pass
42
43
44 def _generate_repo_group_name_hashes(models, op, session):
45 repo_groups = models.RepoGroup.get_all()
46 for repo_group in repo_groups:
47 print(repo_group.group_name)
48 hash_ = RepoGroup.hash_repo_group_name(repo_group.group_name)
49 params = {'hash': hash_, 'id': repo_group.group_id}
50 query = text(
51 'UPDATE groups SET repo_group_name_hash = :hash'
52 ' WHERE group_id = :id').bindparams(**params)
53 op.execute(query)
54 session().commit()
@@ -0,0 +1,39 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4
5 from alembic.migration import MigrationContext
6 from alembic.operations import Operations
7
8 from rhodecode.lib.dbmigrate.versions import _reset_base
9 from rhodecode.model import init_model_encryption
10
11
12 log = logging.getLogger(__name__)
13
14
15 def upgrade(migrate_engine):
16 """
17 Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
20 _reset_base(migrate_engine)
21 from rhodecode.lib.dbmigrate.schema import db_4_16_0_2
22
23 init_model_encryption(db_4_16_0_2)
24
25 context = MigrationContext.configure(migrate_engine.connect())
26 op = Operations(context)
27
28 repo_group = db_4_16_0_2.RepoGroup.__table__
29
30 with op.batch_alter_table(repo_group.name) as batch_op:
31 batch_op.alter_column("repo_group_name_hash", nullable=False)
32
33
34 def downgrade(migrate_engine):
35 pass
36
37
38 def _generate_repo_group_name_hashes(models, op, session):
39 pass
@@ -51,3 +51,4 b' 14502561d22e6b70613674cd675ae9a604b7989f'
51 51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.16.2.rst
12 13 release-notes-4.16.1.rst
13 14 release-notes-4.16.0.rst
14 15 release-notes-4.15.2.rst
@@ -5,7 +5,7 b''
5 5
6 6 self: super: {
7 7 "alembic" = super.buildPythonPackage {
8 name = "alembic-1.0.8";
8 name = "alembic-1.0.9";
9 9 doCheck = false;
10 10 propagatedBuildInputs = [
11 11 self."sqlalchemy"
@@ -14,8 +14,8 b' self: super: {'
14 14 self."python-dateutil"
15 15 ];
16 16 src = fetchurl {
17 url = "https://files.pythonhosted.org/packages/d6/bb/ec1e21f2e303689ad2170eb47fc67df9ad4199ade6759a99474c4d3535c8/alembic-1.0.8.tar.gz";
18 sha256 = "1s34i1j0dsxbflxligwhnkf37a5hvcshsv8ibkcfdjf03ph42pah";
17 url = "https://files.pythonhosted.org/packages/fc/42/8729e2491fa9b8eae160d1cbb429f61712bfc2d779816488c25cfdabf7b8/alembic-1.0.9.tar.gz";
18 sha256 = "0a88rwp7fp0y8ykczj82ivr4ww1kiflcvb882lgfl9azm8csdfa0";
19 19 };
20 20 meta = {
21 21 license = [ pkgs.lib.licenses.mit ];
@@ -310,11 +310,11 b' self: super: {'
310 310 };
311 311 };
312 312 "coverage" = super.buildPythonPackage {
313 name = "coverage-4.5.1";
313 name = "coverage-4.5.3";
314 314 doCheck = false;
315 315 src = fetchurl {
316 url = "https://files.pythonhosted.org/packages/35/fe/e7df7289d717426093c68d156e0fd9117c8f4872b6588e8a8928a0f68424/coverage-4.5.1.tar.gz";
317 sha256 = "1wbrzpxka3xd4nmmkc6q0ir343d91kymwsm8pbmwa0d2a7q4ir2n";
316 url = "https://files.pythonhosted.org/packages/82/70/2280b5b29a0352519bb95ab0ef1ea942d40466ca71c53a2085bdeff7b0eb/coverage-4.5.3.tar.gz";
317 sha256 = "02f6m073qdispn96rc616hg0rnmw1pgqzw3bgxwiwza4zf9hirlx";
318 318 };
319 319 meta = {
320 320 license = [ pkgs.lib.licenses.asl20 ];
@@ -1118,14 +1118,14 b' self: super: {'
1118 1118 };
1119 1119 };
1120 1120 "pexpect" = super.buildPythonPackage {
1121 name = "pexpect-4.6.0";
1121 name = "pexpect-4.7.0";
1122 1122 doCheck = false;
1123 1123 propagatedBuildInputs = [
1124 1124 self."ptyprocess"
1125 1125 ];
1126 1126 src = fetchurl {
1127 url = "https://files.pythonhosted.org/packages/89/43/07d07654ee3e25235d8cea4164cdee0ec39d1fda8e9203156ebe403ffda4/pexpect-4.6.0.tar.gz";
1128 sha256 = "1fla85g47iaxxpjhp9vkxdnv4pgc7rplfy6ja491smrrk0jqi3ia";
1127 url = "https://files.pythonhosted.org/packages/1c/b1/362a0d4235496cb42c33d1d8732b5e2c607b0129ad5fdd76f5a583b9fcb3/pexpect-4.7.0.tar.gz";
1128 sha256 = "1sv2rri15zwhds85a4kamwh9pj49qcxv7m4miyr4jfpfwv81yb4y";
1129 1129 };
1130 1130 meta = {
1131 1131 license = [ pkgs.lib.licenses.isc { fullName = "ISC License (ISCL)"; } ];
@@ -1160,15 +1160,15 b' self: super: {'
1160 1160 };
1161 1161 };
1162 1162 "plaster-pastedeploy" = super.buildPythonPackage {
1163 name = "plaster-pastedeploy-0.6";
1163 name = "plaster-pastedeploy-0.7";
1164 1164 doCheck = false;
1165 1165 propagatedBuildInputs = [
1166 1166 self."pastedeploy"
1167 1167 self."plaster"
1168 1168 ];
1169 1169 src = fetchurl {
1170 url = "https://files.pythonhosted.org/packages/3f/e7/6a6833158d2038ec40085433308a1e164fd1dac595513f6dd556d5669bb8/plaster_pastedeploy-0.6.tar.gz";
1171 sha256 = "1bkggk18f4z2bmsmxyxabvf62znvjwbivzh880419r3ap0616cf2";
1170 url = "https://files.pythonhosted.org/packages/99/69/2d3bc33091249266a1bd3cf24499e40ab31d54dffb4a7d76fe647950b98c/plaster_pastedeploy-0.7.tar.gz";
1171 sha256 = "1zg7gcsvc1kzay1ry5p699rg2qavfsxqwl17mqxzr0gzw6j9679r";
1172 1172 };
1173 1173 meta = {
1174 1174 license = [ pkgs.lib.licenses.mit ];
@@ -1186,15 +1186,15 b' self: super: {'
1186 1186 };
1187 1187 };
1188 1188 "prompt-toolkit" = super.buildPythonPackage {
1189 name = "prompt-toolkit-1.0.15";
1189 name = "prompt-toolkit-1.0.16";
1190 1190 doCheck = false;
1191 1191 propagatedBuildInputs = [
1192 1192 self."six"
1193 1193 self."wcwidth"
1194 1194 ];
1195 1195 src = fetchurl {
1196 url = "https://files.pythonhosted.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz";
1197 sha256 = "05v9h5nydljwpj5nm8n804ms0glajwfy1zagrzqrg91wk3qqi1c5";
1196 url = "https://files.pythonhosted.org/packages/f1/03/bb36771dc9fa7553ac4bdc639a9ecdf6fda0ff4176faf940d97e3c16e41d/prompt_toolkit-1.0.16.tar.gz";
1197 sha256 = "1d65hm6nf0cbq0q0121m60zzy4s1fpg9fn761s1yxf08dridvkn1";
1198 1198 };
1199 1199 meta = {
1200 1200 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1212,11 +1212,11 b' self: super: {'
1212 1212 };
1213 1213 };
1214 1214 "psycopg2" = super.buildPythonPackage {
1215 name = "psycopg2-2.7.7";
1215 name = "psycopg2-2.8.2";
1216 1216 doCheck = false;
1217 1217 src = fetchurl {
1218 url = "https://files.pythonhosted.org/packages/63/54/c039eb0f46f9a9406b59a638415c2012ad7be9b4b97bfddb1f48c280df3a/psycopg2-2.7.7.tar.gz";
1219 sha256 = "0zjbabb4qjx9dm07imhf8y5a9rpa06d5zah80myiimgdi83nslpl";
1218 url = "https://files.pythonhosted.org/packages/23/7e/93c325482c328619870b6cd09370f6dbe1148283daca65115cd63642e60f/psycopg2-2.8.2.tar.gz";
1219 sha256 = "122mn2z3r0zgs8jyq682jjjr6vq5690qmxqf22gj6g41dwdz5b2w";
1220 1220 };
1221 1221 meta = {
1222 1222 license = [ pkgs.lib.licenses.zpl21 { fullName = "GNU Library or Lesser General Public License (LGPL)"; } { fullName = "LGPL with exceptions or ZPL"; } ];
@@ -1373,7 +1373,7 b' self: super: {'
1373 1373 };
1374 1374 };
1375 1375 "pyramid" = super.buildPythonPackage {
1376 name = "pyramid-1.10.2";
1376 name = "pyramid-1.10.4";
1377 1377 doCheck = false;
1378 1378 propagatedBuildInputs = [
1379 1379 self."hupper"
@@ -1388,8 +1388,8 b' self: super: {'
1388 1388 self."repoze.lru"
1389 1389 ];
1390 1390 src = fetchurl {
1391 url = "https://files.pythonhosted.org/packages/bc/0e/73de9b189ff00a963beeedaff90e27b134eedf2806279a1a3fe122fd65b6/pyramid-1.10.2.tar.gz";
1392 sha256 = "0gn6sw6ml67ir150ffivc0ad5hd448p43p9z2bkyp12jh2n9n2p7";
1391 url = "https://files.pythonhosted.org/packages/c2/43/1ae701c9c6bb3a434358e678a5e72c96e8aa55cf4cb1d2fa2041b5dd38b7/pyramid-1.10.4.tar.gz";
1392 sha256 = "0rkxs1ajycg2zh1c94xlmls56mx5m161sn8112skj0amza6cn36q";
1393 1393 };
1394 1394 meta = {
1395 1395 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -1931,11 +1931,11 b' self: super: {'
1931 1931 };
1932 1932 };
1933 1933 "setuptools" = super.buildPythonPackage {
1934 name = "setuptools-40.8.0";
1934 name = "setuptools-41.0.0";
1935 1935 doCheck = false;
1936 1936 src = fetchurl {
1937 url = "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip";
1938 sha256 = "0k9hifpgahnw2a26w3cr346iy733k6d3nwh3f7g9m13y6f8fqkkf";
1937 url = "https://files.pythonhosted.org/packages/ed/69/c805067de1feedbb98c53174b0f2df44cc05e0e9ee73bb85eebc59e508c6/setuptools-41.0.0.zip";
1938 sha256 = "1cfwy2g23qj3262ivj0b1182lgwz7bqqbka35rkqwypynra05lvr";
1939 1939 };
1940 1940 meta = {
1941 1941 license = [ pkgs.lib.licenses.mit ];
@@ -2012,14 +2012,14 b' self: super: {'
2012 2012 };
2013 2013 };
2014 2014 "supervisor" = super.buildPythonPackage {
2015 name = "supervisor-3.3.5";
2015 name = "supervisor-4.0.1";
2016 2016 doCheck = false;
2017 2017 propagatedBuildInputs = [
2018 2018 self."meld3"
2019 2019 ];
2020 2020 src = fetchurl {
2021 url = "https://files.pythonhosted.org/packages/ba/65/92575a8757ed576beaee59251f64a3287bde82bdc03964b89df9e1d29e1b/supervisor-3.3.5.tar.gz";
2022 sha256 = "1w3ahridzbc6rxfpbyx8lij6pjlcgf2ymzyg53llkjqxalp6sk8v";
2021 url = "https://files.pythonhosted.org/packages/96/ec/f8190beeb0c6d29a30aea10389c11d0164b6ff221931ee84093315ecde6a/supervisor-4.0.1.tar.gz";
2022 sha256 = "10l3z7v6v1fyv7m5zbazzxciwvli2n9a41pxi27p4kixgsfp0s1j";
2023 2023 };
2024 2024 meta = {
2025 2025 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -47,7 +47,7 b' pyparsing==2.3.0'
47 47 pyramid-beaker==0.8
48 48 pyramid-debugtoolbar==4.5.0
49 49 pyramid-mako==1.0.2
50 pyramid==1.10.2
50 pyramid==1.10.4
51 51 pyramid_mailer==0.15.1
52 52 python-dateutil
53 53 python-ldap==3.1.0
@@ -67,7 +67,7 b' six==1.11.0'
67 67 sqlalchemy==1.1.18
68 68 sshpubkeys==3.1.0
69 69 subprocess32==3.5.3
70 supervisor==3.3.5
70 supervisor==4.0.1
71 71 translationstring==1.3
72 72 urllib3==1.24.1
73 73 urlobject==2.4.3
@@ -87,7 +87,7 b' zope.interface==4.6.0'
87 87 mysql-python==1.2.5
88 88 pymysql==0.8.1
89 89 pysqlite==2.8.3
90 psycopg2==2.7.7
90 psycopg2==2.8.2
91 91
92 92 # IPYTHON RENDERING
93 93 # entrypoints backport, pypi version doesn't support egg installs
@@ -97,7 +97,7 b' nbformat==4.4.0'
97 97 jupyter_client==5.0.0
98 98
99 99 ## cli tools
100 alembic==1.0.8
100 alembic==1.0.9
101 101 invoke==0.13.0
102 102 bumpversion==0.5.3
103 103
@@ -10,7 +10,7 b' gprof2dot==2017.9.19'
10 10
11 11 mock==1.0.1
12 12 cov-core==1.15.0
13 coverage==4.5.1
13 coverage==4.5.3
14 14
15 15 webtest==2.0.33
16 16 beautifulsoup4==4.6.3
@@ -45,7 +45,7 b' PYRAMID_SETTINGS = {}'
45 45 EXTENSIONS = {}
46 46
47 47 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
48 __dbversion__ = 95 # defines current db version for migrations
48 __dbversion__ = 97 # defines current db version for migrations
49 49 __platform__ = platform.system()
50 50 __license__ = 'AGPLv3, and Commercial License'
51 51 __author__ = 'RhodeCode GmbH'
@@ -424,6 +424,10 b' def admin_routes(config):'
424 424 pattern='/repo_groups')
425 425
426 426 config.add_route(
427 name='repo_groups_data',
428 pattern='/repo_groups_data')
429
430 config.add_route(
427 431 name='repo_group_new',
428 432 pattern='/repo_group/new')
429 433
@@ -23,11 +23,11 b' import pytest'
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import Repository, UserRepoToPerm, User
26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.repo_group import RepoGroupModel
29 29 from rhodecode.tests import (
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController)
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
31 31 from rhodecode.tests.fixture import Fixture
32 32
33 33 fixture = Fixture()
@@ -38,6 +38,7 b' def route_path(name, params=None, **kwar'
38 38
39 39 base_url = {
40 40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
41 42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
42 43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
43 44
@@ -59,13 +60,30 b' def _get_permission_for_user(user, repo)'
59 60
60 61 @pytest.mark.usefixtures("app")
61 62 class TestAdminRepositoryGroups(object):
63
62 64 def test_show_repo_groups(self, autologin_user):
63 response = self.app.get(route_path('repo_groups'))
64 response.mustcontain('data: []')
65 self.app.get(route_path('repo_groups'))
66
67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 response = self.app.get(route_path(
69 'repo_groups_data'), extra_environ=xhr_header)
70
71 all_repo_groups = RepoGroup.query().count()
72 assert response.json['recordsTotal'] == all_repo_groups
65 73
66 def test_show_repo_groups_after_creating_group(self, autologin_user):
74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 response = self.app.get(route_path(
76 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 extra_environ=xhr_header)
78
79 all_repo_groups = RepoGroup.query().count()
80 assert response.json['recordsTotal'] == all_repo_groups
81 assert response.json['recordsFiltered'] == 0
82
83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
67 84 fixture.create_repo_group('test_repo_group')
68 response = self.app.get(route_path('repo_groups'))
85 response = self.app.get(route_path(
86 'repo_groups_data'), extra_environ=xhr_header)
69 87 response.mustcontain('"name_raw": "test_repo_group"')
70 88 fixture.destroy_repo_group('test_repo_group')
71 89
@@ -17,7 +17,7 b''
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 import datetime
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
@@ -30,16 +30,16 b' from pyramid.response import Response'
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32
33 from rhodecode.lib.ext_json import json
34 33 from rhodecode.lib.auth import (
35 34 LoginRequired, CSRFRequired, NotAnonymous,
36 35 HasPermissionAny, HasRepoGroupPermissionAny)
37 36 from rhodecode.lib import helpers as h, audit_logger
38 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
39 38 from rhodecode.model.forms import RepoGroupForm
40 39 from rhodecode.model.repo_group import RepoGroupModel
41 40 from rhodecode.model.scm import RepoGroupList
42 from rhodecode.model.db import Session, RepoGroup
41 from rhodecode.model.db import (
42 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
43 43
44 44 log = logging.getLogger(__name__)
45 45
@@ -88,22 +88,168 b' class AdminRepoGroupsView(BaseAppView, D'
88 88 return False
89 89 return False
90 90
91 # permission check in data loading of
92 # `repo_group_list_data` via RepoGroupList
91 93 @LoginRequired()
92 94 @NotAnonymous()
93 # perms check inside
94 95 @view_config(
95 96 route_name='repo_groups', request_method='GET',
96 97 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
97 98 def repo_group_list(self):
98 99 c = self.load_default_context()
100 return self._get_template_context(c)
99 101
100 repo_group_list = RepoGroup.get_all_repo_groups()
101 repo_group_list_acl = RepoGroupList(
102 repo_group_list, perm_set=['group.admin'])
103 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
104 repo_group_list=repo_group_list_acl, admin=True)
105 c.data = json.dumps(repo_group_data)
106 return self._get_template_context(c)
102 # permission check inside
103 @LoginRequired()
104 @NotAnonymous()
105 @view_config(
106 route_name='repo_groups_data', request_method='GET',
107 renderer='json_ext', xhr=True)
108 def repo_group_list_data(self):
109 self.load_default_context()
110 column_map = {
111 'name_raw': 'group_name_hash',
112 'desc': 'group_description',
113 'last_change_raw': 'updated_on',
114 'top_level_repos': 'repos_total',
115 'owner': 'user_username',
116 }
117 draw, start, limit = self._extract_chunk(self.request)
118 search_q, order_by, order_dir = self._extract_ordering(
119 self.request, column_map=column_map)
120
121 _render = self.request.get_partial_renderer(
122 'rhodecode:templates/data_table/_dt_elements.mako')
123 c = _render.get_call_context()
124
125 def quick_menu(repo_group_name):
126 return _render('quick_repo_group_menu', repo_group_name)
127
128 def repo_group_lnk(repo_group_name):
129 return _render('repo_group_name', repo_group_name)
130
131 def last_change(last_change):
132 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
133 delta = datetime.timedelta(
134 seconds=(datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
135 last_change = last_change + delta
136 return _render("last_change", last_change)
137
138 def desc(desc, personal):
139 return _render(
140 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
141
142 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
143 return _render(
144 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
145
146 def user_profile(username):
147 return _render('user_profile', username)
148
149 auth_repo_group_list = RepoGroupList(
150 RepoGroup.query().all(), perm_set=['group.admin'])
151
152 allowed_ids = [-1]
153 for repo_group in auth_repo_group_list:
154 allowed_ids.append(repo_group.group_id)
155
156 repo_groups_data_total_count = RepoGroup.query()\
157 .filter(or_(
158 # generate multiple IN to fix limitation problems
159 *in_filter_generator(RepoGroup.group_id, allowed_ids)
160 )) \
161 .count()
162
163 repo_groups_data_total_inactive_count = RepoGroup.query()\
164 .filter(RepoGroup.group_id.in_(allowed_ids))\
165 .count()
166
167 repo_count = count(Repository.repo_id)
168 base_q = Session.query(
169 RepoGroup.group_name,
170 RepoGroup.group_name_hash,
171 RepoGroup.group_description,
172 RepoGroup.group_id,
173 RepoGroup.personal,
174 RepoGroup.updated_on,
175 User,
176 repo_count.label('repos_count')
177 ) \
178 .filter(or_(
179 # generate multiple IN to fix limitation problems
180 *in_filter_generator(RepoGroup.group_id, allowed_ids)
181 )) \
182 .outerjoin(Repository) \
183 .join(User, User.user_id == RepoGroup.user_id) \
184 .group_by(RepoGroup, User)
185
186 if search_q:
187 like_expression = u'%{}%'.format(safe_unicode(search_q))
188 base_q = base_q.filter(or_(
189 RepoGroup.group_name.ilike(like_expression),
190 ))
191
192 repo_groups_data_total_filtered_count = base_q.count()
193 # the inactive isn't really used, but we still make it same as other data grids
194 # which use inactive (users,user groups)
195 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
196
197 sort_defined = False
198 if order_by == 'group_name':
199 sort_col = func.lower(RepoGroup.group_name)
200 sort_defined = True
201 elif order_by == 'repos_total':
202 sort_col = repo_count
203 sort_defined = True
204 elif order_by == 'user_username':
205 sort_col = User.username
206 else:
207 sort_col = getattr(RepoGroup, order_by, None)
208
209 if sort_defined or sort_col:
210 if order_dir == 'asc':
211 sort_col = sort_col.asc()
212 else:
213 sort_col = sort_col.desc()
214
215 base_q = base_q.order_by(sort_col)
216 base_q = base_q.offset(start).limit(limit)
217
218 # authenticated access to user groups
219 auth_repo_group_list = base_q.all()
220
221 repo_groups_data = []
222 for repo_gr in auth_repo_group_list:
223 row = {
224 "menu": quick_menu(repo_gr.group_name),
225 "name": repo_group_lnk(repo_gr.group_name),
226 "name_raw": repo_gr.group_name,
227 "last_change": last_change(repo_gr.updated_on),
228 "last_change_raw": datetime_to_time(repo_gr.updated_on),
229
230 "last_changeset": "",
231 "last_changeset_raw": "",
232
233 "desc": desc(repo_gr.group_description, repo_gr.personal),
234 "owner": user_profile(repo_gr.User.username),
235 "top_level_repos": repo_gr.repos_count,
236 "action": repo_group_actions(
237 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
238
239 }
240
241 repo_groups_data.append(row)
242
243 data = ({
244 'draw': draw,
245 'data': repo_groups_data,
246 'recordsTotal': repo_groups_data_total_count,
247 'recordsTotalInactive': repo_groups_data_total_inactive_count,
248 'recordsFiltered': repo_groups_data_total_filtered_count,
249 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
250 })
251
252 return data
107 253
108 254 @LoginRequired()
109 255 @NotAnonymous()
@@ -39,7 +39,7 b' from rhodecode.model.forms import UserGr'
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.scm import UserGroupList
41 41 from rhodecode.model.db import (
42 or_, count, User, UserGroup, UserGroupMember)
42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.user_group import UserGroupModel
45 45 from rhodecode.model.db import true
@@ -107,11 +107,17 b' class AdminUserGroupsView(BaseAppView, D'
107 107 allowed_ids.append(user_group.users_group_id)
108 108
109 109 user_groups_data_total_count = UserGroup.query()\
110 .filter(UserGroup.users_group_id.in_(allowed_ids))\
110 .filter(or_(
111 # generate multiple IN to fix limitation problems
112 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
113 ))\
111 114 .count()
112 115
113 116 user_groups_data_total_inactive_count = UserGroup.query()\
114 .filter(UserGroup.users_group_id.in_(allowed_ids))\
117 .filter(or_(
118 # generate multiple IN to fix limitation problems
119 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
120 ))\
115 121 .filter(UserGroup.users_group_active != true()).count()
116 122
117 123 member_count = count(UserGroupMember.user_id)
@@ -123,11 +129,14 b' class AdminUserGroupsView(BaseAppView, D'
123 129 UserGroup.group_data,
124 130 User,
125 131 member_count.label('member_count')
126 ) \
127 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
128 .outerjoin(UserGroupMember) \
129 .join(User, User.user_id == UserGroup.user_id) \
130 .group_by(UserGroup, User)
132 ) \
133 .filter(or_(
134 # generate multiple IN to fix limitation problems
135 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
136 )) \
137 .outerjoin(UserGroupMember) \
138 .join(User, User.user_id == UserGroup.user_id) \
139 .group_by(UserGroup, User)
131 140
132 141 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
133 142
@@ -141,14 +150,16 b' class AdminUserGroupsView(BaseAppView, D'
141 150 user_groups_data_total_filtered_count = base_q.count()
142 151 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
143 152
153 sort_defined = False
144 154 if order_by == 'members_total':
145 155 sort_col = member_count
156 sort_defined = True
146 157 elif order_by == 'user_username':
147 158 sort_col = User.username
148 159 else:
149 160 sort_col = getattr(UserGroup, order_by, None)
150 161
151 if isinstance(sort_col, count) or sort_col:
162 if sort_defined or sort_col:
152 163 if order_dir == 'asc':
153 164 sort_col = sort_col.asc()
154 165 else:
@@ -162,7 +173,7 b' class AdminUserGroupsView(BaseAppView, D'
162 173
163 174 user_groups_data = []
164 175 for user_gr in auth_user_group_list:
165 user_groups_data.append({
176 row = {
166 177 "users_group_name": user_group_name(user_gr.users_group_name),
167 178 "name_raw": h.escape(user_gr.users_group_name),
168 179 "description": h.escape(user_gr.user_group_description),
@@ -175,7 +186,8 b' class AdminUserGroupsView(BaseAppView, D'
175 186 "owner": user_profile(user_gr.User.username),
176 187 "action": user_group_actions(
177 188 user_gr.users_group_id, user_gr.users_group_name)
178 })
189 }
190 user_groups_data.append(row)
179 191
180 192 data = ({
181 193 'draw': draw,
@@ -67,12 +67,6 b' class RepoSettingsView(RepoAppView):'
67 67 .filter(UserFollowing.user_id == c.default_user_id) \
68 68 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
69 69
70 c.has_origin_repo_read_perm = False
71 if self.db_repo.fork:
72 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
73 'repository.write', 'repository.read', 'repository.admin')(
74 self.db_repo.fork.repo_name, 'repo set as fork page')
75
76 70 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
77 71
78 72 return self._get_template_context(c)
@@ -20,12 +20,13 b''
20 20
21 21 import os
22 22 import sys
23 import shutil
24 23 import logging
25 24 import tempfile
26 25 import textwrap
27
26 import collections
28 27 from .base import VcsServer
28 from rhodecode.model.db import RhodeCodeUi
29 from rhodecode.model.settings import VcsSettingsModel
29 30
30 31 log = logging.getLogger(__name__)
31 32
@@ -37,62 +38,46 b' class MercurialTunnelWrapper(object):'
37 38 self.server = server
38 39 self.stdin = sys.stdin
39 40 self.stdout = sys.stdout
40 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
41 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
42 42
43 43 def create_hooks_env(self):
44 repo_name = self.server.repo_name
45 hg_flags = self.config_to_hgrc(repo_name)
44 46
45 47 content = textwrap.dedent(
46 48 '''
47 # SSH hooks version=1.0.0
48 [hooks]
49 pretxnchangegroup.ssh_auth=python:vcsserver.hooks.pre_push_ssh_auth
50 pretxnchangegroup.ssh=python:vcsserver.hooks.pre_push_ssh
51 changegroup.ssh=python:vcsserver.hooks.post_push_ssh
52
53 preoutgoing.ssh=python:vcsserver.hooks.pre_pull_ssh
54 outgoing.ssh=python:vcsserver.hooks.post_pull_ssh
49 # RhodeCode SSH hooks version=2.0.0
50 {custom}
51 '''
52 ).format(custom='\n'.join(hg_flags))
55 53
56 '''
57 )
54 root = self.server.get_root_store()
55 hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
56 hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
58 57
58 # cleanup custom hgrc file
59 if os.path.isfile(hgrc_custom):
60 with open(hgrc_custom, 'wb') as f:
61 f.write('')
62 log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
63
64 # write temp
59 65 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
60 66 hooks_env_file.write(content)
61 root = self.server.get_root_store()
62 67
63 hgrc_custom = os.path.join(
64 root, self.server.repo_name, '.hg', 'hgrc_rhodecode')
65 log.debug('Wrote custom hgrc file under %s', hgrc_custom)
66 shutil.move(
67 self.hooks_env_path, hgrc_custom)
68
69 hgrc_main = os.path.join(
70 root, self.server.repo_name, '.hg', 'hgrc')
71 include_marker = '%include hgrc_rhodecode'
68 return self.hooks_env_path
72 69
73 if not os.path.isfile(hgrc_main):
74 os.mknod(hgrc_main)
75
76 with open(hgrc_main, 'rb') as f:
77 data = f.read()
78 has_marker = include_marker in data
70 def remove_configs(self):
71 os.remove(self.hooks_env_path)
79 72
80 if not has_marker:
81 log.debug('Adding include marker for hooks')
82 with open(hgrc_main, 'wa') as f:
83 f.write(textwrap.dedent('''
84 # added by RhodeCode
85 {}
86 '''.format(include_marker)))
87
88 def command(self):
73 def command(self, hgrc_path):
89 74 root = self.server.get_root_store()
90 75
91 76 command = (
92 "cd {root}; {hg_path} -R {root}{repo_name} "
77 "cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
93 78 "serve --stdio".format(
94 79 root=root, hg_path=self.server.hg_path,
95 repo_name=self.server.repo_name))
80 repo_name=self.server.repo_name, hgrc=hgrc_path))
96 81 log.debug("Final CMD: %s", command)
97 82 return command
98 83
@@ -102,22 +87,61 b' class MercurialTunnelWrapper(object):'
102 87 action = '?'
103 88 # permissions are check via `pre_push_ssh_auth` hook
104 89 self.server.update_environment(action=action, extras=extras)
105 self.create_hooks_env()
106 return os.system(self.command())
90 custom_hgrc_file = self.create_hooks_env()
91
92 try:
93 return os.system(self.command(custom_hgrc_file))
94 finally:
95 self.remove_configs()
107 96
108 97
109 98 class MercurialServer(VcsServer):
110 99 backend = 'hg'
100 cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
111 101
112 def __init__(self, store, ini_path, repo_name,
113 user, user_permissions, config, env):
114 super(MercurialServer, self).\
115 __init__(user, user_permissions, config, env)
102 def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
103 super(MercurialServer, self).__init__(user, user_permissions, config, env)
116 104
117 105 self.store = store
118 106 self.ini_path = ini_path
119 107 self.repo_name = repo_name
120 self._path = self.hg_path = config.get(
121 'app:main', 'ssh.executable.hg')
108 self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
109 self.tunnel = MercurialTunnelWrapper(server=self)
110
111 def config_to_hgrc(self, repo_name):
112 ui_sections = collections.defaultdict(list)
113 ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
114
115 # write default hooks
116 default_hooks = [
117 ('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
118 ('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
119 ('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
120
121 ('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
122 ('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
123 ]
124
125 for k, v in default_hooks:
126 ui_sections['hooks'].append((k, v))
122 127
123 self.tunnel = MercurialTunnelWrapper(server=self)
128 for entry in ui:
129 if not entry.active:
130 continue
131 sec = entry.section
132 key = entry.key
133
134 if sec in self.cli_flags:
135 # we want only custom hooks, so we skip builtins
136 if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
137 continue
138
139 ui_sections[sec].append([key, entry.value])
140
141 flags = []
142 for _sec, key_val in ui_sections.items():
143 flags.append(' ')
144 flags.append('[{}]'.format(_sec))
145 for key, val in key_val:
146 flags.append('{}= {}'.format(key, val))
147 return flags
@@ -18,6 +18,7 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import os
21 22 import mock
22 23 import pytest
23 24
@@ -68,14 +69,16 b' def hg_server(app):'
68 69
69 70 class TestMercurialServer(object):
70 71
71 def test_command(self, hg_server):
72 def test_command(self, hg_server, tmpdir):
72 73 server = hg_server.create()
74 custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
73 75 expected_command = (
74 'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'.format(
75 root=hg_server.root, hg_path=hg_server.hg_path,
76 'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
77 root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
76 78 repo_name=hg_server.repo_name)
77 79 )
78 assert expected_command == server.tunnel.command()
80 server_command = server.tunnel.command(custom_hgrc)
81 assert expected_command == server_command
79 82
80 83 @pytest.mark.parametrize('permissions, action, code', [
81 84 ({}, 'pull', -2),
@@ -114,7 +114,6 b' class IntegrationSettingsViewBase(BaseAp'
114 114 _ = self.request.translate
115 115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
116 116 include_app_defaults=include_app_defaults)
117
118 117 c.active = 'integrations'
119 118
120 119 return c
@@ -404,6 +403,11 b' class RepoIntegrationsView(IntegrationSe'
404 403 c.repo_name = self.db_repo.repo_name
405 404 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
406 405
406 c.has_origin_repo_read_perm = False
407 if self.db_repo.fork:
408 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
409 'repository.write', 'repository.read', 'repository.admin')(
410 self.db_repo.fork.repo_name, 'summary fork link')
407 411 return c
408 412
409 413 @LoginRequired()
@@ -375,6 +375,27 b' class DbManage(object):'
375 375 hgevolve.ui_active = False
376 376 self.sa.add(hgevolve)
377 377
378 hgevolve = RhodeCodeUi()
379 hgevolve.ui_section = 'experimental'
380 hgevolve.ui_key = 'evolution'
381 hgevolve.ui_value = ''
382 hgevolve.ui_active = False
383 self.sa.add(hgevolve)
384
385 hgevolve = RhodeCodeUi()
386 hgevolve.ui_section = 'experimental'
387 hgevolve.ui_key = 'evolution.exchange'
388 hgevolve.ui_value = ''
389 hgevolve.ui_active = False
390 self.sa.add(hgevolve)
391
392 hgevolve = RhodeCodeUi()
393 hgevolve.ui_section = 'extensions'
394 hgevolve.ui_key = 'topic'
395 hgevolve.ui_value = ''
396 hgevolve.ui_active = False
397 self.sa.add(hgevolve)
398
378 399 # enable hggit disabled by default
379 400 hggit = RhodeCodeUi()
380 401 hggit.ui_section = 'extensions'
@@ -80,7 +80,7 b' def trigger_log_create_pull_request_hook'
80 80 extras = _get_rc_scm_extras(username, repo_name, repo_alias,
81 81 'create_pull_request')
82 82 events.trigger(events.PullRequestCreateEvent(pull_request))
83 extras.update(pull_request.get_api_data())
83 extras.update(pull_request.get_api_data(with_merge_state=False))
84 84 hooks_base.log_create_pull_request(**extras)
85 85
86 86
@@ -371,7 +371,8 b' def config_data_from_db(clear_session=Tr'
371 371 config.append((
372 372 safe_str(setting.section), safe_str(setting.key), False))
373 373 log.debug(
374 'settings ui from db: %s',
374 'settings ui from db@repo[%s]: %s',
375 repo,
375 376 ','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data)))
376 377 if clear_session:
377 378 meta.Session.remove()
@@ -161,7 +161,7 b' class MergeResponse(object):'
161 161 u'This pull request cannot be merged because the source contains '
162 162 u'more branches than the target.'),
163 163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
164 u'This pull request cannot be merged because the target '
164 u'This pull request cannot be merged because the target `{target_ref.name}` '
165 165 u'has multiple heads: `{heads}`.'),
166 166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
167 167 u'This pull request cannot be merged because the target repository is '
@@ -309,6 +309,9 b' class BaseRepository(object):'
309 309 def _remote(self):
310 310 raise NotImplementedError
311 311
312 def _heads(self, branch=None):
313 return []
314
312 315 @LazyProperty
313 316 def EMPTY_COMMIT(self):
314 317 return EmptyCommit(self.EMPTY_COMMIT_ID)
@@ -715,11 +715,16 b' class MercurialRepository(BaseRepository'
715 715
716 716 try:
717 717 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
718 heads = ','.join(self._heads(target_ref.name))
718 heads = '\n,'.join(self._heads(target_ref.name))
719 metadata = {
720 'target_ref': target_ref,
721 'source_ref': source_ref,
722 'heads': heads
723 }
719 724 return MergeResponse(
720 725 False, False, None,
721 726 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
722 metadata={'heads': heads})
727 metadata=metadata)
723 728 except CommitDoesNotExistError:
724 729 log.exception('Failure when looking up branch heads on hg target')
725 730 return MergeResponse(
@@ -25,6 +25,7 b' Database Models for RhodeCode Enterprise'
25 25 import re
26 26 import os
27 27 import time
28 import string
28 29 import hashlib
29 30 import logging
30 31 import datetime
@@ -50,6 +51,7 b' from sqlalchemy.dialects.mysql import LO'
50 51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 52 from pyramid import compat
52 53 from pyramid.threadlocal import get_current_request
54 from webhelpers.text import collapse, remove_formatting
53 55
54 56 from rhodecode.translation import _
55 57 from rhodecode.lib.vcs import get_vcs_instance
@@ -409,6 +411,15 b' class RhodeCodeUi(Base, BaseModel):'
409 411 HOOK_PUSH = 'changegroup.push_logger'
410 412 HOOK_PUSH_KEY = 'pushkey.key_push'
411 413
414 HOOKS_BUILTIN = [
415 HOOK_PRE_PULL,
416 HOOK_PULL,
417 HOOK_PRE_PUSH,
418 HOOK_PRETX_PUSH,
419 HOOK_PUSH,
420 HOOK_PUSH_KEY,
421 ]
422
412 423 # TODO: johbo: Unify way how hooks are configured for git and hg,
413 424 # git part is currently hardcoded.
414 425
@@ -2469,7 +2480,8 b' class RepoGroup(Base, BaseModel):'
2469 2480 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2470 2481
2471 2482 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2472 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2483 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2484 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2473 2485 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2474 2486 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2475 2487 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
@@ -2492,6 +2504,15 b' class RepoGroup(Base, BaseModel):'
2492 2504 return u"<%s('id:%s:%s')>" % (
2493 2505 self.__class__.__name__, self.group_id, self.group_name)
2494 2506
2507 @hybrid_property
2508 def group_name(self):
2509 return self._group_name
2510
2511 @group_name.setter
2512 def group_name(self, value):
2513 self._group_name = value
2514 self.group_name_hash = self.hash_repo_group_name(value)
2515
2495 2516 @validates('group_parent_id')
2496 2517 def validate_group_parent_id(self, key, val):
2497 2518 """
@@ -2508,6 +2529,18 b' class RepoGroup(Base, BaseModel):'
2508 2529 return h.escape(self.group_description)
2509 2530
2510 2531 @classmethod
2532 def hash_repo_group_name(cls, repo_group_name):
2533 val = remove_formatting(repo_group_name)
2534 val = safe_str(val).lower()
2535 chars = []
2536 for c in val:
2537 if c not in string.ascii_letters:
2538 c = str(ord(c))
2539 chars.append(c)
2540
2541 return ''.join(chars)
2542
2543 @classmethod
2511 2544 def _generate_choice(cls, repo_group):
2512 2545 from webhelpers.html import literal as _literal
2513 2546 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
@@ -2770,6 +2803,13 b' class RepoGroup(Base, BaseModel):'
2770 2803 }
2771 2804 return data
2772 2805
2806 def get_dict(self):
2807 # Since we transformed `group_name` to a hybrid property, we need to
2808 # keep compatibility with the code which uses `group_name` field.
2809 result = super(RepoGroup, self).get_dict()
2810 result['group_name'] = result.pop('_group_name', None)
2811 return result
2812
2773 2813
2774 2814 class Permission(Base, BaseModel):
2775 2815 __tablename__ = 'permissions'
@@ -1020,6 +1020,9 b' class PullRequestModel(BaseModel):'
1020 1020 log.debug("Adding %s reviewers", ids_to_add)
1021 1021 log.debug("Removing %s reviewers", ids_to_remove)
1022 1022 changed = False
1023 added_audit_reviewers = []
1024 removed_audit_reviewers = []
1025
1023 1026 for uid in ids_to_add:
1024 1027 changed = True
1025 1028 _usr = self._get_user(uid)
@@ -1030,29 +1033,37 b' class PullRequestModel(BaseModel):'
1030 1033 # NOTE(marcink): mandatory shouldn't be changed now
1031 1034 # reviewer.mandatory = reviewers[uid]['reasons']
1032 1035 Session().add(reviewer)
1033 self._log_audit_action(
1034 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1035 user, pull_request)
1036 added_audit_reviewers.append(reviewer.get_dict())
1036 1037
1037 1038 for uid in ids_to_remove:
1038 1039 changed = True
1040 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1041 # that prevents and fixes cases that we added the same reviewer twice.
1042 # this CAN happen due to the lack of DB checks
1039 1043 reviewers = PullRequestReviewers.query()\
1040 1044 .filter(PullRequestReviewers.user_id == uid,
1041 1045 PullRequestReviewers.pull_request == pull_request)\
1042 1046 .all()
1043 # use .all() in case we accidentally added the same person twice
1044 # this CAN happen due to the lack of DB checks
1047
1045 1048 for obj in reviewers:
1046 old_data = obj.get_dict()
1049 added_audit_reviewers.append(obj.get_dict())
1047 1050 Session().delete(obj)
1048 self._log_audit_action(
1049 'repo.pull_request.reviewer.delete',
1050 {'old_data': old_data}, user, pull_request)
1051 1051
1052 1052 if changed:
1053 Session().expire_all()
1053 1054 pull_request.updated_on = datetime.datetime.now()
1054 1055 Session().add(pull_request)
1055 1056
1057 # finally store audit logs
1058 for user_data in added_audit_reviewers:
1059 self._log_audit_action(
1060 'repo.pull_request.reviewer.add', {'data': user_data},
1061 user, pull_request)
1062 for user_data in removed_audit_reviewers:
1063 self._log_audit_action(
1064 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1065 user, pull_request)
1066
1056 1067 self.notify_reviewers(pull_request, ids_to_add)
1057 1068 return ids_to_add, ids_to_remove
1058 1069
@@ -1306,8 +1317,16 b' class PullRequestModel(BaseModel):'
1306 1317 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1307 1318 metadata = {
1308 1319 'target_ref': pull_request.target_ref_parts,
1309 'source_ref': pull_request.source_ref_parts
1320 'source_ref': pull_request.source_ref_parts,
1310 1321 }
1322 if not possible and target_ref.type == 'branch':
1323 # NOTE(marcink): case for mercurial multiple heads on branch
1324 heads = target_vcs._heads(target_ref.name)
1325 if len(heads) != 1:
1326 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1327 metadata.update({
1328 'heads': heads
1329 })
1311 1330 merge_state = MergeResponse(
1312 1331 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1313 1332
@@ -119,6 +119,7 b' class SettingsModel(BaseModel):'
119 119 new_ui.ui_value = val
120 120 new_ui.ui_active = active
121 121
122 repository_id = ''
122 123 if self.repo:
123 124 repo = self._get_repo(self.repo)
124 125 repository_id = repo.repo_id
@@ -440,26 +441,39 b' class VcsSettingsModel(object):'
440 441 HOOKS_SETTINGS = (
441 442 ('hooks', 'changegroup.repo_size'),
442 443 ('hooks', 'changegroup.push_logger'),
443 ('hooks', 'outgoing.pull_logger'),)
444 ('hooks', 'outgoing.pull_logger'),
445 )
444 446 HG_SETTINGS = (
445 447 ('extensions', 'largefiles'),
446 448 ('phases', 'publish'),
447 ('extensions', 'evolve'),)
449 ('extensions', 'evolve'),
450 ('extensions', 'topic'),
451 ('experimental', 'evolution'),
452 ('experimental', 'evolution.exchange'),
453 )
448 454 GIT_SETTINGS = (
449 ('vcs_git_lfs', 'enabled'),)
455 ('vcs_git_lfs', 'enabled'),
456 )
450 457 GLOBAL_HG_SETTINGS = (
451 458 ('extensions', 'largefiles'),
452 459 ('largefiles', 'usercache'),
453 460 ('phases', 'publish'),
454 461 ('extensions', 'hgsubversion'),
455 ('extensions', 'evolve'),)
462 ('extensions', 'evolve'),
463 ('extensions', 'topic'),
464 ('experimental', 'evolution'),
465 ('experimental', 'evolution.exchange'),
466 )
467
456 468 GLOBAL_GIT_SETTINGS = (
457 469 ('vcs_git_lfs', 'enabled'),
458 ('vcs_git_lfs', 'store_location'))
470 ('vcs_git_lfs', 'store_location')
471 )
459 472
460 473 GLOBAL_SVN_SETTINGS = (
461 474 ('vcs_svn_proxy', 'http_requests_enabled'),
462 ('vcs_svn_proxy', 'http_server_url'))
475 ('vcs_svn_proxy', 'http_server_url')
476 )
463 477
464 478 SVN_BRANCH_SECTION = 'vcs_svn_branch'
465 479 SVN_TAG_SECTION = 'vcs_svn_tag'
@@ -574,12 +588,38 b' class VcsSettingsModel(object):'
574 588 def create_repo_svn_settings(self, data):
575 589 return self._create_svn_settings(self.repo_settings, data)
576 590
591 def _set_evolution(self, settings, is_enabled):
592 if is_enabled:
593 # if evolve is active set evolution=all
594
595 self._create_or_update_ui(
596 settings, *('experimental', 'evolution'), value='all',
597 active=True)
598 self._create_or_update_ui(
599 settings, *('experimental', 'evolution.exchange'), value='yes',
600 active=True)
601 # if evolve is active set topics server support
602 self._create_or_update_ui(
603 settings, *('extensions', 'topic'), value='',
604 active=True)
605
606 else:
607 self._create_or_update_ui(
608 settings, *('experimental', 'evolution'), value='',
609 active=False)
610 self._create_or_update_ui(
611 settings, *('experimental', 'evolution.exchange'), value='no',
612 active=False)
613 self._create_or_update_ui(
614 settings, *('extensions', 'topic'), value='',
615 active=False)
616
577 617 @assert_repo_settings
578 618 def create_or_update_repo_hg_settings(self, data):
579 619 largefiles, phases, evolve = \
580 self.HG_SETTINGS
620 self.HG_SETTINGS[:3]
581 621 largefiles_key, phases_key, evolve_key = \
582 self._get_settings_keys(self.HG_SETTINGS, data)
622 self._get_settings_keys(self.HG_SETTINGS[:3], data)
583 623
584 624 self._create_or_update_ui(
585 625 self.repo_settings, *largefiles, value='',
@@ -587,21 +627,22 b' class VcsSettingsModel(object):'
587 627 self._create_or_update_ui(
588 628 self.repo_settings, *evolve, value='',
589 629 active=data[evolve_key])
630 self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
631
590 632 self._create_or_update_ui(
591 633 self.repo_settings, *phases, value=safe_str(data[phases_key]))
592 634
593 635 def create_or_update_global_hg_settings(self, data):
594 636 largefiles, largefiles_store, phases, hgsubversion, evolve \
595 = self.GLOBAL_HG_SETTINGS
637 = self.GLOBAL_HG_SETTINGS[:5]
596 638 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
597 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
639 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data)
598 640
599 641 self._create_or_update_ui(
600 642 self.global_settings, *largefiles, value='',
601 643 active=data[largefiles_key])
602 644 self._create_or_update_ui(
603 self.global_settings, *largefiles_store,
604 value=data[largefiles_store_key])
645 self.global_settings, *largefiles_store, value=data[largefiles_store_key])
605 646 self._create_or_update_ui(
606 647 self.global_settings, *phases, value=safe_str(data[phases_key]))
607 648 self._create_or_update_ui(
@@ -609,9 +650,10 b' class VcsSettingsModel(object):'
609 650 self._create_or_update_ui(
610 651 self.global_settings, *evolve, value='',
611 652 active=data[evolve_key])
653 self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
612 654
613 655 def create_or_update_repo_git_settings(self, data):
614 # NOTE(marcink): # comma make unpack work properly
656 # NOTE(marcink): # comma makes unpack work properly
615 657 lfs_enabled, \
616 658 = self.GIT_SETTINGS
617 659
@@ -675,6 +717,7 b' class VcsSettingsModel(object):'
675 717 def get_repo_ui_settings(self, section=None, key=None):
676 718 global_uis = self.global_settings.get_ui(section, key)
677 719 repo_uis = self.repo_settings.get_ui(section, key)
720
678 721 filtered_repo_uis = self._filter_ui_settings(repo_uis)
679 722 filtered_repo_uis_keys = [
680 723 (s.section, s.key) for s in filtered_repo_uis]
@@ -40,15 +40,32 b''
40 40
41 41 <script>
42 42 $(document).ready(function() {
43
44 var get_datatable_count = function(){
45 var api = $('#group_list_table').dataTable().api();
46 $('#repo_group_count').text(api.page.info().recordsDisplay);
47 };
43 var $repoGroupsListTable = $('#group_list_table');
48 44
49 45 // repo group list
50 $('#group_list_table').DataTable({
51 data: ${c.data|n},
46 $repoGroupsListTable.DataTable({
47 processing: true,
48 serverSide: true,
49 ajax: {
50 "url": "${h.route_path('repo_groups_data')}",
51 "dataSrc": function (json) {
52 var filteredCount = json.recordsFiltered;
53 var filteredInactiveCount = json.recordsFilteredInactive;
54 var totalInactive = json.recordsTotalInactive;
55 var total = json.recordsTotal;
56
57 var _text = _gettext(
58 "{0} of {1} repository groups").format(
59 filteredCount, total);
60
61 if (total === filteredCount) {
62 _text = _gettext("{0} repository groups").format(total);
63 }
64 $('#repo_group_count').text(_text);
65 return json.data;
66 },
67 },
68
52 69 dom: 'rtp',
53 70 pageLength: ${c.visual.admin_grid_items},
54 71 order: [[ 0, "asc" ]],
@@ -66,36 +83,34 b''
66 83 { data: {"_": "owner",
67 84 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" },
68 85 { data: {"_": "action",
69 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
86 "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
70 87 ],
71 88 language: {
72 89 paginate: DEFAULT_GRID_PAGINATION,
90 sProcessing: _gettext('loading...'),
73 91 emptyTable: _gettext("No repository groups available yet.")
74 92 },
75 "initComplete": function( settings, json ) {
76 get_datatable_count();
77 quick_repo_menu();
78 }
79 93 });
80 94
81 // update the counter when doing search
82 $('#group_list_table').on( 'search.dt', function (e,settings) {
83 get_datatable_count();
95 $repoGroupsListTable.on('xhr.dt', function(e, settings, json, xhr){
96 $repoGroupsListTable.css('opacity', 1);
97 });
98
99 $repoGroupsListTable.on('preXhr.dt', function(e, settings, data){
100 $repoGroupsListTable.css('opacity', 0.3);
84 101 });
85 102
86 // filter, filter both grids
87 $('#q_filter').on( 'keyup', function () {
103 // filter
104 $('#q_filter').on('keyup',
105 $.debounce(250, function() {
106 $repoGroupsListTable.DataTable().search(
107 $('#q_filter').val()
108 ).draw();
109 })
110 );
111 });
88 112
89 var repo_group_api = $('#group_list_table').dataTable().api();
90 repo_group_api
91 .columns(0)
92 .search(this.value)
93 .draw();
94 });
113 </script>
95 114
96 // refilter table if page load via back button
97 $("#q_filter").trigger('keyup');
98 });
99 </script>
100 115 </%def>
101 116
@@ -70,7 +70,7 b''
70 70 { data: {"_": "state",
71 71 "sort": "state"}, title: "${_('State')}", className: "td-tags td-state" },
72 72 { data: {"_": "action",
73 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
73 "sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
74 74 ],
75 75 language: {
76 76 paginate: DEFAULT_GRID_PAGINATION,
@@ -105,7 +105,7 b''
105 105 // filter
106 106 $('#q_filter').on('keyup',
107 107 $.debounce(250, function() {
108 $('#user_group_list_table').DataTable().search(
108 $userGroupsListTable.DataTable().search(
109 109 $('#q_filter').val()
110 110 ).draw();
111 111 })
@@ -133,13 +133,13 b''
133 133
134 134 <div class="checkbox">
135 135 ${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
136 <label for="extensions_evolve${suffix}">${_('Enable evolve extension')}</label>
136 <label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
137 137 </div>
138 138 <div class="label">
139 139 % if display_globals:
140 <span class="help-block">${_('Enable evolve extension for all repositories.')}</span>
140 <span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
141 141 % else:
142 <span class="help-block">${_('Enable evolve extension for this repository.')}</span>
142 <span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
143 143 % endif
144 144 </div>
145 145
@@ -35,6 +35,8 b' def route_path(name, params=None, **kwar'
35 35 ADMIN_PREFIX + '/repos',
36 36 'repo_groups':
37 37 ADMIN_PREFIX + '/repo_groups',
38 'repo_groups_data':
39 ADMIN_PREFIX + '/repo_groups_data',
38 40 'user_groups':
39 41 ADMIN_PREFIX + '/user_groups',
40 42 'user_groups_data':
@@ -63,8 +65,9 b' class TestAdminDelegatedUser(TestControl'
63 65 response = self.app.get(route_path('repos'), status=200)
64 66 response.mustcontain('data: []')
65 67
66 response = self.app.get(route_path('repo_groups'), status=200)
67 response.mustcontain('data: []')
68 response = self.app.get(route_path('repo_groups_data'),
69 status=200, extra_environ=xhr_header)
70 assert response.json['data'] == []
68 71
69 72 response = self.app.get(route_path('user_groups_data'),
70 73 status=200, extra_environ=xhr_header)
@@ -97,7 +100,8 b' class TestAdminDelegatedUser(TestControl'
97 100 response = self.app.get(route_path('repos'), status=200)
98 101 response.mustcontain('"name_raw": "{}"'.format(repo_name))
99 102
100 response = self.app.get(route_path('repo_groups'), status=200)
103 response = self.app.get(route_path('repo_groups_data'),
104 extra_environ=xhr_header, status=200)
101 105 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
102 106
103 107 response = self.app.get(route_path('user_groups_data'),
@@ -139,7 +143,8 b' class TestAdminDelegatedUser(TestControl'
139 143 response = self.app.get(route_path('repos'), status=200)
140 144 response.mustcontain('"name_raw": "{}"'.format(repo_name))
141 145
142 response = self.app.get(route_path('repo_groups'), status=200)
146 response = self.app.get(route_path('repo_groups_data'),
147 extra_environ=xhr_header, status=200)
143 148 response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
144 149
145 150 response = self.app.get(route_path('user_groups_data'),
@@ -501,8 +501,8 b' class TestCreateOrUpdateUi(object):'
501 501
502 502 def test_update(self, repo_stub, settings_util):
503 503 model = VcsSettingsModel(repo=repo_stub.repo_name)
504
505 largefiles, phases, evolve = model.HG_SETTINGS
504 # care about only 3 first settings
505 largefiles, phases, evolve = model.HG_SETTINGS[:3]
506 506
507 507 section = 'test-section'
508 508 key = 'test-key'
@@ -531,10 +531,11 b' class TestCreateOrUpdateRepoHgSettings(o'
531 531 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
532 532 model.create_or_update_repo_hg_settings(self.FORM_DATA)
533 533 expected_calls = [
534 mock.call(model.repo_settings, 'extensions', 'largefiles',
535 active=False, value=''),
536 mock.call(model.repo_settings, 'extensions', 'evolve',
537 active=False, value=''),
534 mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
535 mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
536 mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
537 mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
538 mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
538 539 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
539 540 ]
540 541 assert expected_calls == create_mock.call_args_list
@@ -589,17 +590,16 b' class TestCreateOrUpdateGlobalHgSettings'
589 590 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
590 591 model.create_or_update_global_hg_settings(self.FORM_DATA)
591 592 expected_calls = [
592 mock.call(model.global_settings, 'extensions', 'largefiles',
593 active=False, value=''),
594 mock.call(model.global_settings, 'largefiles', 'usercache',
595 value='/example/largefiles-store'),
596 mock.call(model.global_settings, 'phases', 'publish',
597 value='False'),
598 mock.call(model.global_settings, 'extensions', 'hgsubversion',
599 active=False),
600 mock.call(model.global_settings, 'extensions', 'evolve',
601 active=False, value='')
593 mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
594 mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
595 mock.call(model.global_settings, 'phases', 'publish', value='False'),
596 mock.call(model.global_settings, 'extensions', 'hgsubversion', active=False),
597 mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
598 mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
599 mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
600 mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
602 601 ]
602
603 603 assert expected_calls == create_mock.call_args_list
604 604
605 605 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
@@ -625,10 +625,8 b' class TestCreateOrUpdateGlobalGitSetting'
625 625 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
626 626 model.create_or_update_global_git_settings(self.FORM_DATA)
627 627 expected_calls = [
628 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled',
629 active=False, value=False),
630 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location',
631 value='/example/lfs-store'),
628 mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
629 mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
632 630 ]
633 631 assert expected_calls == create_mock.call_args_list
634 632
@@ -512,7 +512,7 b' def test_outdated_comments('
512 512 (MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
513 513 'This pull request cannot be merged because the source contains more branches than the target.'),
514 514 (MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
515 'This pull request cannot be merged because the target has multiple heads: `a,b,c`.'),
515 'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'),
516 516 (MergeFailureReason.TARGET_IS_LOCKED,
517 517 'This pull request cannot be merged because the target repository is locked by user:123.'),
518 518 (MergeFailureReason.MISSING_TARGET_REF,
General Comments 0
You need to be logged in to leave comments. Login now