##// END OF EJS Templates
pull-request: extended default reviewers functionality....
marcink -
r1769:fb9baff8 default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from rhodecode.model.db import Repository
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
36
37
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
41
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
44 ({'source': 0, 'my': 1}, 'opened by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
47 ({'source': 1}, 'Pull Requests from'),
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
51
52 response = self.app.get(
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
56
57 assert_response = response.assert_response()
58 assert_response.element_equals_to('.panel-title', expected_title)
59 element = assert_response.get_element('.panel-title')
60 element_text = assert_response._element_to_string(element)
61
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
65 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
68
69 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
71
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
75
76 response = self.app.get(
77 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
80
81 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
23
24
25 def reviewer_as_json(user, reasons, mandatory):
26 """
27 Returns json struct of a reviewer for frontend
28
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
32 """
33
34 return {
35 'user_id': user.user_id,
36 'reasons': reasons,
37 'mandatory': mandatory,
38 'username': user.username,
39 'firstname': user.firstname,
40 'lastname': user.lastname,
41 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
43
44
45 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
47
48 """ Return json for default reviewers of a repository """
49
50 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
53
54 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
57 'rules': {},
58 'rules_data': {},
59 }
60
61
62 def validate_default_reviewers(review_members, reviewer_rules):
63 """
64 Function to validate submitted reviewers against the saved rules
65
66 """
67 reviewers = []
68 reviewer_by_id = {}
69 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
72
73 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
75
76 return reviewers
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_author_to_review = Column(
21 "forbid_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_author_to_review.create(table=repo_review_rule_table)
23
24 forbid_adding_reviewers = Column(
25 "forbid_adding_reviewers", Boolean(), nullable=True, default=False)
26 forbid_adding_reviewers.create(table=repo_review_rule_table)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,38 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_user_table = db.RepoReviewRuleUser.__table__
19 repo_review_rule_user_group_table = db.RepoReviewRuleUserGroup.__table__
20
21 mandatory_user = Column(
22 "mandatory", Boolean(), nullable=True, default=False)
23 mandatory_user.create(table=repo_review_rule_user_table)
24
25 mandatory_user_group = Column(
26 "mandatory", Boolean(), nullable=True, default=False)
27 mandatory_user_group.create(table=repo_review_rule_user_group_table)
28
29 fixups(db, meta.Session)
30
31
32 def downgrade(migrate_engine):
33 meta = MetaData()
34 meta.bind = migrate_engine
35
36
37 def fixups(models, _SESSION):
38 pass
@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request_reviewers = db.PullRequestReviewers.__table__
19
20 mandatory = Column(
21 "mandatory", Boolean(), nullable=True, default=False)
22 mandatory.create(table=pull_request_reviewers)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -0,0 +1,40 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request = db.PullRequest.__table__
19 pull_request_version = db.PullRequestVersion.__table__
20
21 reviewer_data_1 = Column(
22 'reviewer_data_json',
23 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
24 reviewer_data_1.create(table=pull_request)
25
26 reviewer_data_2 = Column(
27 'reviewer_data_json',
28 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
29 reviewer_data_2.create(table=pull_request_version)
30
31 fixups(db, meta.Session)
32
33
34 def downgrade(migrate_engine):
35 meta = MetaData()
36 meta.bind = migrate_engine
37
38
39 def fixups(models, _SESSION):
40 pass
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
23
24
25 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=[])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29
30
31 class ReviewerListSchema(colander.SequenceSchema):
32 reviewers = ReviewerSchema()
33
34
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pylons
43 # link to config for pylons
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 73 # defines current db version for migrations
54 __dbversion__ = 77 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,279 +1,297 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User
23 from rhodecode.model.db import User
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCreatePullRequestApi(object):
32 class TestCreatePullRequestApi(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 def test_create_with_wrong_data(self):
41 def test_create_with_wrong_data(self):
42 required_data = {
42 required_data = {
43 'source_repo': 'tests/source_repo',
43 'source_repo': 'tests/source_repo',
44 'target_repo': 'tests/target_repo',
44 'target_repo': 'tests/target_repo',
45 'source_ref': 'branch:default:initial',
45 'source_ref': 'branch:default:initial',
46 'target_ref': 'branch:default:new-feature',
46 'target_ref': 'branch:default:new-feature',
47 'title': 'Test PR 1'
47 'title': 'Test PR 1'
48 }
48 }
49 for key in required_data:
49 for key in required_data:
50 data = required_data.copy()
50 data = required_data.copy()
51 data.pop(key)
51 data.pop(key)
52 id_, params = build_data(
52 id_, params = build_data(
53 self.apikey, 'create_pull_request', **data)
53 self.apikey, 'create_pull_request', **data)
54 response = api_call(self.app, params)
54 response = api_call(self.app, params)
55
55
56 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
57 assert_error(id_, expected, given=response.body)
57 assert_error(id_, expected, given=response.body)
58
58
59 @pytest.mark.backends("git", "hg")
59 @pytest.mark.backends("git", "hg")
60 def test_create_with_correct_data(self, backend):
60 def test_create_with_correct_data(self, backend):
61 data = self._prepare_data(backend)
61 data = self._prepare_data(backend)
62 RepoModel().revoke_user_permission(
62 RepoModel().revoke_user_permission(
63 self.source.repo_name, User.DEFAULT_USER)
63 self.source.repo_name, User.DEFAULT_USER)
64 id_, params = build_data(
64 id_, params = build_data(
65 self.apikey_regular, 'create_pull_request', **data)
65 self.apikey_regular, 'create_pull_request', **data)
66 response = api_call(self.app, params)
66 response = api_call(self.app, params)
67 expected_message = "Created new pull request `{title}`".format(
67 expected_message = "Created new pull request `{title}`".format(
68 title=data['title'])
68 title=data['title'])
69 result = response.json
69 result = response.json
70 assert result['result']['msg'] == expected_message
70 assert result['result']['msg'] == expected_message
71 pull_request_id = result['result']['pull_request_id']
71 pull_request_id = result['result']['pull_request_id']
72 pull_request = PullRequestModel().get(pull_request_id)
72 pull_request = PullRequestModel().get(pull_request_id)
73 assert pull_request.title == data['title']
73 assert pull_request.title == data['title']
74 assert pull_request.description == data['description']
74 assert pull_request.description == data['description']
75 assert pull_request.source_ref == data['source_ref']
75 assert pull_request.source_ref == data['source_ref']
76 assert pull_request.target_ref == data['target_ref']
76 assert pull_request.target_ref == data['target_ref']
77 assert pull_request.source_repo.repo_name == data['source_repo']
77 assert pull_request.source_repo.repo_name == data['source_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
79 assert pull_request.revisions == [self.commit_ids['change']]
79 assert pull_request.revisions == [self.commit_ids['change']]
80 assert pull_request.reviewers == []
80 assert pull_request.reviewers == []
81
81
82 @pytest.mark.backends("git", "hg")
82 @pytest.mark.backends("git", "hg")
83 def test_create_with_empty_description(self, backend):
83 def test_create_with_empty_description(self, backend):
84 data = self._prepare_data(backend)
84 data = self._prepare_data(backend)
85 data.pop('description')
85 data.pop('description')
86 id_, params = build_data(
86 id_, params = build_data(
87 self.apikey_regular, 'create_pull_request', **data)
87 self.apikey_regular, 'create_pull_request', **data)
88 response = api_call(self.app, params)
88 response = api_call(self.app, params)
89 expected_message = "Created new pull request `{title}`".format(
89 expected_message = "Created new pull request `{title}`".format(
90 title=data['title'])
90 title=data['title'])
91 result = response.json
91 result = response.json
92 assert result['result']['msg'] == expected_message
92 assert result['result']['msg'] == expected_message
93 pull_request_id = result['result']['pull_request_id']
93 pull_request_id = result['result']['pull_request_id']
94 pull_request = PullRequestModel().get(pull_request_id)
94 pull_request = PullRequestModel().get(pull_request_id)
95 assert pull_request.description == ''
95 assert pull_request.description == ''
96
96
97 @pytest.mark.backends("git", "hg")
97 @pytest.mark.backends("git", "hg")
98 def test_create_with_reviewers_specified_by_names(
98 def test_create_with_reviewers_specified_by_names(
99 self, backend, no_notifications):
99 self, backend, no_notifications):
100 data = self._prepare_data(backend)
100 data = self._prepare_data(backend)
101 reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
101 reviewers = [
102 {'username': TEST_USER_REGULAR_LOGIN,
103 'reasons': ['added manually']},
104 {'username': TEST_USER_ADMIN_LOGIN,
105 'reasons': ['added manually']},
106 ]
102 data['reviewers'] = reviewers
107 data['reviewers'] = reviewers
103 id_, params = build_data(
108 id_, params = build_data(
104 self.apikey_regular, 'create_pull_request', **data)
109 self.apikey_regular, 'create_pull_request', **data)
105 response = api_call(self.app, params)
110 response = api_call(self.app, params)
106
111
107 expected_message = "Created new pull request `{title}`".format(
112 expected_message = "Created new pull request `{title}`".format(
108 title=data['title'])
113 title=data['title'])
109 result = response.json
114 result = response.json
110 assert result['result']['msg'] == expected_message
115 assert result['result']['msg'] == expected_message
111 pull_request_id = result['result']['pull_request_id']
116 pull_request_id = result['result']['pull_request_id']
112 pull_request = PullRequestModel().get(pull_request_id)
117 pull_request = PullRequestModel().get(pull_request_id)
113 actual_reviewers = [r.user.username for r in pull_request.reviewers]
118 actual_reviewers = [
119 {'username': r.user.username,
120 'reasons': ['added manually'],
121 } for r in pull_request.reviewers
122 ]
114 assert sorted(actual_reviewers) == sorted(reviewers)
123 assert sorted(actual_reviewers) == sorted(reviewers)
115
124
116 @pytest.mark.backends("git", "hg")
125 @pytest.mark.backends("git", "hg")
117 def test_create_with_reviewers_specified_by_ids(
126 def test_create_with_reviewers_specified_by_ids(
118 self, backend, no_notifications):
127 self, backend, no_notifications):
119 data = self._prepare_data(backend)
128 data = self._prepare_data(backend)
120 reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
121 reviewers = [
129 reviewers = [
122 UserModel().get_by_username(n).user_id for n in reviewer_names]
130 {'username': UserModel().get_by_username(
131 TEST_USER_REGULAR_LOGIN).user_id,
132 'reasons': ['added manually']},
133 {'username': UserModel().get_by_username(
134 TEST_USER_ADMIN_LOGIN).user_id,
135 'reasons': ['added manually']},
136 ]
137
123 data['reviewers'] = reviewers
138 data['reviewers'] = reviewers
124 id_, params = build_data(
139 id_, params = build_data(
125 self.apikey_regular, 'create_pull_request', **data)
140 self.apikey_regular, 'create_pull_request', **data)
126 response = api_call(self.app, params)
141 response = api_call(self.app, params)
127
142
128 expected_message = "Created new pull request `{title}`".format(
143 expected_message = "Created new pull request `{title}`".format(
129 title=data['title'])
144 title=data['title'])
130 result = response.json
145 result = response.json
131 assert result['result']['msg'] == expected_message
146 assert result['result']['msg'] == expected_message
132 pull_request_id = result['result']['pull_request_id']
147 pull_request_id = result['result']['pull_request_id']
133 pull_request = PullRequestModel().get(pull_request_id)
148 pull_request = PullRequestModel().get(pull_request_id)
134 actual_reviewers = [r.user.username for r in pull_request.reviewers]
149 actual_reviewers = [
135 assert sorted(actual_reviewers) == sorted(reviewer_names)
150 {'username': r.user.user_id,
151 'reasons': ['added manually'],
152 } for r in pull_request.reviewers
153 ]
154 assert sorted(actual_reviewers) == sorted(reviewers)
136
155
137 @pytest.mark.backends("git", "hg")
156 @pytest.mark.backends("git", "hg")
138 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
157 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
139 data = self._prepare_data(backend)
158 data = self._prepare_data(backend)
140 reviewers = ['somebody']
159 data['reviewers'] = [{'username': 'somebody'}]
141 data['reviewers'] = reviewers
142 id_, params = build_data(
160 id_, params = build_data(
143 self.apikey_regular, 'create_pull_request', **data)
161 self.apikey_regular, 'create_pull_request', **data)
144 response = api_call(self.app, params)
162 response = api_call(self.app, params)
145 expected_message = 'user `somebody` does not exist'
163 expected_message = 'user `somebody` does not exist'
146 assert_error(id_, expected_message, given=response.body)
164 assert_error(id_, expected_message, given=response.body)
147
165
148 @pytest.mark.backends("git", "hg")
166 @pytest.mark.backends("git", "hg")
149 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
167 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
150 data = self._prepare_data(backend)
168 data = self._prepare_data(backend)
151 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
169 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
152 data['reviewers'] = reviewers
170 data['reviewers'] = reviewers
153 id_, params = build_data(
171 id_, params = build_data(
154 self.apikey_regular, 'create_pull_request', **data)
172 self.apikey_regular, 'create_pull_request', **data)
155 response = api_call(self.app, params)
173 response = api_call(self.app, params)
156 expected_message = 'reviewers should be specified as a list'
174 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
157 assert_error(id_, expected_message, given=response.body)
175 assert_error(id_, expected_message, given=response.body)
158
176
159 @pytest.mark.backends("git", "hg")
177 @pytest.mark.backends("git", "hg")
160 def test_create_with_no_commit_hashes(self, backend):
178 def test_create_with_no_commit_hashes(self, backend):
161 data = self._prepare_data(backend)
179 data = self._prepare_data(backend)
162 expected_source_ref = data['source_ref']
180 expected_source_ref = data['source_ref']
163 expected_target_ref = data['target_ref']
181 expected_target_ref = data['target_ref']
164 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
182 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
165 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
183 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
166 id_, params = build_data(
184 id_, params = build_data(
167 self.apikey_regular, 'create_pull_request', **data)
185 self.apikey_regular, 'create_pull_request', **data)
168 response = api_call(self.app, params)
186 response = api_call(self.app, params)
169 expected_message = "Created new pull request `{title}`".format(
187 expected_message = "Created new pull request `{title}`".format(
170 title=data['title'])
188 title=data['title'])
171 result = response.json
189 result = response.json
172 assert result['result']['msg'] == expected_message
190 assert result['result']['msg'] == expected_message
173 pull_request_id = result['result']['pull_request_id']
191 pull_request_id = result['result']['pull_request_id']
174 pull_request = PullRequestModel().get(pull_request_id)
192 pull_request = PullRequestModel().get(pull_request_id)
175 assert pull_request.source_ref == expected_source_ref
193 assert pull_request.source_ref == expected_source_ref
176 assert pull_request.target_ref == expected_target_ref
194 assert pull_request.target_ref == expected_target_ref
177
195
178 @pytest.mark.backends("git", "hg")
196 @pytest.mark.backends("git", "hg")
179 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
197 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
180 def test_create_fails_with_wrong_repo(self, backend, data_key):
198 def test_create_fails_with_wrong_repo(self, backend, data_key):
181 repo_name = 'fake-repo'
199 repo_name = 'fake-repo'
182 data = self._prepare_data(backend)
200 data = self._prepare_data(backend)
183 data[data_key] = repo_name
201 data[data_key] = repo_name
184 id_, params = build_data(
202 id_, params = build_data(
185 self.apikey_regular, 'create_pull_request', **data)
203 self.apikey_regular, 'create_pull_request', **data)
186 response = api_call(self.app, params)
204 response = api_call(self.app, params)
187 expected_message = 'repository `{}` does not exist'.format(repo_name)
205 expected_message = 'repository `{}` does not exist'.format(repo_name)
188 assert_error(id_, expected_message, given=response.body)
206 assert_error(id_, expected_message, given=response.body)
189
207
190 @pytest.mark.backends("git", "hg")
208 @pytest.mark.backends("git", "hg")
191 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
209 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
192 def test_create_fails_with_non_existing_branch(self, backend, data_key):
210 def test_create_fails_with_non_existing_branch(self, backend, data_key):
193 branch_name = 'test-branch'
211 branch_name = 'test-branch'
194 data = self._prepare_data(backend)
212 data = self._prepare_data(backend)
195 data[data_key] = "branch:{}".format(branch_name)
213 data[data_key] = "branch:{}".format(branch_name)
196 id_, params = build_data(
214 id_, params = build_data(
197 self.apikey_regular, 'create_pull_request', **data)
215 self.apikey_regular, 'create_pull_request', **data)
198 response = api_call(self.app, params)
216 response = api_call(self.app, params)
199 expected_message = 'The specified branch `{}` does not exist'.format(
217 expected_message = 'The specified branch `{}` does not exist'.format(
200 branch_name)
218 branch_name)
201 assert_error(id_, expected_message, given=response.body)
219 assert_error(id_, expected_message, given=response.body)
202
220
203 @pytest.mark.backends("git", "hg")
221 @pytest.mark.backends("git", "hg")
204 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
222 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
205 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
223 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
206 data = self._prepare_data(backend)
224 data = self._prepare_data(backend)
207 ref = 'stange-ref'
225 ref = 'stange-ref'
208 data[data_key] = ref
226 data[data_key] = ref
209 id_, params = build_data(
227 id_, params = build_data(
210 self.apikey_regular, 'create_pull_request', **data)
228 self.apikey_regular, 'create_pull_request', **data)
211 response = api_call(self.app, params)
229 response = api_call(self.app, params)
212 expected_message = (
230 expected_message = (
213 'Ref `{ref}` given in a wrong format. Please check the API'
231 'Ref `{ref}` given in a wrong format. Please check the API'
214 ' documentation for more details'.format(ref=ref))
232 ' documentation for more details'.format(ref=ref))
215 assert_error(id_, expected_message, given=response.body)
233 assert_error(id_, expected_message, given=response.body)
216
234
217 @pytest.mark.backends("git", "hg")
235 @pytest.mark.backends("git", "hg")
218 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
236 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
219 def test_create_fails_with_non_existing_ref(self, backend, data_key):
237 def test_create_fails_with_non_existing_ref(self, backend, data_key):
220 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
238 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
221 ref = self._get_full_ref(backend, commit_id)
239 ref = self._get_full_ref(backend, commit_id)
222 data = self._prepare_data(backend)
240 data = self._prepare_data(backend)
223 data[data_key] = ref
241 data[data_key] = ref
224 id_, params = build_data(
242 id_, params = build_data(
225 self.apikey_regular, 'create_pull_request', **data)
243 self.apikey_regular, 'create_pull_request', **data)
226 response = api_call(self.app, params)
244 response = api_call(self.app, params)
227 expected_message = 'Ref `{}` does not exist'.format(ref)
245 expected_message = 'Ref `{}` does not exist'.format(ref)
228 assert_error(id_, expected_message, given=response.body)
246 assert_error(id_, expected_message, given=response.body)
229
247
230 @pytest.mark.backends("git", "hg")
248 @pytest.mark.backends("git", "hg")
231 def test_create_fails_when_no_revisions(self, backend):
249 def test_create_fails_when_no_revisions(self, backend):
232 data = self._prepare_data(backend, source_head='initial')
250 data = self._prepare_data(backend, source_head='initial')
233 id_, params = build_data(
251 id_, params = build_data(
234 self.apikey_regular, 'create_pull_request', **data)
252 self.apikey_regular, 'create_pull_request', **data)
235 response = api_call(self.app, params)
253 response = api_call(self.app, params)
236 expected_message = 'no commits found'
254 expected_message = 'no commits found'
237 assert_error(id_, expected_message, given=response.body)
255 assert_error(id_, expected_message, given=response.body)
238
256
239 @pytest.mark.backends("git", "hg")
257 @pytest.mark.backends("git", "hg")
240 def test_create_fails_when_no_permissions(self, backend):
258 def test_create_fails_when_no_permissions(self, backend):
241 data = self._prepare_data(backend)
259 data = self._prepare_data(backend)
242 RepoModel().revoke_user_permission(
260 RepoModel().revoke_user_permission(
243 self.source.repo_name, User.DEFAULT_USER)
261 self.source.repo_name, User.DEFAULT_USER)
244 RepoModel().revoke_user_permission(
262 RepoModel().revoke_user_permission(
245 self.source.repo_name, self.test_user)
263 self.source.repo_name, self.test_user)
246 id_, params = build_data(
264 id_, params = build_data(
247 self.apikey_regular, 'create_pull_request', **data)
265 self.apikey_regular, 'create_pull_request', **data)
248 response = api_call(self.app, params)
266 response = api_call(self.app, params)
249 expected_message = 'repository `{}` does not exist'.format(
267 expected_message = 'repository `{}` does not exist'.format(
250 self.source.repo_name)
268 self.source.repo_name)
251 assert_error(id_, expected_message, given=response.body)
269 assert_error(id_, expected_message, given=response.body)
252
270
253 def _prepare_data(
271 def _prepare_data(
254 self, backend, source_head='change', target_head='initial'):
272 self, backend, source_head='change', target_head='initial'):
255 commits = [
273 commits = [
256 {'message': 'initial'},
274 {'message': 'initial'},
257 {'message': 'change'},
275 {'message': 'change'},
258 {'message': 'new-feature', 'parents': ['initial']},
276 {'message': 'new-feature', 'parents': ['initial']},
259 ]
277 ]
260 self.commit_ids = backend.create_master_repo(commits)
278 self.commit_ids = backend.create_master_repo(commits)
261 self.source = backend.create_repo(heads=[source_head])
279 self.source = backend.create_repo(heads=[source_head])
262 self.target = backend.create_repo(heads=[target_head])
280 self.target = backend.create_repo(heads=[target_head])
263 data = {
281 data = {
264 'source_repo': self.source.repo_name,
282 'source_repo': self.source.repo_name,
265 'target_repo': self.target.repo_name,
283 'target_repo': self.target.repo_name,
266 'source_ref': self._get_full_ref(
284 'source_ref': self._get_full_ref(
267 backend, self.commit_ids[source_head]),
285 backend, self.commit_ids[source_head]),
268 'target_ref': self._get_full_ref(
286 'target_ref': self._get_full_ref(
269 backend, self.commit_ids[target_head]),
287 backend, self.commit_ids[target_head]),
270 'title': 'Test PR 1',
288 'title': 'Test PR 1',
271 'description': 'Test'
289 'description': 'Test'
272 }
290 }
273 RepoModel().grant_user_permission(
291 RepoModel().grant_user_permission(
274 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
292 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
275 return data
293 return data
276
294
277 def _get_full_ref(self, backend, commit_id):
295 def _get_full_ref(self, backend, commit_id):
278 return 'branch:{branch}:{commit_id}'.format(
296 return 'branch:{branch}:{commit_id}'.format(
279 branch=backend.default_branch_name, commit_id=commit_id)
297 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,133 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24 import urlobject
24 import urlobject
25 from pylons import url
25 from pylons import url
26
26
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util):
36 def test_api_get_pull_request(self, pr_util):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47
47
48 url_obj = urlobject.URLObject(
48 url_obj = urlobject.URLObject(
49 url(
49 url(
50 'pullrequest_show',
50 'pullrequest_show',
51 repo_name=pull_request.target_repo.repo_name,
51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 pr_url = unicode(
53 pr_url = unicode(
54 url_obj.with_netloc('test.example.com:80'))
54 url_obj.with_netloc('test.example.com:80'))
55 source_url = unicode(
55 source_url = unicode(
56 pull_request.source_repo.clone_url()
56 pull_request.source_repo.clone_url()
57 .with_netloc('test.example.com:80'))
57 .with_netloc('test.example.com:80'))
58 target_url = unicode(
58 target_url = unicode(
59 pull_request.target_repo.clone_url()
59 pull_request.target_repo.clone_url()
60 .with_netloc('test.example.com:80'))
60 .with_netloc('test.example.com:80'))
61 shadow_url = unicode(
61 shadow_url = unicode(
62 PullRequestModel().get_shadow_clone_url(pull_request))
62 PullRequestModel().get_shadow_clone_url(pull_request))
63 expected = {
63 expected = {
64 'pull_request_id': pull_request.pull_request_id,
64 'pull_request_id': pull_request.pull_request_id,
65 'url': pr_url,
65 'url': pr_url,
66 'title': pull_request.title,
66 'title': pull_request.title,
67 'description': pull_request.description,
67 'description': pull_request.description,
68 'status': pull_request.status,
68 'status': pull_request.status,
69 'created_on': pull_request.created_on,
69 'created_on': pull_request.created_on,
70 'updated_on': pull_request.updated_on,
70 'updated_on': pull_request.updated_on,
71 'commit_ids': pull_request.revisions,
71 'commit_ids': pull_request.revisions,
72 'review_status': pull_request.calculated_review_status(),
72 'review_status': pull_request.calculated_review_status(),
73 'mergeable': {
73 'mergeable': {
74 'status': True,
74 'status': True,
75 'message': 'This pull request can be automatically merged.',
75 'message': 'This pull request can be automatically merged.',
76 },
76 },
77 'source': {
77 'source': {
78 'clone_url': source_url,
78 'clone_url': source_url,
79 'repository': pull_request.source_repo.repo_name,
79 'repository': pull_request.source_repo.repo_name,
80 'reference': {
80 'reference': {
81 'name': pull_request.source_ref_parts.name,
81 'name': pull_request.source_ref_parts.name,
82 'type': pull_request.source_ref_parts.type,
82 'type': pull_request.source_ref_parts.type,
83 'commit_id': pull_request.source_ref_parts.commit_id,
83 'commit_id': pull_request.source_ref_parts.commit_id,
84 },
84 },
85 },
85 },
86 'target': {
86 'target': {
87 'clone_url': target_url,
87 'clone_url': target_url,
88 'repository': pull_request.target_repo.repo_name,
88 'repository': pull_request.target_repo.repo_name,
89 'reference': {
89 'reference': {
90 'name': pull_request.target_ref_parts.name,
90 'name': pull_request.target_ref_parts.name,
91 'type': pull_request.target_ref_parts.type,
91 'type': pull_request.target_ref_parts.type,
92 'commit_id': pull_request.target_ref_parts.commit_id,
92 'commit_id': pull_request.target_ref_parts.commit_id,
93 },
93 },
94 },
94 },
95 'merge': {
95 'merge': {
96 'clone_url': shadow_url,
96 'clone_url': shadow_url,
97 'reference': {
97 'reference': {
98 'name': pull_request.shadow_merge_ref.name,
98 'name': pull_request.shadow_merge_ref.name,
99 'type': pull_request.shadow_merge_ref.type,
99 'type': pull_request.shadow_merge_ref.type,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 },
101 },
102 },
102 },
103 'author': pull_request.author.get_api_data(include_secrets=False,
103 'author': pull_request.author.get_api_data(include_secrets=False,
104 details='basic'),
104 details='basic'),
105 'reviewers': [
105 'reviewers': [
106 {
106 {
107 'user': reviewer.get_api_data(include_secrets=False,
107 'user': reviewer.get_api_data(include_secrets=False,
108 details='basic'),
108 details='basic'),
109 'reasons': reasons,
109 'reasons': reasons,
110 'review_status': st[0][1].status if st else 'not_reviewed',
110 'review_status': st[0][1].status if st else 'not_reviewed',
111 }
111 }
112 for reviewer, reasons, st in pull_request.reviewers_statuses()
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 ]
114 ]
114 }
115 }
115 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
116
117
117 def test_api_get_pull_request_repo_error(self):
118 def test_api_get_pull_request_repo_error(self):
118 id_, params = build_data(
119 id_, params = build_data(
119 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
120 repoid=666, pullrequestid=1)
121 repoid=666, pullrequestid=1)
121 response = api_call(self.app, params)
122 response = api_call(self.app, params)
122
123
123 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
124 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
125
126
126 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
127 id_, params = build_data(
128 id_, params = build_data(
128 self.apikey, 'get_pull_request',
129 self.apikey, 'get_pull_request',
129 repoid=1, pullrequestid=666)
130 repoid=1, pullrequestid=666)
130 response = api_call(self.app, params)
131 response = api_call(self.app, params)
131
132
132 expected = 'pull request `666` does not exist'
133 expected = 'pull request `666` does not exist'
133 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
@@ -1,205 +1,213 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error)
28 build_data, api_call, assert_ok, assert_error)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdatePullRequest(object):
32 class TestUpdatePullRequest(object):
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, silence_action_logger, no_notifications):
36 self, pr_util, silence_action_logger, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'update_pull_request',
40 self.apikey, 'update_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id,
42 pullrequestid=pull_request.pull_request_id,
43 title='New TITLE OF A PR',
43 title='New TITLE OF A PR',
44 description='New DESC OF A PR',
44 description='New DESC OF A PR',
45 )
45 )
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 expected = {
48 expected = {
49 "msg": "Updated pull request `{}`".format(
49 "msg": "Updated pull request `{}`".format(
50 pull_request.pull_request_id),
50 pull_request.pull_request_id),
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 }
54 }
55
55
56 response_json = response.json['result']
56 response_json = response.json['result']
57 assert response_json == expected
57 assert response_json == expected
58 pr = response_json['pull_request']
58 pr = response_json['pull_request']
59 assert pr['title'] == 'New TITLE OF A PR'
59 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
61
61
62 @pytest.mark.backends("git", "hg")
62 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
63 def test_api_try_update_closed_pull_request(
64 self, pr_util, silence_action_logger, no_notifications):
64 self, pr_util, silence_action_logger, no_notifications):
65 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
66 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
67 pull_request, TEST_USER_ADMIN_LOGIN)
68
68
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_pull_request',
70 self.apikey, 'update_pull_request',
71 repoid=pull_request.target_repo.repo_name,
71 repoid=pull_request.target_repo.repo_name,
72 pullrequestid=pull_request.pull_request_id)
72 pullrequestid=pull_request.pull_request_id)
73 response = api_call(self.app, params)
73 response = api_call(self.app, params)
74
74
75 expected = 'pull request `{}` update failed, pull request ' \
75 expected = 'pull request `{}` update failed, pull request ' \
76 'is closed'.format(pull_request.pull_request_id)
76 'is closed'.format(pull_request.pull_request_id)
77
77
78 assert_error(id_, expected, response.body)
78 assert_error(id_, expected, response.body)
79
79
80 @pytest.mark.backends("git", "hg")
80 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(
81 def test_api_update_update_commits(
82 self, pr_util, silence_action_logger, no_notifications):
82 self, pr_util, silence_action_logger, no_notifications):
83 commits = [
83 commits = [
84 {'message': 'a'},
84 {'message': 'a'},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
87 ]
87 ]
88 pull_request = pr_util.create_pull_request(
88 pull_request = pr_util.create_pull_request(
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
90 pr_util.update_source_repository(head='c')
90 pr_util.update_source_repository(head='c')
91 repo = pull_request.source_repo.scm_instance()
91 repo = pull_request.source_repo.scm_instance()
92 commits = [x for x in repo.get_commits()]
92 commits = [x for x in repo.get_commits()]
93 print commits
93 print commits
94
94
95 added_commit_id = commits[-1].raw_id # c commit
95 added_commit_id = commits[-1].raw_id # c commit
96 common_commit_id = commits[1].raw_id # b commit is common ancestor
96 common_commit_id = commits[1].raw_id # b commit is common ancestor
97 total_commits = [added_commit_id, common_commit_id]
97 total_commits = [added_commit_id, common_commit_id]
98
98
99 id_, params = build_data(
99 id_, params = build_data(
100 self.apikey, 'update_pull_request',
100 self.apikey, 'update_pull_request',
101 repoid=pull_request.target_repo.repo_name,
101 repoid=pull_request.target_repo.repo_name,
102 pullrequestid=pull_request.pull_request_id,
102 pullrequestid=pull_request.pull_request_id,
103 update_commits=True
103 update_commits=True
104 )
104 )
105 response = api_call(self.app, params)
105 response = api_call(self.app, params)
106
106
107 expected = {
107 expected = {
108 "msg": "Updated pull request `{}`".format(
108 "msg": "Updated pull request `{}`".format(
109 pull_request.pull_request_id),
109 pull_request.pull_request_id),
110 "pull_request": response.json['result']['pull_request'],
110 "pull_request": response.json['result']['pull_request'],
111 "updated_commits": {"added": [added_commit_id],
111 "updated_commits": {"added": [added_commit_id],
112 "common": [common_commit_id],
112 "common": [common_commit_id],
113 "total": total_commits,
113 "total": total_commits,
114 "removed": []},
114 "removed": []},
115 "updated_reviewers": {"added": [], "removed": []},
115 "updated_reviewers": {"added": [], "removed": []},
116 }
116 }
117
117
118 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
119
119
120 @pytest.mark.backends("git", "hg")
120 @pytest.mark.backends("git", "hg")
121 def test_api_update_change_reviewers(
121 def test_api_update_change_reviewers(
122 self, pr_util, silence_action_logger, no_notifications):
122 self, user_util, pr_util, silence_action_logger, no_notifications):
123 a = user_util.create_user()
124 b = user_util.create_user()
125 c = user_util.create_user()
126 new_reviewers = [
127 {'username': b.username,'reasons': ['updated via API'],
128 'mandatory':False},
129 {'username': c.username, 'reasons': ['updated via API'],
130 'mandatory':False},
131 ]
123
132
124 users = [x.username for x in User.get_all()]
133 added = [b.username, c.username]
125 new = [users.pop(0)]
134 removed = [a.username]
126 removed = sorted(new)
127 added = sorted(users)
128
135
129 pull_request = pr_util.create_pull_request(reviewers=new)
136 pull_request = pr_util.create_pull_request(
137 reviewers=[(a.username, ['added via API'], False)])
130
138
131 id_, params = build_data(
139 id_, params = build_data(
132 self.apikey, 'update_pull_request',
140 self.apikey, 'update_pull_request',
133 repoid=pull_request.target_repo.repo_name,
141 repoid=pull_request.target_repo.repo_name,
134 pullrequestid=pull_request.pull_request_id,
142 pullrequestid=pull_request.pull_request_id,
135 reviewers=added)
143 reviewers=new_reviewers)
136 response = api_call(self.app, params)
144 response = api_call(self.app, params)
137 expected = {
145 expected = {
138 "msg": "Updated pull request `{}`".format(
146 "msg": "Updated pull request `{}`".format(
139 pull_request.pull_request_id),
147 pull_request.pull_request_id),
140 "pull_request": response.json['result']['pull_request'],
148 "pull_request": response.json['result']['pull_request'],
141 "updated_commits": {"added": [], "common": [], "removed": []},
149 "updated_commits": {"added": [], "common": [], "removed": []},
142 "updated_reviewers": {"added": added, "removed": removed},
150 "updated_reviewers": {"added": added, "removed": removed},
143 }
151 }
144
152
145 assert_ok(id_, expected, response.body)
153 assert_ok(id_, expected, response.body)
146
154
147 @pytest.mark.backends("git", "hg")
155 @pytest.mark.backends("git", "hg")
148 def test_api_update_bad_user_in_reviewers(self, pr_util):
156 def test_api_update_bad_user_in_reviewers(self, pr_util):
149 pull_request = pr_util.create_pull_request()
157 pull_request = pr_util.create_pull_request()
150
158
151 id_, params = build_data(
159 id_, params = build_data(
152 self.apikey, 'update_pull_request',
160 self.apikey, 'update_pull_request',
153 repoid=pull_request.target_repo.repo_name,
161 repoid=pull_request.target_repo.repo_name,
154 pullrequestid=pull_request.pull_request_id,
162 pullrequestid=pull_request.pull_request_id,
155 reviewers=['bad_name'])
163 reviewers=[{'username': 'bad_name'}])
156 response = api_call(self.app, params)
164 response = api_call(self.app, params)
157
165
158 expected = 'user `bad_name` does not exist'
166 expected = 'user `bad_name` does not exist'
159
167
160 assert_error(id_, expected, response.body)
168 assert_error(id_, expected, response.body)
161
169
162 @pytest.mark.backends("git", "hg")
170 @pytest.mark.backends("git", "hg")
163 def test_api_update_repo_error(self, pr_util):
171 def test_api_update_repo_error(self, pr_util):
164 id_, params = build_data(
172 id_, params = build_data(
165 self.apikey, 'update_pull_request',
173 self.apikey, 'update_pull_request',
166 repoid='fake',
174 repoid='fake',
167 pullrequestid='fake',
175 pullrequestid='fake',
168 reviewers=['bad_name'])
176 reviewers=[{'username': 'bad_name'}])
169 response = api_call(self.app, params)
177 response = api_call(self.app, params)
170
178
171 expected = 'repository `fake` does not exist'
179 expected = 'repository `fake` does not exist'
172
180
173 response_json = response.json['error']
181 response_json = response.json['error']
174 assert response_json == expected
182 assert response_json == expected
175
183
176 @pytest.mark.backends("git", "hg")
184 @pytest.mark.backends("git", "hg")
177 def test_api_update_pull_request_error(self, pr_util):
185 def test_api_update_pull_request_error(self, pr_util):
178 pull_request = pr_util.create_pull_request()
186 pull_request = pr_util.create_pull_request()
179
187
180 id_, params = build_data(
188 id_, params = build_data(
181 self.apikey, 'update_pull_request',
189 self.apikey, 'update_pull_request',
182 repoid=pull_request.target_repo.repo_name,
190 repoid=pull_request.target_repo.repo_name,
183 pullrequestid=999999,
191 pullrequestid=999999,
184 reviewers=['bad_name'])
192 reviewers=[{'username': 'bad_name'}])
185 response = api_call(self.app, params)
193 response = api_call(self.app, params)
186
194
187 expected = 'pull request `999999` does not exist'
195 expected = 'pull request `999999` does not exist'
188 assert_error(id_, expected, response.body)
196 assert_error(id_, expected, response.body)
189
197
190 @pytest.mark.backends("git", "hg")
198 @pytest.mark.backends("git", "hg")
191 def test_api_update_pull_request_no_perms_to_update(
199 def test_api_update_pull_request_no_perms_to_update(
192 self, user_util, pr_util):
200 self, user_util, pr_util):
193 user = user_util.create_user()
201 user = user_util.create_user()
194 pull_request = pr_util.create_pull_request()
202 pull_request = pr_util.create_pull_request()
195
203
196 id_, params = build_data(
204 id_, params = build_data(
197 user.api_key, 'update_pull_request',
205 user.api_key, 'update_pull_request',
198 repoid=pull_request.target_repo.repo_name,
206 repoid=pull_request.target_repo.repo_name,
199 pullrequestid=pull_request.pull_request_id,)
207 pullrequestid=pull_request.pull_request_id,)
200 response = api_call(self.app, params)
208 response = api_call(self.app, params)
201
209
202 expected = ('pull request `%s` update failed, '
210 expected = ('pull request `%s` update failed, '
203 'no permission to update.') % pull_request.pull_request_id
211 'no permission to update.') % pull_request.pull_request_id
204
212
205 assert_error(id_, expected, response.body)
213 assert_error(id_, expected, response.body)
@@ -1,730 +1,734 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error)
28 validate_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import \
39 ReviewerListSchema
37
40
38 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
39
42
40
43
41 @jsonrpc_method()
44 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
45 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
46 """
44 Get a pull request based on the given ID.
47 Get a pull request based on the given ID.
45
48
46 :param apiuser: This is filled automatically from the |authtoken|.
49 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
50 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
51 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
52 request was opened.
50 :type repoid: str or int
53 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
54 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
55 :type pullrequestid: int
53
56
54 Example output:
57 Example output:
55
58
56 .. code-block:: bash
59 .. code-block:: bash
57
60
58 "id": <id_given_in_input>,
61 "id": <id_given_in_input>,
59 "result":
62 "result":
60 {
63 {
61 "pull_request_id": "<pull_request_id>",
64 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
65 "url": "<url>",
63 "title": "<title>",
66 "title": "<title>",
64 "description": "<description>",
67 "description": "<description>",
65 "status" : "<status>",
68 "status" : "<status>",
66 "created_on": "<date_time_created>",
69 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
70 "updated_on": "<date_time_updated>",
68 "commit_ids": [
71 "commit_ids": [
69 ...
72 ...
70 "<commit_id>",
73 "<commit_id>",
71 "<commit_id>",
74 "<commit_id>",
72 ...
75 ...
73 ],
76 ],
74 "review_status": "<review_status>",
77 "review_status": "<review_status>",
75 "mergeable": {
78 "mergeable": {
76 "status": "<bool>",
79 "status": "<bool>",
77 "message": "<message>",
80 "message": "<message>",
78 },
81 },
79 "source": {
82 "source": {
80 "clone_url": "<clone_url>",
83 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
84 "repository": "<repository_name>",
82 "reference":
85 "reference":
83 {
86 {
84 "name": "<name>",
87 "name": "<name>",
85 "type": "<type>",
88 "type": "<type>",
86 "commit_id": "<commit_id>",
89 "commit_id": "<commit_id>",
87 }
90 }
88 },
91 },
89 "target": {
92 "target": {
90 "clone_url": "<clone_url>",
93 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
94 "repository": "<repository_name>",
92 "reference":
95 "reference":
93 {
96 {
94 "name": "<name>",
97 "name": "<name>",
95 "type": "<type>",
98 "type": "<type>",
96 "commit_id": "<commit_id>",
99 "commit_id": "<commit_id>",
97 }
100 }
98 },
101 },
99 "merge": {
102 "merge": {
100 "clone_url": "<clone_url>",
103 "clone_url": "<clone_url>",
101 "reference":
104 "reference":
102 {
105 {
103 "name": "<name>",
106 "name": "<name>",
104 "type": "<type>",
107 "type": "<type>",
105 "commit_id": "<commit_id>",
108 "commit_id": "<commit_id>",
106 }
109 }
107 },
110 },
108 "author": <user_obj>,
111 "author": <user_obj>,
109 "reviewers": [
112 "reviewers": [
110 ...
113 ...
111 {
114 {
112 "user": "<user_obj>",
115 "user": "<user_obj>",
113 "review_status": "<review_status>",
116 "review_status": "<review_status>",
114 }
117 }
115 ...
118 ...
116 ]
119 ]
117 },
120 },
118 "error": null
121 "error": null
119 """
122 """
120 get_repo_or_error(repoid)
123 get_repo_or_error(repoid)
121 pull_request = get_pull_request_or_error(pullrequestid)
124 pull_request = get_pull_request_or_error(pullrequestid)
122 if not PullRequestModel().check_user_read(
125 if not PullRequestModel().check_user_read(
123 pull_request, apiuser, api=True):
126 pull_request, apiuser, api=True):
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
127 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 data = pull_request.get_api_data()
128 data = pull_request.get_api_data()
126 return data
129 return data
127
130
128
131
129 @jsonrpc_method()
132 @jsonrpc_method()
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
133 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 """
134 """
132 Get all pull requests from the repository specified in `repoid`.
135 Get all pull requests from the repository specified in `repoid`.
133
136
134 :param apiuser: This is filled automatically from the |authtoken|.
137 :param apiuser: This is filled automatically from the |authtoken|.
135 :type apiuser: AuthUser
138 :type apiuser: AuthUser
136 :param repoid: Repository name or repository ID.
139 :param repoid: Repository name or repository ID.
137 :type repoid: str or int
140 :type repoid: str or int
138 :param status: Only return pull requests with the specified status.
141 :param status: Only return pull requests with the specified status.
139 Valid options are.
142 Valid options are.
140 * ``new`` (default)
143 * ``new`` (default)
141 * ``open``
144 * ``open``
142 * ``closed``
145 * ``closed``
143 :type status: str
146 :type status: str
144
147
145 Example output:
148 Example output:
146
149
147 .. code-block:: bash
150 .. code-block:: bash
148
151
149 "id": <id_given_in_input>,
152 "id": <id_given_in_input>,
150 "result":
153 "result":
151 [
154 [
152 ...
155 ...
153 {
156 {
154 "pull_request_id": "<pull_request_id>",
157 "pull_request_id": "<pull_request_id>",
155 "url": "<url>",
158 "url": "<url>",
156 "title" : "<title>",
159 "title" : "<title>",
157 "description": "<description>",
160 "description": "<description>",
158 "status": "<status>",
161 "status": "<status>",
159 "created_on": "<date_time_created>",
162 "created_on": "<date_time_created>",
160 "updated_on": "<date_time_updated>",
163 "updated_on": "<date_time_updated>",
161 "commit_ids": [
164 "commit_ids": [
162 ...
165 ...
163 "<commit_id>",
166 "<commit_id>",
164 "<commit_id>",
167 "<commit_id>",
165 ...
168 ...
166 ],
169 ],
167 "review_status": "<review_status>",
170 "review_status": "<review_status>",
168 "mergeable": {
171 "mergeable": {
169 "status": "<bool>",
172 "status": "<bool>",
170 "message: "<message>",
173 "message: "<message>",
171 },
174 },
172 "source": {
175 "source": {
173 "clone_url": "<clone_url>",
176 "clone_url": "<clone_url>",
174 "reference":
177 "reference":
175 {
178 {
176 "name": "<name>",
179 "name": "<name>",
177 "type": "<type>",
180 "type": "<type>",
178 "commit_id": "<commit_id>",
181 "commit_id": "<commit_id>",
179 }
182 }
180 },
183 },
181 "target": {
184 "target": {
182 "clone_url": "<clone_url>",
185 "clone_url": "<clone_url>",
183 "reference":
186 "reference":
184 {
187 {
185 "name": "<name>",
188 "name": "<name>",
186 "type": "<type>",
189 "type": "<type>",
187 "commit_id": "<commit_id>",
190 "commit_id": "<commit_id>",
188 }
191 }
189 },
192 },
190 "merge": {
193 "merge": {
191 "clone_url": "<clone_url>",
194 "clone_url": "<clone_url>",
192 "reference":
195 "reference":
193 {
196 {
194 "name": "<name>",
197 "name": "<name>",
195 "type": "<type>",
198 "type": "<type>",
196 "commit_id": "<commit_id>",
199 "commit_id": "<commit_id>",
197 }
200 }
198 },
201 },
199 "author": <user_obj>,
202 "author": <user_obj>,
200 "reviewers": [
203 "reviewers": [
201 ...
204 ...
202 {
205 {
203 "user": "<user_obj>",
206 "user": "<user_obj>",
204 "review_status": "<review_status>",
207 "review_status": "<review_status>",
205 }
208 }
206 ...
209 ...
207 ]
210 ]
208 }
211 }
209 ...
212 ...
210 ],
213 ],
211 "error": null
214 "error": null
212
215
213 """
216 """
214 repo = get_repo_or_error(repoid)
217 repo = get_repo_or_error(repoid)
215 if not has_superadmin_permission(apiuser):
218 if not has_superadmin_permission(apiuser):
216 _perms = (
219 _perms = (
217 'repository.admin', 'repository.write', 'repository.read',)
220 'repository.admin', 'repository.write', 'repository.read',)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
221 validate_repo_permissions(apiuser, repoid, repo, _perms)
219
222
220 status = Optional.extract(status)
223 status = Optional.extract(status)
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
224 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 data = [pr.get_api_data() for pr in pull_requests]
225 data = [pr.get_api_data() for pr in pull_requests]
223 return data
226 return data
224
227
225
228
226 @jsonrpc_method()
229 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
230 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 userid=Optional(OAttr('apiuser'))):
231 userid=Optional(OAttr('apiuser'))):
229 """
232 """
230 Merge the pull request specified by `pullrequestid` into its target
233 Merge the pull request specified by `pullrequestid` into its target
231 repository.
234 repository.
232
235
233 :param apiuser: This is filled automatically from the |authtoken|.
236 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
237 :type apiuser: AuthUser
235 :param repoid: The Repository name or repository ID of the
238 :param repoid: The Repository name or repository ID of the
236 target repository to which the |pr| is to be merged.
239 target repository to which the |pr| is to be merged.
237 :type repoid: str or int
240 :type repoid: str or int
238 :param pullrequestid: ID of the pull request which shall be merged.
241 :param pullrequestid: ID of the pull request which shall be merged.
239 :type pullrequestid: int
242 :type pullrequestid: int
240 :param userid: Merge the pull request as this user.
243 :param userid: Merge the pull request as this user.
241 :type userid: Optional(str or int)
244 :type userid: Optional(str or int)
242
245
243 Example output:
246 Example output:
244
247
245 .. code-block:: bash
248 .. code-block:: bash
246
249
247 "id": <id_given_in_input>,
250 "id": <id_given_in_input>,
248 "result": {
251 "result": {
249 "executed": "<bool>",
252 "executed": "<bool>",
250 "failure_reason": "<int>",
253 "failure_reason": "<int>",
251 "merge_commit_id": "<merge_commit_id>",
254 "merge_commit_id": "<merge_commit_id>",
252 "possible": "<bool>",
255 "possible": "<bool>",
253 "merge_ref": {
256 "merge_ref": {
254 "commit_id": "<commit_id>",
257 "commit_id": "<commit_id>",
255 "type": "<type>",
258 "type": "<type>",
256 "name": "<name>"
259 "name": "<name>"
257 }
260 }
258 },
261 },
259 "error": null
262 "error": null
260 """
263 """
261 repo = get_repo_or_error(repoid)
264 repo = get_repo_or_error(repoid)
262 if not isinstance(userid, Optional):
265 if not isinstance(userid, Optional):
263 if (has_superadmin_permission(apiuser) or
266 if (has_superadmin_permission(apiuser) or
264 HasRepoPermissionAnyApi('repository.admin')(
267 HasRepoPermissionAnyApi('repository.admin')(
265 user=apiuser, repo_name=repo.repo_name)):
268 user=apiuser, repo_name=repo.repo_name)):
266 apiuser = get_user_or_error(userid)
269 apiuser = get_user_or_error(userid)
267 else:
270 else:
268 raise JSONRPCError('userid is not the same as your user')
271 raise JSONRPCError('userid is not the same as your user')
269
272
270 pull_request = get_pull_request_or_error(pullrequestid)
273 pull_request = get_pull_request_or_error(pullrequestid)
271
274
272 check = MergeCheck.validate(pull_request, user=apiuser)
275 check = MergeCheck.validate(pull_request, user=apiuser)
273 merge_possible = not check.failed
276 merge_possible = not check.failed
274
277
275 if not merge_possible:
278 if not merge_possible:
276 error_messages = []
279 error_messages = []
277 for err_type, error_msg in check.errors:
280 for err_type, error_msg in check.errors:
278 error_msg = request.translate(error_msg)
281 error_msg = request.translate(error_msg)
279 error_messages.append(error_msg)
282 error_messages.append(error_msg)
280
283
281 reasons = ','.join(error_messages)
284 reasons = ','.join(error_messages)
282 raise JSONRPCError(
285 raise JSONRPCError(
283 'merge not possible for following reasons: {}'.format(reasons))
286 'merge not possible for following reasons: {}'.format(reasons))
284
287
285 target_repo = pull_request.target_repo
288 target_repo = pull_request.target_repo
286 extras = vcs_operation_context(
289 extras = vcs_operation_context(
287 request.environ, repo_name=target_repo.repo_name,
290 request.environ, repo_name=target_repo.repo_name,
288 username=apiuser.username, action='push',
291 username=apiuser.username, action='push',
289 scm=target_repo.repo_type)
292 scm=target_repo.repo_type)
290 merge_response = PullRequestModel().merge(
293 merge_response = PullRequestModel().merge(
291 pull_request, apiuser, extras=extras)
294 pull_request, apiuser, extras=extras)
292 if merge_response.executed:
295 if merge_response.executed:
293 PullRequestModel().close_pull_request(
296 PullRequestModel().close_pull_request(
294 pull_request.pull_request_id, apiuser)
297 pull_request.pull_request_id, apiuser)
295
298
296 Session().commit()
299 Session().commit()
297
300
298 # In previous versions the merge response directly contained the merge
301 # In previous versions the merge response directly contained the merge
299 # commit id. It is now contained in the merge reference object. To be
302 # commit id. It is now contained in the merge reference object. To be
300 # backwards compatible we have to extract it again.
303 # backwards compatible we have to extract it again.
301 merge_response = merge_response._asdict()
304 merge_response = merge_response._asdict()
302 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
305 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
303
306
304 return merge_response
307 return merge_response
305
308
306
309
307 @jsonrpc_method()
310 @jsonrpc_method()
308 def close_pull_request(request, apiuser, repoid, pullrequestid,
311 def close_pull_request(request, apiuser, repoid, pullrequestid,
309 userid=Optional(OAttr('apiuser'))):
312 userid=Optional(OAttr('apiuser'))):
310 """
313 """
311 Close the pull request specified by `pullrequestid`.
314 Close the pull request specified by `pullrequestid`.
312
315
313 :param apiuser: This is filled automatically from the |authtoken|.
316 :param apiuser: This is filled automatically from the |authtoken|.
314 :type apiuser: AuthUser
317 :type apiuser: AuthUser
315 :param repoid: Repository name or repository ID to which the pull
318 :param repoid: Repository name or repository ID to which the pull
316 request belongs.
319 request belongs.
317 :type repoid: str or int
320 :type repoid: str or int
318 :param pullrequestid: ID of the pull request to be closed.
321 :param pullrequestid: ID of the pull request to be closed.
319 :type pullrequestid: int
322 :type pullrequestid: int
320 :param userid: Close the pull request as this user.
323 :param userid: Close the pull request as this user.
321 :type userid: Optional(str or int)
324 :type userid: Optional(str or int)
322
325
323 Example output:
326 Example output:
324
327
325 .. code-block:: bash
328 .. code-block:: bash
326
329
327 "id": <id_given_in_input>,
330 "id": <id_given_in_input>,
328 "result": {
331 "result": {
329 "pull_request_id": "<int>",
332 "pull_request_id": "<int>",
330 "closed": "<bool>"
333 "closed": "<bool>"
331 },
334 },
332 "error": null
335 "error": null
333
336
334 """
337 """
335 repo = get_repo_or_error(repoid)
338 repo = get_repo_or_error(repoid)
336 if not isinstance(userid, Optional):
339 if not isinstance(userid, Optional):
337 if (has_superadmin_permission(apiuser) or
340 if (has_superadmin_permission(apiuser) or
338 HasRepoPermissionAnyApi('repository.admin')(
341 HasRepoPermissionAnyApi('repository.admin')(
339 user=apiuser, repo_name=repo.repo_name)):
342 user=apiuser, repo_name=repo.repo_name)):
340 apiuser = get_user_or_error(userid)
343 apiuser = get_user_or_error(userid)
341 else:
344 else:
342 raise JSONRPCError('userid is not the same as your user')
345 raise JSONRPCError('userid is not the same as your user')
343
346
344 pull_request = get_pull_request_or_error(pullrequestid)
347 pull_request = get_pull_request_or_error(pullrequestid)
345 if not PullRequestModel().check_user_update(
348 if not PullRequestModel().check_user_update(
346 pull_request, apiuser, api=True):
349 pull_request, apiuser, api=True):
347 raise JSONRPCError(
350 raise JSONRPCError(
348 'pull request `%s` close failed, no permission to close.' % (
351 'pull request `%s` close failed, no permission to close.' % (
349 pullrequestid,))
352 pullrequestid,))
350 if pull_request.is_closed():
353 if pull_request.is_closed():
351 raise JSONRPCError(
354 raise JSONRPCError(
352 'pull request `%s` is already closed' % (pullrequestid,))
355 'pull request `%s` is already closed' % (pullrequestid,))
353
356
354 PullRequestModel().close_pull_request(
357 PullRequestModel().close_pull_request(
355 pull_request.pull_request_id, apiuser)
358 pull_request.pull_request_id, apiuser)
356 Session().commit()
359 Session().commit()
357 data = {
360 data = {
358 'pull_request_id': pull_request.pull_request_id,
361 'pull_request_id': pull_request.pull_request_id,
359 'closed': True,
362 'closed': True,
360 }
363 }
361 return data
364 return data
362
365
363
366
364 @jsonrpc_method()
367 @jsonrpc_method()
365 def comment_pull_request(
368 def comment_pull_request(
366 request, apiuser, repoid, pullrequestid, message=Optional(None),
369 request, apiuser, repoid, pullrequestid, message=Optional(None),
367 commit_id=Optional(None), status=Optional(None),
370 commit_id=Optional(None), status=Optional(None),
368 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
371 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
369 resolves_comment_id=Optional(None),
372 resolves_comment_id=Optional(None),
370 userid=Optional(OAttr('apiuser'))):
373 userid=Optional(OAttr('apiuser'))):
371 """
374 """
372 Comment on the pull request specified with the `pullrequestid`,
375 Comment on the pull request specified with the `pullrequestid`,
373 in the |repo| specified by the `repoid`, and optionally change the
376 in the |repo| specified by the `repoid`, and optionally change the
374 review status.
377 review status.
375
378
376 :param apiuser: This is filled automatically from the |authtoken|.
379 :param apiuser: This is filled automatically from the |authtoken|.
377 :type apiuser: AuthUser
380 :type apiuser: AuthUser
378 :param repoid: The repository name or repository ID.
381 :param repoid: The repository name or repository ID.
379 :type repoid: str or int
382 :type repoid: str or int
380 :param pullrequestid: The pull request ID.
383 :param pullrequestid: The pull request ID.
381 :type pullrequestid: int
384 :type pullrequestid: int
382 :param commit_id: Specify the commit_id for which to set a comment. If
385 :param commit_id: Specify the commit_id for which to set a comment. If
383 given commit_id is different than latest in the PR status
386 given commit_id is different than latest in the PR status
384 change won't be performed.
387 change won't be performed.
385 :type commit_id: str
388 :type commit_id: str
386 :param message: The text content of the comment.
389 :param message: The text content of the comment.
387 :type message: str
390 :type message: str
388 :param status: (**Optional**) Set the approval status of the pull
391 :param status: (**Optional**) Set the approval status of the pull
389 request. One of: 'not_reviewed', 'approved', 'rejected',
392 request. One of: 'not_reviewed', 'approved', 'rejected',
390 'under_review'
393 'under_review'
391 :type status: str
394 :type status: str
392 :param comment_type: Comment type, one of: 'note', 'todo'
395 :param comment_type: Comment type, one of: 'note', 'todo'
393 :type comment_type: Optional(str), default: 'note'
396 :type comment_type: Optional(str), default: 'note'
394 :param userid: Comment on the pull request as this user
397 :param userid: Comment on the pull request as this user
395 :type userid: Optional(str or int)
398 :type userid: Optional(str or int)
396
399
397 Example output:
400 Example output:
398
401
399 .. code-block:: bash
402 .. code-block:: bash
400
403
401 id : <id_given_in_input>
404 id : <id_given_in_input>
402 result : {
405 result : {
403 "pull_request_id": "<Integer>",
406 "pull_request_id": "<Integer>",
404 "comment_id": "<Integer>",
407 "comment_id": "<Integer>",
405 "status": {"given": <given_status>,
408 "status": {"given": <given_status>,
406 "was_changed": <bool status_was_actually_changed> },
409 "was_changed": <bool status_was_actually_changed> },
407 },
410 },
408 error : null
411 error : null
409 """
412 """
410 repo = get_repo_or_error(repoid)
413 repo = get_repo_or_error(repoid)
411 if not isinstance(userid, Optional):
414 if not isinstance(userid, Optional):
412 if (has_superadmin_permission(apiuser) or
415 if (has_superadmin_permission(apiuser) or
413 HasRepoPermissionAnyApi('repository.admin')(
416 HasRepoPermissionAnyApi('repository.admin')(
414 user=apiuser, repo_name=repo.repo_name)):
417 user=apiuser, repo_name=repo.repo_name)):
415 apiuser = get_user_or_error(userid)
418 apiuser = get_user_or_error(userid)
416 else:
419 else:
417 raise JSONRPCError('userid is not the same as your user')
420 raise JSONRPCError('userid is not the same as your user')
418
421
419 pull_request = get_pull_request_or_error(pullrequestid)
422 pull_request = get_pull_request_or_error(pullrequestid)
420 if not PullRequestModel().check_user_read(
423 if not PullRequestModel().check_user_read(
421 pull_request, apiuser, api=True):
424 pull_request, apiuser, api=True):
422 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
423 message = Optional.extract(message)
426 message = Optional.extract(message)
424 status = Optional.extract(status)
427 status = Optional.extract(status)
425 commit_id = Optional.extract(commit_id)
428 commit_id = Optional.extract(commit_id)
426 comment_type = Optional.extract(comment_type)
429 comment_type = Optional.extract(comment_type)
427 resolves_comment_id = Optional.extract(resolves_comment_id)
430 resolves_comment_id = Optional.extract(resolves_comment_id)
428
431
429 if not message and not status:
432 if not message and not status:
430 raise JSONRPCError(
433 raise JSONRPCError(
431 'Both message and status parameters are missing. '
434 'Both message and status parameters are missing. '
432 'At least one is required.')
435 'At least one is required.')
433
436
434 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
437 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
435 status is not None):
438 status is not None):
436 raise JSONRPCError('Unknown comment status: `%s`' % status)
439 raise JSONRPCError('Unknown comment status: `%s`' % status)
437
440
438 if commit_id and commit_id not in pull_request.revisions:
441 if commit_id and commit_id not in pull_request.revisions:
439 raise JSONRPCError(
442 raise JSONRPCError(
440 'Invalid commit_id `%s` for this pull request.' % commit_id)
443 'Invalid commit_id `%s` for this pull request.' % commit_id)
441
444
442 allowed_to_change_status = PullRequestModel().check_user_change_status(
445 allowed_to_change_status = PullRequestModel().check_user_change_status(
443 pull_request, apiuser)
446 pull_request, apiuser)
444
447
445 # if commit_id is passed re-validated if user is allowed to change status
448 # if commit_id is passed re-validated if user is allowed to change status
446 # based on latest commit_id from the PR
449 # based on latest commit_id from the PR
447 if commit_id:
450 if commit_id:
448 commit_idx = pull_request.revisions.index(commit_id)
451 commit_idx = pull_request.revisions.index(commit_id)
449 if commit_idx != 0:
452 if commit_idx != 0:
450 allowed_to_change_status = False
453 allowed_to_change_status = False
451
454
452 if resolves_comment_id:
455 if resolves_comment_id:
453 comment = ChangesetComment.get(resolves_comment_id)
456 comment = ChangesetComment.get(resolves_comment_id)
454 if not comment:
457 if not comment:
455 raise JSONRPCError(
458 raise JSONRPCError(
456 'Invalid resolves_comment_id `%s` for this pull request.'
459 'Invalid resolves_comment_id `%s` for this pull request.'
457 % resolves_comment_id)
460 % resolves_comment_id)
458 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
461 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
459 raise JSONRPCError(
462 raise JSONRPCError(
460 'Comment `%s` is wrong type for setting status to resolved.'
463 'Comment `%s` is wrong type for setting status to resolved.'
461 % resolves_comment_id)
464 % resolves_comment_id)
462
465
463 text = message
466 text = message
464 status_label = ChangesetStatus.get_status_lbl(status)
467 status_label = ChangesetStatus.get_status_lbl(status)
465 if status and allowed_to_change_status:
468 if status and allowed_to_change_status:
466 st_message = ('Status change %(transition_icon)s %(status)s'
469 st_message = ('Status change %(transition_icon)s %(status)s'
467 % {'transition_icon': '>', 'status': status_label})
470 % {'transition_icon': '>', 'status': status_label})
468 text = message or st_message
471 text = message or st_message
469
472
470 rc_config = SettingsModel().get_all_settings()
473 rc_config = SettingsModel().get_all_settings()
471 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
474 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
472
475
473 status_change = status and allowed_to_change_status
476 status_change = status and allowed_to_change_status
474 comment = CommentsModel().create(
477 comment = CommentsModel().create(
475 text=text,
478 text=text,
476 repo=pull_request.target_repo.repo_id,
479 repo=pull_request.target_repo.repo_id,
477 user=apiuser.user_id,
480 user=apiuser.user_id,
478 pull_request=pull_request.pull_request_id,
481 pull_request=pull_request.pull_request_id,
479 f_path=None,
482 f_path=None,
480 line_no=None,
483 line_no=None,
481 status_change=(status_label if status_change else None),
484 status_change=(status_label if status_change else None),
482 status_change_type=(status if status_change else None),
485 status_change_type=(status if status_change else None),
483 closing_pr=False,
486 closing_pr=False,
484 renderer=renderer,
487 renderer=renderer,
485 comment_type=comment_type,
488 comment_type=comment_type,
486 resolves_comment_id=resolves_comment_id
489 resolves_comment_id=resolves_comment_id
487 )
490 )
488
491
489 if allowed_to_change_status and status:
492 if allowed_to_change_status and status:
490 ChangesetStatusModel().set_status(
493 ChangesetStatusModel().set_status(
491 pull_request.target_repo.repo_id,
494 pull_request.target_repo.repo_id,
492 status,
495 status,
493 apiuser.user_id,
496 apiuser.user_id,
494 comment,
497 comment,
495 pull_request=pull_request.pull_request_id
498 pull_request=pull_request.pull_request_id
496 )
499 )
497 Session().flush()
500 Session().flush()
498
501
499 Session().commit()
502 Session().commit()
500 data = {
503 data = {
501 'pull_request_id': pull_request.pull_request_id,
504 'pull_request_id': pull_request.pull_request_id,
502 'comment_id': comment.comment_id if comment else None,
505 'comment_id': comment.comment_id if comment else None,
503 'status': {'given': status, 'was_changed': status_change},
506 'status': {'given': status, 'was_changed': status_change},
504 }
507 }
505 return data
508 return data
506
509
507
510
508 @jsonrpc_method()
511 @jsonrpc_method()
509 def create_pull_request(
512 def create_pull_request(
510 request, apiuser, source_repo, target_repo, source_ref, target_ref,
513 request, apiuser, source_repo, target_repo, source_ref, target_ref,
511 title, description=Optional(''), reviewers=Optional(None)):
514 title, description=Optional(''), reviewers=Optional(None)):
512 """
515 """
513 Creates a new pull request.
516 Creates a new pull request.
514
517
515 Accepts refs in the following formats:
518 Accepts refs in the following formats:
516
519
517 * branch:<branch_name>:<sha>
520 * branch:<branch_name>:<sha>
518 * branch:<branch_name>
521 * branch:<branch_name>
519 * bookmark:<bookmark_name>:<sha> (Mercurial only)
522 * bookmark:<bookmark_name>:<sha> (Mercurial only)
520 * bookmark:<bookmark_name> (Mercurial only)
523 * bookmark:<bookmark_name> (Mercurial only)
521
524
522 :param apiuser: This is filled automatically from the |authtoken|.
525 :param apiuser: This is filled automatically from the |authtoken|.
523 :type apiuser: AuthUser
526 :type apiuser: AuthUser
524 :param source_repo: Set the source repository name.
527 :param source_repo: Set the source repository name.
525 :type source_repo: str
528 :type source_repo: str
526 :param target_repo: Set the target repository name.
529 :param target_repo: Set the target repository name.
527 :type target_repo: str
530 :type target_repo: str
528 :param source_ref: Set the source ref name.
531 :param source_ref: Set the source ref name.
529 :type source_ref: str
532 :type source_ref: str
530 :param target_ref: Set the target ref name.
533 :param target_ref: Set the target ref name.
531 :type target_ref: str
534 :type target_ref: str
532 :param title: Set the pull request title.
535 :param title: Set the pull request title.
533 :type title: str
536 :type title: str
534 :param description: Set the pull request description.
537 :param description: Set the pull request description.
535 :type description: Optional(str)
538 :type description: Optional(str)
536 :param reviewers: Set the new pull request reviewers list.
539 :param reviewers: Set the new pull request reviewers list.
537 :type reviewers: Optional(list)
540 :type reviewers: Optional(list)
538 Accepts username strings or objects of the format:
541 Accepts username strings or objects of the format:
539
542
540 {'username': 'nick', 'reasons': ['original author']}
543 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
541 """
544 """
542
545
543 source = get_repo_or_error(source_repo)
546 source = get_repo_or_error(source_repo)
544 target = get_repo_or_error(target_repo)
547 target = get_repo_or_error(target_repo)
545 if not has_superadmin_permission(apiuser):
548 if not has_superadmin_permission(apiuser):
546 _perms = ('repository.admin', 'repository.write', 'repository.read',)
549 _perms = ('repository.admin', 'repository.write', 'repository.read',)
547 validate_repo_permissions(apiuser, source_repo, source, _perms)
550 validate_repo_permissions(apiuser, source_repo, source, _perms)
548
551
549 full_source_ref = resolve_ref_or_error(source_ref, source)
552 full_source_ref = resolve_ref_or_error(source_ref, source)
550 full_target_ref = resolve_ref_or_error(target_ref, target)
553 full_target_ref = resolve_ref_or_error(target_ref, target)
551 source_commit = get_commit_or_error(full_source_ref, source)
554 source_commit = get_commit_or_error(full_source_ref, source)
552 target_commit = get_commit_or_error(full_target_ref, target)
555 target_commit = get_commit_or_error(full_target_ref, target)
553 source_scm = source.scm_instance()
556 source_scm = source.scm_instance()
554 target_scm = target.scm_instance()
557 target_scm = target.scm_instance()
555
558
556 commit_ranges = target_scm.compare(
559 commit_ranges = target_scm.compare(
557 target_commit.raw_id, source_commit.raw_id, source_scm,
560 target_commit.raw_id, source_commit.raw_id, source_scm,
558 merge=True, pre_load=[])
561 merge=True, pre_load=[])
559
562
560 ancestor = target_scm.get_common_ancestor(
563 ancestor = target_scm.get_common_ancestor(
561 target_commit.raw_id, source_commit.raw_id, source_scm)
564 target_commit.raw_id, source_commit.raw_id, source_scm)
562
565
563 if not commit_ranges:
566 if not commit_ranges:
564 raise JSONRPCError('no commits found')
567 raise JSONRPCError('no commits found')
565
568
566 if not ancestor:
569 if not ancestor:
567 raise JSONRPCError('no common ancestor found')
570 raise JSONRPCError('no common ancestor found')
568
571
569 reviewer_objects = Optional.extract(reviewers) or []
572 reviewer_objects = Optional.extract(reviewers) or []
570 if not isinstance(reviewer_objects, list):
573 if reviewer_objects:
571 raise JSONRPCError('reviewers should be specified as a list')
574 schema = ReviewerListSchema()
575 try:
576 reviewer_objects = schema.deserialize(reviewer_objects)
577 except Invalid as err:
578 raise JSONRPCValidationError(colander_exc=err)
572
579
573 reviewers_reasons = []
580 reviewers = []
574 for reviewer_object in reviewer_objects:
581 for reviewer_object in reviewer_objects:
575 reviewer_reasons = []
582 user = get_user_or_error(reviewer_object['username'])
576 if isinstance(reviewer_object, (basestring, int)):
583 reasons = reviewer_object['reasons']
577 reviewer_username = reviewer_object
584 mandatory = reviewer_object['mandatory']
578 else:
585 reviewers.append((user.user_id, reasons, mandatory))
579 reviewer_username = reviewer_object['username']
580 reviewer_reasons = reviewer_object.get('reasons', [])
581
582 user = get_user_or_error(reviewer_username)
583 reviewers_reasons.append((user.user_id, reviewer_reasons))
584
586
585 pull_request_model = PullRequestModel()
587 pull_request_model = PullRequestModel()
586 pull_request = pull_request_model.create(
588 pull_request = pull_request_model.create(
587 created_by=apiuser.user_id,
589 created_by=apiuser.user_id,
588 source_repo=source_repo,
590 source_repo=source_repo,
589 source_ref=full_source_ref,
591 source_ref=full_source_ref,
590 target_repo=target_repo,
592 target_repo=target_repo,
591 target_ref=full_target_ref,
593 target_ref=full_target_ref,
592 revisions=reversed(
594 revisions=reversed(
593 [commit.raw_id for commit in reversed(commit_ranges)]),
595 [commit.raw_id for commit in reversed(commit_ranges)]),
594 reviewers=reviewers_reasons,
596 reviewers=reviewers,
595 title=title,
597 title=title,
596 description=Optional.extract(description)
598 description=Optional.extract(description)
597 )
599 )
598
600
599 Session().commit()
601 Session().commit()
600 data = {
602 data = {
601 'msg': 'Created new pull request `{}`'.format(title),
603 'msg': 'Created new pull request `{}`'.format(title),
602 'pull_request_id': pull_request.pull_request_id,
604 'pull_request_id': pull_request.pull_request_id,
603 }
605 }
604 return data
606 return data
605
607
606
608
607 @jsonrpc_method()
609 @jsonrpc_method()
608 def update_pull_request(
610 def update_pull_request(
609 request, apiuser, repoid, pullrequestid, title=Optional(''),
611 request, apiuser, repoid, pullrequestid, title=Optional(''),
610 description=Optional(''), reviewers=Optional(None),
612 description=Optional(''), reviewers=Optional(None),
611 update_commits=Optional(None), close_pull_request=Optional(None)):
613 update_commits=Optional(None), close_pull_request=Optional(None)):
612 """
614 """
613 Updates a pull request.
615 Updates a pull request.
614
616
615 :param apiuser: This is filled automatically from the |authtoken|.
617 :param apiuser: This is filled automatically from the |authtoken|.
616 :type apiuser: AuthUser
618 :type apiuser: AuthUser
617 :param repoid: The repository name or repository ID.
619 :param repoid: The repository name or repository ID.
618 :type repoid: str or int
620 :type repoid: str or int
619 :param pullrequestid: The pull request ID.
621 :param pullrequestid: The pull request ID.
620 :type pullrequestid: int
622 :type pullrequestid: int
621 :param title: Set the pull request title.
623 :param title: Set the pull request title.
622 :type title: str
624 :type title: str
623 :param description: Update pull request description.
625 :param description: Update pull request description.
624 :type description: Optional(str)
626 :type description: Optional(str)
625 :param reviewers: Update pull request reviewers list with new value.
627 :param reviewers: Update pull request reviewers list with new value.
626 :type reviewers: Optional(list)
628 :type reviewers: Optional(list)
629 Accepts username strings or objects of the format:
630
631 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
632
627 :param update_commits: Trigger update of commits for this pull request
633 :param update_commits: Trigger update of commits for this pull request
628 :type: update_commits: Optional(bool)
634 :type: update_commits: Optional(bool)
629 :param close_pull_request: Close this pull request with rejected state
635 :param close_pull_request: Close this pull request with rejected state
630 :type: close_pull_request: Optional(bool)
636 :type: close_pull_request: Optional(bool)
631
637
632 Example output:
638 Example output:
633
639
634 .. code-block:: bash
640 .. code-block:: bash
635
641
636 id : <id_given_in_input>
642 id : <id_given_in_input>
637 result : {
643 result : {
638 "msg": "Updated pull request `63`",
644 "msg": "Updated pull request `63`",
639 "pull_request": <pull_request_object>,
645 "pull_request": <pull_request_object>,
640 "updated_reviewers": {
646 "updated_reviewers": {
641 "added": [
647 "added": [
642 "username"
648 "username"
643 ],
649 ],
644 "removed": []
650 "removed": []
645 },
651 },
646 "updated_commits": {
652 "updated_commits": {
647 "added": [
653 "added": [
648 "<sha1_hash>"
654 "<sha1_hash>"
649 ],
655 ],
650 "common": [
656 "common": [
651 "<sha1_hash>",
657 "<sha1_hash>",
652 "<sha1_hash>",
658 "<sha1_hash>",
653 ],
659 ],
654 "removed": []
660 "removed": []
655 }
661 }
656 }
662 }
657 error : null
663 error : null
658 """
664 """
659
665
660 repo = get_repo_or_error(repoid)
666 repo = get_repo_or_error(repoid)
661 pull_request = get_pull_request_or_error(pullrequestid)
667 pull_request = get_pull_request_or_error(pullrequestid)
662 if not PullRequestModel().check_user_update(
668 if not PullRequestModel().check_user_update(
663 pull_request, apiuser, api=True):
669 pull_request, apiuser, api=True):
664 raise JSONRPCError(
670 raise JSONRPCError(
665 'pull request `%s` update failed, no permission to update.' % (
671 'pull request `%s` update failed, no permission to update.' % (
666 pullrequestid,))
672 pullrequestid,))
667 if pull_request.is_closed():
673 if pull_request.is_closed():
668 raise JSONRPCError(
674 raise JSONRPCError(
669 'pull request `%s` update failed, pull request is closed' % (
675 'pull request `%s` update failed, pull request is closed' % (
670 pullrequestid,))
676 pullrequestid,))
671
677
678
672 reviewer_objects = Optional.extract(reviewers) or []
679 reviewer_objects = Optional.extract(reviewers) or []
673 if not isinstance(reviewer_objects, list):
680 if reviewer_objects:
674 raise JSONRPCError('reviewers should be specified as a list')
681 schema = ReviewerListSchema()
682 try:
683 reviewer_objects = schema.deserialize(reviewer_objects)
684 except Invalid as err:
685 raise JSONRPCValidationError(colander_exc=err)
675
686
676 reviewers_reasons = []
687 reviewers = []
677 reviewer_ids = set()
678 for reviewer_object in reviewer_objects:
688 for reviewer_object in reviewer_objects:
679 reviewer_reasons = []
689 user = get_user_or_error(reviewer_object['username'])
680 if isinstance(reviewer_object, (int, basestring)):
690 reasons = reviewer_object['reasons']
681 reviewer_username = reviewer_object
691 mandatory = reviewer_object['mandatory']
682 else:
692 reviewers.append((user.user_id, reasons, mandatory))
683 reviewer_username = reviewer_object['username']
684 reviewer_reasons = reviewer_object.get('reasons', [])
685
686 user = get_user_or_error(reviewer_username)
687 reviewer_ids.add(user.user_id)
688 reviewers_reasons.append((user.user_id, reviewer_reasons))
689
693
690 title = Optional.extract(title)
694 title = Optional.extract(title)
691 description = Optional.extract(description)
695 description = Optional.extract(description)
692 if title or description:
696 if title or description:
693 PullRequestModel().edit(
697 PullRequestModel().edit(
694 pull_request, title or pull_request.title,
698 pull_request, title or pull_request.title,
695 description or pull_request.description)
699 description or pull_request.description)
696 Session().commit()
700 Session().commit()
697
701
698 commit_changes = {"added": [], "common": [], "removed": []}
702 commit_changes = {"added": [], "common": [], "removed": []}
699 if str2bool(Optional.extract(update_commits)):
703 if str2bool(Optional.extract(update_commits)):
700 if PullRequestModel().has_valid_update_type(pull_request):
704 if PullRequestModel().has_valid_update_type(pull_request):
701 update_response = PullRequestModel().update_commits(
705 update_response = PullRequestModel().update_commits(
702 pull_request)
706 pull_request)
703 commit_changes = update_response.changes or commit_changes
707 commit_changes = update_response.changes or commit_changes
704 Session().commit()
708 Session().commit()
705
709
706 reviewers_changes = {"added": [], "removed": []}
710 reviewers_changes = {"added": [], "removed": []}
707 if reviewer_ids:
711 if reviewers:
708 added_reviewers, removed_reviewers = \
712 added_reviewers, removed_reviewers = \
709 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
713 PullRequestModel().update_reviewers(pull_request, reviewers)
710
714
711 reviewers_changes['added'] = sorted(
715 reviewers_changes['added'] = sorted(
712 [get_user_or_error(n).username for n in added_reviewers])
716 [get_user_or_error(n).username for n in added_reviewers])
713 reviewers_changes['removed'] = sorted(
717 reviewers_changes['removed'] = sorted(
714 [get_user_or_error(n).username for n in removed_reviewers])
718 [get_user_or_error(n).username for n in removed_reviewers])
715 Session().commit()
719 Session().commit()
716
720
717 if str2bool(Optional.extract(close_pull_request)):
721 if str2bool(Optional.extract(close_pull_request)):
718 PullRequestModel().close_pull_request_with_comment(
722 PullRequestModel().close_pull_request_with_comment(
719 pull_request, apiuser, repo)
723 pull_request, apiuser, repo)
720 Session().commit()
724 Session().commit()
721
725
722 data = {
726 data = {
723 'msg': 'Updated pull request `{}`'.format(
727 'msg': 'Updated pull request `{}`'.format(
724 pull_request.pull_request_id),
728 pull_request.pull_request_id),
725 'pull_request': pull_request.get_api_data(),
729 'pull_request': pull_request.get_api_data(),
726 'updated_commits': commit_changes,
730 'updated_commits': commit_changes,
727 'updated_reviewers': reviewers_changes
731 'updated_reviewers': reviewers_changes
728 }
732 }
729
733
730 return data
734 return data
@@ -1,301 +1,308 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 from pylons import tmpl_context as c
23 from pylons import tmpl_context as c
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils import PartialRenderer
27 from rhodecode.lib.utils import PartialRenderer
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.model import repo
31 from rhodecode.model import repo
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33 from rhodecode.model.scm import ScmModel
33 from rhodecode.model.scm import ScmModel
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 ADMIN_PREFIX = '/_admin'
38 ADMIN_PREFIX = '/_admin'
39 STATIC_FILE_PREFIX = '/_static'
39 STATIC_FILE_PREFIX = '/_static'
40
40
41
41
42 def get_format_ref_id(repo):
42 def get_format_ref_id(repo):
43 """Returns a `repo` specific reference formatter function"""
43 """Returns a `repo` specific reference formatter function"""
44 if h.is_svn(repo):
44 if h.is_svn(repo):
45 return _format_ref_id_svn
45 return _format_ref_id_svn
46 else:
46 else:
47 return _format_ref_id
47 return _format_ref_id
48
48
49
49
50 def _format_ref_id(name, raw_id):
50 def _format_ref_id(name, raw_id):
51 """Default formatting of a given reference `name`"""
51 """Default formatting of a given reference `name`"""
52 return name
52 return name
53
53
54
54
55 def _format_ref_id_svn(name, raw_id):
55 def _format_ref_id_svn(name, raw_id):
56 """Special way of formatting a reference for Subversion including path"""
56 """Special way of formatting a reference for Subversion including path"""
57 return '%s@%s' % (name, raw_id)
57 return '%s@%s' % (name, raw_id)
58
58
59
59
60 class TemplateArgs(StrictAttributeDict):
60 class TemplateArgs(StrictAttributeDict):
61 pass
61 pass
62
62
63
63
64 class BaseAppView(object):
64 class BaseAppView(object):
65
65
66 def __init__(self, context, request):
66 def __init__(self, context, request):
67 self.request = request
67 self.request = request
68 self.context = context
68 self.context = context
69 self.session = request.session
69 self.session = request.session
70 self._rhodecode_user = request.user # auth user
70 self._rhodecode_user = request.user # auth user
71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 self._maybe_needs_password_change(
72 self._maybe_needs_password_change(
73 request.matched_route.name, self._rhodecode_db_user)
73 request.matched_route.name, self._rhodecode_db_user)
74
74
75 def _maybe_needs_password_change(self, view_name, user_obj):
75 def _maybe_needs_password_change(self, view_name, user_obj):
76 log.debug('Checking if user %s needs password change on view %s',
76 log.debug('Checking if user %s needs password change on view %s',
77 user_obj, view_name)
77 user_obj, view_name)
78 skip_user_views = [
78 skip_user_views = [
79 'logout', 'login',
79 'logout', 'login',
80 'my_account_password', 'my_account_password_update'
80 'my_account_password', 'my_account_password_update'
81 ]
81 ]
82
82
83 if not user_obj:
83 if not user_obj:
84 return
84 return
85
85
86 if user_obj.username == User.DEFAULT_USER:
86 if user_obj.username == User.DEFAULT_USER:
87 return
87 return
88
88
89 now = time.time()
89 now = time.time()
90 should_change = user_obj.user_data.get('force_password_change')
90 should_change = user_obj.user_data.get('force_password_change')
91 change_after = safe_int(should_change) or 0
91 change_after = safe_int(should_change) or 0
92 if should_change and now > change_after:
92 if should_change and now > change_after:
93 log.debug('User %s requires password change', user_obj)
93 log.debug('User %s requires password change', user_obj)
94 h.flash('You are required to change your password', 'warning',
94 h.flash('You are required to change your password', 'warning',
95 ignore_duplicate=True)
95 ignore_duplicate=True)
96
96
97 if view_name not in skip_user_views:
97 if view_name not in skip_user_views:
98 raise HTTPFound(
98 raise HTTPFound(
99 self.request.route_path('my_account_password'))
99 self.request.route_path('my_account_password'))
100
100
101 def _get_local_tmpl_context(self):
101 def _get_local_tmpl_context(self):
102 c = TemplateArgs()
102 c = TemplateArgs()
103 c.auth_user = self.request.user
103 c.auth_user = self.request.user
104 return c
104 return c
105
105
106 def _register_global_c(self, tmpl_args):
106 def _register_global_c(self, tmpl_args):
107 """
107 """
108 Registers attributes to pylons global `c`
108 Registers attributes to pylons global `c`
109 """
109 """
110 # TODO(marcink): remove once pyramid migration is finished
110 # TODO(marcink): remove once pyramid migration is finished
111 for k, v in tmpl_args.items():
111 for k, v in tmpl_args.items():
112 setattr(c, k, v)
112 setattr(c, k, v)
113
113
114 def _get_template_context(self, tmpl_args):
114 def _get_template_context(self, tmpl_args):
115 self._register_global_c(tmpl_args)
115 self._register_global_c(tmpl_args)
116
116
117 local_tmpl_args = {
117 local_tmpl_args = {
118 'defaults': {},
118 'defaults': {},
119 'errors': {},
119 'errors': {},
120 }
120 }
121 local_tmpl_args.update(tmpl_args)
121 local_tmpl_args.update(tmpl_args)
122 return local_tmpl_args
122 return local_tmpl_args
123
123
124 def load_default_context(self):
124 def load_default_context(self):
125 """
125 """
126 example:
126 example:
127
127
128 def load_default_context(self):
128 def load_default_context(self):
129 c = self._get_local_tmpl_context()
129 c = self._get_local_tmpl_context()
130 c.custom_var = 'foobar'
130 c.custom_var = 'foobar'
131 self._register_global_c(c)
131 self._register_global_c(c)
132 return c
132 return c
133 """
133 """
134 raise NotImplementedError('Needs implementation in view class')
134 raise NotImplementedError('Needs implementation in view class')
135
135
136
136
137 class RepoAppView(BaseAppView):
137 class RepoAppView(BaseAppView):
138
138
139 def __init__(self, context, request):
139 def __init__(self, context, request):
140 super(RepoAppView, self).__init__(context, request)
140 super(RepoAppView, self).__init__(context, request)
141 self.db_repo = request.db_repo
141 self.db_repo = request.db_repo
142 self.db_repo_name = self.db_repo.repo_name
142 self.db_repo_name = self.db_repo.repo_name
143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144
144
145 def _handle_missing_requirements(self, error):
145 def _handle_missing_requirements(self, error):
146 log.error(
146 log.error(
147 'Requirements are missing for repository %s: %s',
147 'Requirements are missing for repository %s: %s',
148 self.db_repo_name, error.message)
148 self.db_repo_name, error.message)
149
149
150 def _get_local_tmpl_context(self):
150 def _get_local_tmpl_context(self):
151 c = super(RepoAppView, self)._get_local_tmpl_context()
151 c = super(RepoAppView, self)._get_local_tmpl_context()
152 # register common vars for this type of view
152 # register common vars for this type of view
153 c.rhodecode_db_repo = self.db_repo
153 c.rhodecode_db_repo = self.db_repo
154 c.repo_name = self.db_repo_name
154 c.repo_name = self.db_repo_name
155 c.repository_pull_requests = self.db_repo_pull_requests
155 c.repository_pull_requests = self.db_repo_pull_requests
156
156
157 c.repository_requirements_missing = False
157 c.repository_requirements_missing = False
158 try:
158 try:
159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 except RepositoryRequirementError as e:
160 except RepositoryRequirementError as e:
161 c.repository_requirements_missing = True
161 c.repository_requirements_missing = True
162 self._handle_missing_requirements(e)
162 self._handle_missing_requirements(e)
163
163
164 return c
164 return c
165
165
166
166
167 class DataGridAppView(object):
167 class DataGridAppView(object):
168 """
168 """
169 Common class to have re-usable grid rendering components
169 Common class to have re-usable grid rendering components
170 """
170 """
171
171
172 def _extract_ordering(self, request, column_map=None):
172 def _extract_ordering(self, request, column_map=None):
173 column_map = column_map or {}
173 column_map = column_map or {}
174 column_index = safe_int(request.GET.get('order[0][column]'))
174 column_index = safe_int(request.GET.get('order[0][column]'))
175 order_dir = request.GET.get(
175 order_dir = request.GET.get(
176 'order[0][dir]', 'desc')
176 'order[0][dir]', 'desc')
177 order_by = request.GET.get(
177 order_by = request.GET.get(
178 'columns[%s][data][sort]' % column_index, 'name_raw')
178 'columns[%s][data][sort]' % column_index, 'name_raw')
179
179
180 # translate datatable to DB columns
180 # translate datatable to DB columns
181 order_by = column_map.get(order_by) or order_by
181 order_by = column_map.get(order_by) or order_by
182
182
183 search_q = request.GET.get('search[value]')
183 search_q = request.GET.get('search[value]')
184 return search_q, order_by, order_dir
184 return search_q, order_by, order_dir
185
185
186 def _extract_chunk(self, request):
186 def _extract_chunk(self, request):
187 start = safe_int(request.GET.get('start'), 0)
187 start = safe_int(request.GET.get('start'), 0)
188 length = safe_int(request.GET.get('length'), 25)
188 length = safe_int(request.GET.get('length'), 25)
189 draw = safe_int(request.GET.get('draw'))
189 draw = safe_int(request.GET.get('draw'))
190 return draw, start, length
190 return draw, start, length
191
191
192
192
193 class BaseReferencesView(RepoAppView):
193 class BaseReferencesView(RepoAppView):
194 """
194 """
195 Base for reference view for branches, tags and bookmarks.
195 Base for reference view for branches, tags and bookmarks.
196 """
196 """
197 def load_default_context(self):
197 def load_default_context(self):
198 c = self._get_local_tmpl_context()
198 c = self._get_local_tmpl_context()
199
199
200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 c.repo_info = self.db_repo
201 c.repo_info = self.db_repo
202
202
203 self._register_global_c(c)
203 self._register_global_c(c)
204 return c
204 return c
205
205
206 def load_refs_context(self, ref_items, partials_template):
206 def load_refs_context(self, ref_items, partials_template):
207 _render = PartialRenderer(partials_template)
207 _render = PartialRenderer(partials_template)
208 _data = []
208 _data = []
209 pre_load = ["author", "date", "message"]
209 pre_load = ["author", "date", "message"]
210
210
211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213
213
214 for ref_name, commit_id in ref_items:
214 for ref_name, commit_id in ref_items:
215 commit = self.rhodecode_vcs_repo.get_commit(
215 commit = self.rhodecode_vcs_repo.get_commit(
216 commit_id=commit_id, pre_load=pre_load)
216 commit_id=commit_id, pre_load=pre_load)
217
217
218 # TODO: johbo: Unify generation of reference links
218 # TODO: johbo: Unify generation of reference links
219 use_commit_id = '/' in ref_name or is_svn
219 use_commit_id = '/' in ref_name or is_svn
220 files_url = h.url(
220 files_url = h.url(
221 'files_home',
221 'files_home',
222 repo_name=c.repo_name,
222 repo_name=c.repo_name,
223 f_path=ref_name if is_svn else '',
223 f_path=ref_name if is_svn else '',
224 revision=commit_id if use_commit_id else ref_name,
224 revision=commit_id if use_commit_id else ref_name,
225 at=ref_name)
225 at=ref_name)
226
226
227 _data.append({
227 _data.append({
228 "name": _render('name', ref_name, files_url),
228 "name": _render('name', ref_name, files_url),
229 "name_raw": ref_name,
229 "name_raw": ref_name,
230 "date": _render('date', commit.date),
230 "date": _render('date', commit.date),
231 "date_raw": datetime_to_time(commit.date),
231 "date_raw": datetime_to_time(commit.date),
232 "author": _render('author', commit.author),
232 "author": _render('author', commit.author),
233 "commit": _render(
233 "commit": _render(
234 'commit', commit.message, commit.raw_id, commit.idx),
234 'commit', commit.message, commit.raw_id, commit.idx),
235 "commit_raw": commit.idx,
235 "commit_raw": commit.idx,
236 "compare": _render(
236 "compare": _render(
237 'compare', format_ref_id(ref_name, commit.raw_id)),
237 'compare', format_ref_id(ref_name, commit.raw_id)),
238 })
238 })
239 c.has_references = bool(_data)
239 c.has_references = bool(_data)
240 c.data = json.dumps(_data)
240 c.data = json.dumps(_data)
241
241
242
242
243 class RepoRoutePredicate(object):
243 class RepoRoutePredicate(object):
244 def __init__(self, val, config):
244 def __init__(self, val, config):
245 self.val = val
245 self.val = val
246
246
247 def text(self):
247 def text(self):
248 return 'repo_route = %s' % self.val
248 return 'repo_route = %s' % self.val
249
249
250 phash = text
250 phash = text
251
251
252 def __call__(self, info, request):
252 def __call__(self, info, request):
253 repo_name = info['match']['repo_name']
253 repo_name = info['match']['repo_name']
254 repo_model = repo.RepoModel()
254 repo_model = repo.RepoModel()
255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
256 # if we match quickly from database, short circuit the operation,
256 # if we match quickly from database, short circuit the operation,
257 # and validate repo based on the type.
257 # and validate repo based on the type.
258 if by_name_match:
258 if by_name_match:
259 # register this as request object we can re-use later
259 # register this as request object we can re-use later
260 request.db_repo = by_name_match
260 request.db_repo = by_name_match
261 return True
261 return True
262
262
263 by_id_match = repo_model.get_repo_by_id(repo_name)
263 by_id_match = repo_model.get_repo_by_id(repo_name)
264 if by_id_match:
264 if by_id_match:
265 request.db_repo = by_id_match
265 request.db_repo = by_id_match
266 return True
266 return True
267
267
268 return False
268 return False
269
269
270
270
271 class RepoTypeRoutePredicate(object):
271 class RepoTypeRoutePredicate(object):
272 def __init__(self, val, config):
272 def __init__(self, val, config):
273 self.val = val or ['hg', 'git', 'svn']
273 self.val = val or ['hg', 'git', 'svn']
274
274
275 def text(self):
275 def text(self):
276 return 'repo_accepted_type = %s' % self.val
276 return 'repo_accepted_type = %s' % self.val
277
277
278 phash = text
278 phash = text
279
279
280 def __call__(self, info, request):
280 def __call__(self, info, request):
281
281
282 rhodecode_db_repo = request.db_repo
282 rhodecode_db_repo = request.db_repo
283
283
284 log.debug(
284 log.debug(
285 '%s checking repo type for %s in %s',
285 '%s checking repo type for %s in %s',
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287
287
288 if rhodecode_db_repo.repo_type in self.val:
288 if rhodecode_db_repo.repo_type in self.val:
289 return True
289 return True
290 else:
290 else:
291 log.warning('Current view is not supported for repo type:%s',
291 log.warning('Current view is not supported for repo type:%s',
292 rhodecode_db_repo.repo_type)
292 rhodecode_db_repo.repo_type)
293 #
294 # h.flash(h.literal(
295 # _('Action not supported for %s.' % rhodecode_repo.alias)),
296 # category='warning')
297 # return redirect(
298 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
299
293 return False
300 return False
294
301
295
302
296
303
297 def includeme(config):
304 def includeme(config):
298 config.add_route_predicate(
305 config.add_route_predicate(
299 'repo_route', RepoRoutePredicate)
306 'repo_route', RepoRoutePredicate)
300 config.add_route_predicate(
307 config.add_route_predicate(
301 'repo_accepted_types', RepoTypeRoutePredicate) No newline at end of file
308 'repo_accepted_types', RepoTypeRoutePredicate)
@@ -1,69 +1,64 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24
24
25 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.controllers import utils
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RepoReviewRulesView(RepoAppView):
32 class RepoReviewRulesView(RepoAppView):
33 def load_default_context(self):
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
35
35
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
37 c.repo_info = self.db_repo
37 c.repo_info = self.db_repo
38
38
39 self._register_global_c(c)
39 self._register_global_c(c)
40 return c
40 return c
41
41
42 @LoginRequired()
42 @LoginRequired()
43 @HasRepoPermissionAnyDecorator('repository.admin')
43 @HasRepoPermissionAnyDecorator('repository.admin')
44 @view_config(
44 @view_config(
45 route_name='repo_reviewers', request_method='GET',
45 route_name='repo_reviewers', request_method='GET',
46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
47 def repo_review_rules(self):
47 def repo_review_rules(self):
48 c = self.load_default_context()
48 c = self.load_default_context()
49 c.active = 'reviewers'
49 c.active = 'reviewers'
50
50
51 return self._get_template_context(c)
51 return self._get_template_context(c)
52
52
53 @LoginRequired()
53 @LoginRequired()
54 @HasRepoPermissionAnyDecorator(
54 @HasRepoPermissionAnyDecorator(
55 'repository.read', 'repository.write', 'repository.admin')
55 'repository.read', 'repository.write', 'repository.admin')
56 @view_config(
56 @view_config(
57 route_name='repo_default_reviewers_data', request_method='GET',
57 route_name='repo_default_reviewers_data', request_method='GET',
58 renderer='json_ext')
58 renderer='json_ext')
59 def repo_default_reviewers_data(self):
59 def repo_default_reviewers_data(self):
60 reasons = ['Default reviewer', 'Repository owner']
60 review_data = get_default_reviewers_data(
61 default = utils.reviewer_as_json(
61 self.db_repo.user, None, None, None, None)
62 user=self.db_repo.user, reasons=reasons, mandatory=False)
62 return review_data
63
63
64 return {
64
65 'api_ver': 'v1', # define version for later possible schema upgrade
66 'reviewers': [default],
67 'rules': {},
68 'rules_data': {},
69 }
@@ -1,978 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(url('summary_home', repo_name=source_repo.repo_name))
88 redirect(url('summary_home', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231 reviewers = [
232 (r['user_id'], r['reasons']) for r in _form['review_members']]
233
231
234 # find the ancestor for this pr
232 # find the ancestor for this pr
235 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
236 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
237
235
238 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
239 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
240
238
241 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
242 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
243
241
244 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
245 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
246
244
247 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
248 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
249
247
250 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
251 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
252 if not pullrequest_title:
250 if not pullrequest_title:
253 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
254 source=source_repo,
252 source=source_repo,
255 source_ref=title_source_ref,
253 source_ref=title_source_ref,
256 target=target_repo
254 target=target_repo
257 )
255 )
258
256
259 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
261
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user, source_db_repo, source_commit, target_db_repo,
265 target_commit)
266
267 reviewers = validate_default_reviewers(
268 _form['review_members'], reviewer_rules)
269
260 try:
270 try:
261 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
262 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
263 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
264 description
274 description, reviewer_rules
265 )
275 )
266 Session().commit()
276 Session().commit()
267 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
268 category='success')
278 category='success')
269 except Exception as e:
279 except Exception as e:
270 msg = _('Error occurred during sending pull request')
280 msg = _('Error occurred during creation of this pull request.')
271 log.exception(msg)
281 log.exception(msg)
272 h.flash(msg, category='error')
282 h.flash(msg, category='error')
273 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
274
284
275 return redirect(url('pullrequest_show', repo_name=target_repo,
285 return redirect(url('pullrequest_show', repo_name=target_repo,
276 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
277
287
278 @LoginRequired()
288 @LoginRequired()
279 @NotAnonymous()
289 @NotAnonymous()
280 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
281 'repository.admin')
291 'repository.admin')
282 @auth.CSRFRequired()
292 @auth.CSRFRequired()
283 @jsonify
293 @jsonify
284 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
285 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
286 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
287 # only owner or admin can update it
297 # only owner or admin can update it
288 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
289 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
290 if allowed_to_update:
300 if allowed_to_update:
291 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
292
302
293 if 'review_members' in controls:
303 if 'review_members' in controls:
294 self._update_reviewers(
304 self._update_reviewers(
295 pull_request_id, controls['review_members'])
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
296 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
297 self._update_commits(pull_request)
308 self._update_commits(pull_request)
298 elif str2bool(request.POST.get('close_pull_request', 'false')):
309 elif str2bool(request.POST.get('close_pull_request', 'false')):
299 self._reject_close(pull_request)
310 self._reject_close(pull_request)
300 elif str2bool(request.POST.get('edit_pull_request', 'false')):
311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
301 self._edit_pull_request(pull_request)
312 self._edit_pull_request(pull_request)
302 else:
313 else:
303 raise HTTPBadRequest()
314 raise HTTPBadRequest()
304 return True
315 return True
305 raise HTTPForbidden()
316 raise HTTPForbidden()
306
317
307 def _edit_pull_request(self, pull_request):
318 def _edit_pull_request(self, pull_request):
308 try:
319 try:
309 PullRequestModel().edit(
320 PullRequestModel().edit(
310 pull_request, request.POST.get('title'),
321 pull_request, request.POST.get('title'),
311 request.POST.get('description'))
322 request.POST.get('description'))
312 except ValueError:
323 except ValueError:
313 msg = _(u'Cannot update closed pull requests.')
324 msg = _(u'Cannot update closed pull requests.')
314 h.flash(msg, category='error')
325 h.flash(msg, category='error')
315 return
326 return
316 else:
327 else:
317 Session().commit()
328 Session().commit()
318
329
319 msg = _(u'Pull request title & description updated.')
330 msg = _(u'Pull request title & description updated.')
320 h.flash(msg, category='success')
331 h.flash(msg, category='success')
321 return
332 return
322
333
323 def _update_commits(self, pull_request):
334 def _update_commits(self, pull_request):
324 resp = PullRequestModel().update_commits(pull_request)
335 resp = PullRequestModel().update_commits(pull_request)
325
336
326 if resp.executed:
337 if resp.executed:
327
338
328 if resp.target_changed and resp.source_changed:
339 if resp.target_changed and resp.source_changed:
329 changed = 'target and source repositories'
340 changed = 'target and source repositories'
330 elif resp.target_changed and not resp.source_changed:
341 elif resp.target_changed and not resp.source_changed:
331 changed = 'target repository'
342 changed = 'target repository'
332 elif not resp.target_changed and resp.source_changed:
343 elif not resp.target_changed and resp.source_changed:
333 changed = 'source repository'
344 changed = 'source repository'
334 else:
345 else:
335 changed = 'nothing'
346 changed = 'nothing'
336
347
337 msg = _(
348 msg = _(
338 u'Pull request updated to "{source_commit_id}" with '
349 u'Pull request updated to "{source_commit_id}" with '
339 u'{count_added} added, {count_removed} removed commits. '
350 u'{count_added} added, {count_removed} removed commits. '
340 u'Source of changes: {change_source}')
351 u'Source of changes: {change_source}')
341 msg = msg.format(
352 msg = msg.format(
342 source_commit_id=pull_request.source_ref_parts.commit_id,
353 source_commit_id=pull_request.source_ref_parts.commit_id,
343 count_added=len(resp.changes.added),
354 count_added=len(resp.changes.added),
344 count_removed=len(resp.changes.removed),
355 count_removed=len(resp.changes.removed),
345 change_source=changed)
356 change_source=changed)
346 h.flash(msg, category='success')
357 h.flash(msg, category='success')
347
358
348 registry = get_current_registry()
359 registry = get_current_registry()
349 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
350 channelstream_config = rhodecode_plugins.get('channelstream', {})
361 channelstream_config = rhodecode_plugins.get('channelstream', {})
351 if channelstream_config.get('enabled'):
362 if channelstream_config.get('enabled'):
352 message = msg + (
363 message = msg + (
353 ' - <a onclick="window.location.reload()">'
364 ' - <a onclick="window.location.reload()">'
354 '<strong>{}</strong></a>'.format(_('Reload page')))
365 '<strong>{}</strong></a>'.format(_('Reload page')))
355 channel = '/repo${}$/pr/{}'.format(
366 channel = '/repo${}$/pr/{}'.format(
356 pull_request.target_repo.repo_name,
367 pull_request.target_repo.repo_name,
357 pull_request.pull_request_id
368 pull_request.pull_request_id
358 )
369 )
359 payload = {
370 payload = {
360 'type': 'message',
371 'type': 'message',
361 'user': 'system',
372 'user': 'system',
362 'exclude_users': [request.user.username],
373 'exclude_users': [request.user.username],
363 'channel': channel,
374 'channel': channel,
364 'message': {
375 'message': {
365 'message': message,
376 'message': message,
366 'level': 'success',
377 'level': 'success',
367 'topic': '/notifications'
378 'topic': '/notifications'
368 }
379 }
369 }
380 }
370 channelstream_request(
381 channelstream_request(
371 channelstream_config, [payload], '/message',
382 channelstream_config, [payload], '/message',
372 raise_exc=False)
383 raise_exc=False)
373 else:
384 else:
374 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
375 warning_reasons = [
386 warning_reasons = [
376 UpdateFailureReason.NO_CHANGE,
387 UpdateFailureReason.NO_CHANGE,
377 UpdateFailureReason.WRONG_REF_TYPE,
388 UpdateFailureReason.WRONG_REF_TYPE,
378 ]
389 ]
379 category = 'warning' if resp.reason in warning_reasons else 'error'
390 category = 'warning' if resp.reason in warning_reasons else 'error'
380 h.flash(msg, category=category)
391 h.flash(msg, category=category)
381
392
382 @auth.CSRFRequired()
393 @auth.CSRFRequired()
383 @LoginRequired()
394 @LoginRequired()
384 @NotAnonymous()
395 @NotAnonymous()
385 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 'repository.admin')
397 'repository.admin')
387 def merge(self, repo_name, pull_request_id):
398 def merge(self, repo_name, pull_request_id):
388 """
399 """
389 POST /{repo_name}/pull-request/{pull_request_id}
400 POST /{repo_name}/pull-request/{pull_request_id}
390
401
391 Merge will perform a server-side merge of the specified
402 Merge will perform a server-side merge of the specified
392 pull request, if the pull request is approved and mergeable.
403 pull request, if the pull request is approved and mergeable.
393 After successful merging, the pull request is automatically
404 After successful merging, the pull request is automatically
394 closed, with a relevant comment.
405 closed, with a relevant comment.
395 """
406 """
396 pull_request_id = safe_int(pull_request_id)
407 pull_request_id = safe_int(pull_request_id)
397 pull_request = PullRequest.get_or_404(pull_request_id)
408 pull_request = PullRequest.get_or_404(pull_request_id)
398 user = c.rhodecode_user
409 user = c.rhodecode_user
399
410
400 check = MergeCheck.validate(pull_request, user)
411 check = MergeCheck.validate(pull_request, user)
401 merge_possible = not check.failed
412 merge_possible = not check.failed
402
413
403 for err_type, error_msg in check.errors:
414 for err_type, error_msg in check.errors:
404 h.flash(error_msg, category=err_type)
415 h.flash(error_msg, category=err_type)
405
416
406 if merge_possible:
417 if merge_possible:
407 log.debug("Pre-conditions checked, trying to merge.")
418 log.debug("Pre-conditions checked, trying to merge.")
408 extras = vcs_operation_context(
419 extras = vcs_operation_context(
409 request.environ, repo_name=pull_request.target_repo.repo_name,
420 request.environ, repo_name=pull_request.target_repo.repo_name,
410 username=user.username, action='push',
421 username=user.username, action='push',
411 scm=pull_request.target_repo.repo_type)
422 scm=pull_request.target_repo.repo_type)
412 self._merge_pull_request(pull_request, user, extras)
423 self._merge_pull_request(pull_request, user, extras)
413
424
414 return redirect(url(
425 return redirect(url(
415 'pullrequest_show',
426 'pullrequest_show',
416 repo_name=pull_request.target_repo.repo_name,
427 repo_name=pull_request.target_repo.repo_name,
417 pull_request_id=pull_request.pull_request_id))
428 pull_request_id=pull_request.pull_request_id))
418
429
419 def _merge_pull_request(self, pull_request, user, extras):
430 def _merge_pull_request(self, pull_request, user, extras):
420 merge_resp = PullRequestModel().merge(
431 merge_resp = PullRequestModel().merge(
421 pull_request, user, extras=extras)
432 pull_request, user, extras=extras)
422
433
423 if merge_resp.executed:
434 if merge_resp.executed:
424 log.debug("The merge was successful, closing the pull request.")
435 log.debug("The merge was successful, closing the pull request.")
425 PullRequestModel().close_pull_request(
436 PullRequestModel().close_pull_request(
426 pull_request.pull_request_id, user)
437 pull_request.pull_request_id, user)
427 Session().commit()
438 Session().commit()
428 msg = _('Pull request was successfully merged and closed.')
439 msg = _('Pull request was successfully merged and closed.')
429 h.flash(msg, category='success')
440 h.flash(msg, category='success')
430 else:
441 else:
431 log.debug(
442 log.debug(
432 "The merge was not successful. Merge response: %s",
443 "The merge was not successful. Merge response: %s",
433 merge_resp)
444 merge_resp)
434 msg = PullRequestModel().merge_status_message(
445 msg = PullRequestModel().merge_status_message(
435 merge_resp.failure_reason)
446 merge_resp.failure_reason)
436 h.flash(msg, category='error')
447 h.flash(msg, category='error')
437
448
438 def _update_reviewers(self, pull_request_id, review_members):
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
439 reviewers = [
450
440 (int(r['user_id']), r['reasons']) for r in review_members]
451 get_default_reviewers_data, validate_default_reviewers = \
452 PullRequestModel().get_reviewer_functions()
453
454 try:
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 except ValueError as e:
457 log.error('Reviewers Validation:{}'.format(e))
458 h.flash(e, category='error')
459 return
460
441 PullRequestModel().update_reviewers(pull_request_id, reviewers)
461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
442 Session().commit()
463 Session().commit()
443
464
444 def _reject_close(self, pull_request):
465 def _reject_close(self, pull_request):
445 if pull_request.is_closed():
466 if pull_request.is_closed():
446 raise HTTPForbidden()
467 raise HTTPForbidden()
447
468
448 PullRequestModel().close_pull_request_with_comment(
469 PullRequestModel().close_pull_request_with_comment(
449 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
450 Session().commit()
471 Session().commit()
451
472
452 @LoginRequired()
473 @LoginRequired()
453 @NotAnonymous()
474 @NotAnonymous()
454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 'repository.admin')
476 'repository.admin')
456 @auth.CSRFRequired()
477 @auth.CSRFRequired()
457 @jsonify
478 @jsonify
458 def delete(self, repo_name, pull_request_id):
479 def delete(self, repo_name, pull_request_id):
459 pull_request_id = safe_int(pull_request_id)
480 pull_request_id = safe_int(pull_request_id)
460 pull_request = PullRequest.get_or_404(pull_request_id)
481 pull_request = PullRequest.get_or_404(pull_request_id)
461
482
462 pr_closed = pull_request.is_closed()
483 pr_closed = pull_request.is_closed()
463 allowed_to_delete = PullRequestModel().check_user_delete(
484 allowed_to_delete = PullRequestModel().check_user_delete(
464 pull_request, c.rhodecode_user) and not pr_closed
485 pull_request, c.rhodecode_user) and not pr_closed
465
486
466 # only owner can delete it !
487 # only owner can delete it !
467 if allowed_to_delete:
488 if allowed_to_delete:
468 PullRequestModel().delete(pull_request)
489 PullRequestModel().delete(pull_request)
469 Session().commit()
490 Session().commit()
470 h.flash(_('Successfully deleted pull request'),
491 h.flash(_('Successfully deleted pull request'),
471 category='success')
492 category='success')
472 return redirect(url('my_account_pullrequests'))
493 return redirect(url('my_account_pullrequests'))
473
494
474 h.flash(_('Your are not allowed to delete this pull request'),
495 h.flash(_('Your are not allowed to delete this pull request'),
475 category='error')
496 category='error')
476 raise HTTPForbidden()
497 raise HTTPForbidden()
477
498
478 def _get_pr_version(self, pull_request_id, version=None):
499 def _get_pr_version(self, pull_request_id, version=None):
479 pull_request_id = safe_int(pull_request_id)
500 pull_request_id = safe_int(pull_request_id)
480 at_version = None
501 at_version = None
481
502
482 if version and version == 'latest':
503 if version and version == 'latest':
483 pull_request_ver = PullRequest.get(pull_request_id)
504 pull_request_ver = PullRequest.get(pull_request_id)
484 pull_request_obj = pull_request_ver
505 pull_request_obj = pull_request_ver
485 _org_pull_request_obj = pull_request_obj
506 _org_pull_request_obj = pull_request_obj
486 at_version = 'latest'
507 at_version = 'latest'
487 elif version:
508 elif version:
488 pull_request_ver = PullRequestVersion.get_or_404(version)
509 pull_request_ver = PullRequestVersion.get_or_404(version)
489 pull_request_obj = pull_request_ver
510 pull_request_obj = pull_request_ver
490 _org_pull_request_obj = pull_request_ver.pull_request
511 _org_pull_request_obj = pull_request_ver.pull_request
491 at_version = pull_request_ver.pull_request_version_id
512 at_version = pull_request_ver.pull_request_version_id
492 else:
513 else:
493 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 pull_request_id)
494
516
495 pull_request_display_obj = PullRequest.get_pr_display_object(
517 pull_request_display_obj = PullRequest.get_pr_display_object(
496 pull_request_obj, _org_pull_request_obj)
518 pull_request_obj, _org_pull_request_obj)
497
519
498 return _org_pull_request_obj, pull_request_obj, \
520 return _org_pull_request_obj, pull_request_obj, \
499 pull_request_display_obj, at_version
521 pull_request_display_obj, at_version
500
522
501 def _get_diffset(
523 def _get_diffset(
502 self, source_repo, source_ref_id, target_ref_id, target_commit,
524 self, source_repo, source_ref_id, target_ref_id, target_commit,
503 source_commit, diff_limit, file_limit, display_inline_comments):
525 source_commit, diff_limit, file_limit, display_inline_comments):
504 vcs_diff = PullRequestModel().get_diff(
526 vcs_diff = PullRequestModel().get_diff(
505 source_repo, source_ref_id, target_ref_id)
527 source_repo, source_ref_id, target_ref_id)
506
528
507 diff_processor = diffs.DiffProcessor(
529 diff_processor = diffs.DiffProcessor(
508 vcs_diff, format='newdiff', diff_limit=diff_limit,
530 vcs_diff, format='newdiff', diff_limit=diff_limit,
509 file_limit=file_limit, show_full_diff=c.fulldiff)
531 file_limit=file_limit, show_full_diff=c.fulldiff)
510
532
511 _parsed = diff_processor.prepare()
533 _parsed = diff_processor.prepare()
512
534
513 def _node_getter(commit):
535 def _node_getter(commit):
514 def get_node(fname):
536 def get_node(fname):
515 try:
537 try:
516 return commit.get_node(fname)
538 return commit.get_node(fname)
517 except NodeDoesNotExistError:
539 except NodeDoesNotExistError:
518 return None
540 return None
519
541
520 return get_node
542 return get_node
521
543
522 diffset = codeblocks.DiffSet(
544 diffset = codeblocks.DiffSet(
523 repo_name=c.repo_name,
545 repo_name=c.repo_name,
524 source_repo_name=c.source_repo.repo_name,
546 source_repo_name=c.source_repo.repo_name,
525 source_node_getter=_node_getter(target_commit),
547 source_node_getter=_node_getter(target_commit),
526 target_node_getter=_node_getter(source_commit),
548 target_node_getter=_node_getter(source_commit),
527 comments=display_inline_comments
549 comments=display_inline_comments
528 )
550 )
529 diffset = diffset.render_patchset(
551 diffset = diffset.render_patchset(
530 _parsed, target_commit.raw_id, source_commit.raw_id)
552 _parsed, target_commit.raw_id, source_commit.raw_id)
531
553
532 return diffset
554 return diffset
533
555
534 @LoginRequired()
556 @LoginRequired()
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 'repository.admin')
558 'repository.admin')
537 def show(self, repo_name, pull_request_id):
559 def show(self, repo_name, pull_request_id):
538 pull_request_id = safe_int(pull_request_id)
560 pull_request_id = safe_int(pull_request_id)
539 version = request.GET.get('version')
561 version = request.GET.get('version')
540 from_version = request.GET.get('from_version') or version
562 from_version = request.GET.get('from_version') or version
541 merge_checks = request.GET.get('merge_checks')
563 merge_checks = request.GET.get('merge_checks')
542 c.fulldiff = str2bool(request.GET.get('fulldiff'))
564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
543
565
544 (pull_request_latest,
566 (pull_request_latest,
545 pull_request_at_ver,
567 pull_request_at_ver,
546 pull_request_display_obj,
568 pull_request_display_obj,
547 at_version) = self._get_pr_version(
569 at_version) = self._get_pr_version(
548 pull_request_id, version=version)
570 pull_request_id, version=version)
549 pr_closed = pull_request_latest.is_closed()
571 pr_closed = pull_request_latest.is_closed()
550
572
551 if pr_closed and (version or from_version):
573 if pr_closed and (version or from_version):
552 # not allow to browse versions
574 # not allow to browse versions
553 return redirect(h.url('pullrequest_show', repo_name=repo_name,
575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
554 pull_request_id=pull_request_id))
576 pull_request_id=pull_request_id))
555
577
556 versions = pull_request_display_obj.versions()
578 versions = pull_request_display_obj.versions()
557
579
558 c.at_version = at_version
580 c.at_version = at_version
559 c.at_version_num = (at_version
581 c.at_version_num = (at_version
560 if at_version and at_version != 'latest'
582 if at_version and at_version != 'latest'
561 else None)
583 else None)
562 c.at_version_pos = ChangesetComment.get_index_from_version(
584 c.at_version_pos = ChangesetComment.get_index_from_version(
563 c.at_version_num, versions)
585 c.at_version_num, versions)
564
586
565 (prev_pull_request_latest,
587 (prev_pull_request_latest,
566 prev_pull_request_at_ver,
588 prev_pull_request_at_ver,
567 prev_pull_request_display_obj,
589 prev_pull_request_display_obj,
568 prev_at_version) = self._get_pr_version(
590 prev_at_version) = self._get_pr_version(
569 pull_request_id, version=from_version)
591 pull_request_id, version=from_version)
570
592
571 c.from_version = prev_at_version
593 c.from_version = prev_at_version
572 c.from_version_num = (prev_at_version
594 c.from_version_num = (prev_at_version
573 if prev_at_version and prev_at_version != 'latest'
595 if prev_at_version and prev_at_version != 'latest'
574 else None)
596 else None)
575 c.from_version_pos = ChangesetComment.get_index_from_version(
597 c.from_version_pos = ChangesetComment.get_index_from_version(
576 c.from_version_num, versions)
598 c.from_version_num, versions)
577
599
578 # define if we're in COMPARE mode or VIEW at version mode
600 # define if we're in COMPARE mode or VIEW at version mode
579 compare = at_version != prev_at_version
601 compare = at_version != prev_at_version
580
602
581 # pull_requests repo_name we opened it against
603 # pull_requests repo_name we opened it against
582 # ie. target_repo must match
604 # ie. target_repo must match
583 if repo_name != pull_request_at_ver.target_repo.repo_name:
605 if repo_name != pull_request_at_ver.target_repo.repo_name:
584 raise HTTPNotFound
606 raise HTTPNotFound
585
607
586 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
587 pull_request_at_ver)
609 pull_request_at_ver)
588
610
589 c.pull_request = pull_request_display_obj
611 c.pull_request = pull_request_display_obj
590 c.pull_request_latest = pull_request_latest
612 c.pull_request_latest = pull_request_latest
591
613
592 if compare or (at_version and not at_version == 'latest'):
614 if compare or (at_version and not at_version == 'latest'):
593 c.allowed_to_change_status = False
615 c.allowed_to_change_status = False
594 c.allowed_to_update = False
616 c.allowed_to_update = False
595 c.allowed_to_merge = False
617 c.allowed_to_merge = False
596 c.allowed_to_delete = False
618 c.allowed_to_delete = False
597 c.allowed_to_comment = False
619 c.allowed_to_comment = False
598 c.allowed_to_close = False
620 c.allowed_to_close = False
599 else:
621 else:
600 c.allowed_to_change_status = PullRequestModel(). \
622 can_change_status = PullRequestModel().check_user_change_status(
601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
623 pull_request_at_ver, c.rhodecode_user)
602 and not pr_closed
624 c.allowed_to_change_status = can_change_status and not pr_closed
603
625
604 c.allowed_to_update = PullRequestModel().check_user_update(
626 c.allowed_to_update = PullRequestModel().check_user_update(
605 pull_request_latest, c.rhodecode_user) and not pr_closed
627 pull_request_latest, c.rhodecode_user) and not pr_closed
606 c.allowed_to_merge = PullRequestModel().check_user_merge(
628 c.allowed_to_merge = PullRequestModel().check_user_merge(
607 pull_request_latest, c.rhodecode_user) and not pr_closed
629 pull_request_latest, c.rhodecode_user) and not pr_closed
608 c.allowed_to_delete = PullRequestModel().check_user_delete(
630 c.allowed_to_delete = PullRequestModel().check_user_delete(
609 pull_request_latest, c.rhodecode_user) and not pr_closed
631 pull_request_latest, c.rhodecode_user) and not pr_closed
610 c.allowed_to_comment = not pr_closed
632 c.allowed_to_comment = not pr_closed
611 c.allowed_to_close = c.allowed_to_merge and not pr_closed
633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
612
634
635 c.forbid_adding_reviewers = False
636 c.forbid_author_to_review = False
637
638 if pull_request_latest.reviewer_data and \
639 'rules' in pull_request_latest.reviewer_data:
640 rules = pull_request_latest.reviewer_data['rules'] or {}
641 try:
642 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
643 c.forbid_author_to_review = rules.get('forbid_author_to_review')
644 except Exception:
645 pass
646
613 # check merge capabilities
647 # check merge capabilities
614 _merge_check = MergeCheck.validate(
648 _merge_check = MergeCheck.validate(
615 pull_request_latest, user=c.rhodecode_user)
649 pull_request_latest, user=c.rhodecode_user)
616 c.pr_merge_errors = _merge_check.error_details
650 c.pr_merge_errors = _merge_check.error_details
617 c.pr_merge_possible = not _merge_check.failed
651 c.pr_merge_possible = not _merge_check.failed
618 c.pr_merge_message = _merge_check.merge_msg
652 c.pr_merge_message = _merge_check.merge_msg
619
653
620 c.pull_request_review_status = _merge_check.review_status
654 c.pull_request_review_status = _merge_check.review_status
621 if merge_checks:
655 if merge_checks:
622 return render('/pullrequests/pullrequest_merge_checks.mako')
656 return render('/pullrequests/pullrequest_merge_checks.mako')
623
657
624 comments_model = CommentsModel()
658 comments_model = CommentsModel()
625
659
626 # reviewers and statuses
660 # reviewers and statuses
627 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
661 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
628 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
662 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
629
663
630 # GENERAL COMMENTS with versions #
664 # GENERAL COMMENTS with versions #
631 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
665 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
632 q = q.order_by(ChangesetComment.comment_id.asc())
666 q = q.order_by(ChangesetComment.comment_id.asc())
633 general_comments = q
667 general_comments = q
634
668
635 # pick comments we want to render at current version
669 # pick comments we want to render at current version
636 c.comment_versions = comments_model.aggregate_comments(
670 c.comment_versions = comments_model.aggregate_comments(
637 general_comments, versions, c.at_version_num)
671 general_comments, versions, c.at_version_num)
638 c.comments = c.comment_versions[c.at_version_num]['until']
672 c.comments = c.comment_versions[c.at_version_num]['until']
639
673
640 # INLINE COMMENTS with versions #
674 # INLINE COMMENTS with versions #
641 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
675 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
642 q = q.order_by(ChangesetComment.comment_id.asc())
676 q = q.order_by(ChangesetComment.comment_id.asc())
643 inline_comments = q
677 inline_comments = q
644
678
645 c.inline_versions = comments_model.aggregate_comments(
679 c.inline_versions = comments_model.aggregate_comments(
646 inline_comments, versions, c.at_version_num, inline=True)
680 inline_comments, versions, c.at_version_num, inline=True)
647
681
648 # inject latest version
682 # inject latest version
649 latest_ver = PullRequest.get_pr_display_object(
683 latest_ver = PullRequest.get_pr_display_object(
650 pull_request_latest, pull_request_latest)
684 pull_request_latest, pull_request_latest)
651
685
652 c.versions = versions + [latest_ver]
686 c.versions = versions + [latest_ver]
653
687
654 # if we use version, then do not show later comments
688 # if we use version, then do not show later comments
655 # than current version
689 # than current version
656 display_inline_comments = collections.defaultdict(
690 display_inline_comments = collections.defaultdict(
657 lambda: collections.defaultdict(list))
691 lambda: collections.defaultdict(list))
658 for co in inline_comments:
692 for co in inline_comments:
659 if c.at_version_num:
693 if c.at_version_num:
660 # pick comments that are at least UPTO given version, so we
694 # pick comments that are at least UPTO given version, so we
661 # don't render comments for higher version
695 # don't render comments for higher version
662 should_render = co.pull_request_version_id and \
696 should_render = co.pull_request_version_id and \
663 co.pull_request_version_id <= c.at_version_num
697 co.pull_request_version_id <= c.at_version_num
664 else:
698 else:
665 # showing all, for 'latest'
699 # showing all, for 'latest'
666 should_render = True
700 should_render = True
667
701
668 if should_render:
702 if should_render:
669 display_inline_comments[co.f_path][co.line_no].append(co)
703 display_inline_comments[co.f_path][co.line_no].append(co)
670
704
671 # load diff data into template context, if we use compare mode then
705 # load diff data into template context, if we use compare mode then
672 # diff is calculated based on changes between versions of PR
706 # diff is calculated based on changes between versions of PR
673
707
674 source_repo = pull_request_at_ver.source_repo
708 source_repo = pull_request_at_ver.source_repo
675 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
709 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
676
710
677 target_repo = pull_request_at_ver.target_repo
711 target_repo = pull_request_at_ver.target_repo
678 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
712 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
679
713
680 if compare:
714 if compare:
681 # in compare switch the diff base to latest commit from prev version
715 # in compare switch the diff base to latest commit from prev version
682 target_ref_id = prev_pull_request_display_obj.revisions[0]
716 target_ref_id = prev_pull_request_display_obj.revisions[0]
683
717
684 # despite opening commits for bookmarks/branches/tags, we always
718 # despite opening commits for bookmarks/branches/tags, we always
685 # convert this to rev to prevent changes after bookmark or branch change
719 # convert this to rev to prevent changes after bookmark or branch change
686 c.source_ref_type = 'rev'
720 c.source_ref_type = 'rev'
687 c.source_ref = source_ref_id
721 c.source_ref = source_ref_id
688
722
689 c.target_ref_type = 'rev'
723 c.target_ref_type = 'rev'
690 c.target_ref = target_ref_id
724 c.target_ref = target_ref_id
691
725
692 c.source_repo = source_repo
726 c.source_repo = source_repo
693 c.target_repo = target_repo
727 c.target_repo = target_repo
694
728
695 # diff_limit is the old behavior, will cut off the whole diff
729 # diff_limit is the old behavior, will cut off the whole diff
696 # if the limit is applied otherwise will just hide the
730 # if the limit is applied otherwise will just hide the
697 # big files from the front-end
731 # big files from the front-end
698 diff_limit = self.cut_off_limit_diff
732 diff_limit = self.cut_off_limit_diff
699 file_limit = self.cut_off_limit_file
733 file_limit = self.cut_off_limit_file
700
734
701 c.commit_ranges = []
735 c.commit_ranges = []
702 source_commit = EmptyCommit()
736 source_commit = EmptyCommit()
703 target_commit = EmptyCommit()
737 target_commit = EmptyCommit()
704 c.missing_requirements = False
738 c.missing_requirements = False
705
739
706 source_scm = source_repo.scm_instance()
740 source_scm = source_repo.scm_instance()
707 target_scm = target_repo.scm_instance()
741 target_scm = target_repo.scm_instance()
708
742
709 # try first shadow repo, fallback to regular repo
743 # try first shadow repo, fallback to regular repo
710 try:
744 try:
711 commits_source_repo = pull_request_latest.get_shadow_repo()
745 commits_source_repo = pull_request_latest.get_shadow_repo()
712 except Exception:
746 except Exception:
713 log.debug('Failed to get shadow repo', exc_info=True)
747 log.debug('Failed to get shadow repo', exc_info=True)
714 commits_source_repo = source_scm
748 commits_source_repo = source_scm
715
749
716 c.commits_source_repo = commits_source_repo
750 c.commits_source_repo = commits_source_repo
717 commit_cache = {}
751 commit_cache = {}
718 try:
752 try:
719 pre_load = ["author", "branch", "date", "message"]
753 pre_load = ["author", "branch", "date", "message"]
720 show_revs = pull_request_at_ver.revisions
754 show_revs = pull_request_at_ver.revisions
721 for rev in show_revs:
755 for rev in show_revs:
722 comm = commits_source_repo.get_commit(
756 comm = commits_source_repo.get_commit(
723 commit_id=rev, pre_load=pre_load)
757 commit_id=rev, pre_load=pre_load)
724 c.commit_ranges.append(comm)
758 c.commit_ranges.append(comm)
725 commit_cache[comm.raw_id] = comm
759 commit_cache[comm.raw_id] = comm
726
760
761 # Order here matters, we first need to get target, and then
762 # the source
727 target_commit = commits_source_repo.get_commit(
763 target_commit = commits_source_repo.get_commit(
728 commit_id=safe_str(target_ref_id))
764 commit_id=safe_str(target_ref_id))
765
729 source_commit = commits_source_repo.get_commit(
766 source_commit = commits_source_repo.get_commit(
730 commit_id=safe_str(source_ref_id))
767 commit_id=safe_str(source_ref_id))
768
731 except CommitDoesNotExistError:
769 except CommitDoesNotExistError:
732 pass
770 log.warning(
771 'Failed to get commit from `{}` repo'.format(
772 commits_source_repo), exc_info=True)
733 except RepositoryRequirementError:
773 except RepositoryRequirementError:
734 log.warning(
774 log.warning(
735 'Failed to get all required data from repo', exc_info=True)
775 'Failed to get all required data from repo', exc_info=True)
736 c.missing_requirements = True
776 c.missing_requirements = True
737
777
738 c.ancestor = None # set it to None, to hide it from PR view
778 c.ancestor = None # set it to None, to hide it from PR view
739
779
740 try:
780 try:
741 ancestor_id = source_scm.get_common_ancestor(
781 ancestor_id = source_scm.get_common_ancestor(
742 source_commit.raw_id, target_commit.raw_id, target_scm)
782 source_commit.raw_id, target_commit.raw_id, target_scm)
743 c.ancestor_commit = source_scm.get_commit(ancestor_id)
783 c.ancestor_commit = source_scm.get_commit(ancestor_id)
744 except Exception:
784 except Exception:
745 c.ancestor_commit = None
785 c.ancestor_commit = None
746
786
747 c.statuses = source_repo.statuses(
787 c.statuses = source_repo.statuses(
748 [x.raw_id for x in c.commit_ranges])
788 [x.raw_id for x in c.commit_ranges])
749
789
750 # auto collapse if we have more than limit
790 # auto collapse if we have more than limit
751 collapse_limit = diffs.DiffProcessor._collapse_commits_over
791 collapse_limit = diffs.DiffProcessor._collapse_commits_over
752 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
792 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
753 c.compare_mode = compare
793 c.compare_mode = compare
754
794
755 c.missing_commits = False
795 c.missing_commits = False
756 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
796 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
757 or source_commit == target_commit):
797 or source_commit == target_commit):
758
798
759 c.missing_commits = True
799 c.missing_commits = True
760 else:
800 else:
761
801
762 c.diffset = self._get_diffset(
802 c.diffset = self._get_diffset(
763 commits_source_repo, source_ref_id, target_ref_id,
803 commits_source_repo, source_ref_id, target_ref_id,
764 target_commit, source_commit,
804 target_commit, source_commit,
765 diff_limit, file_limit, display_inline_comments)
805 diff_limit, file_limit, display_inline_comments)
766
806
767 c.limited_diff = c.diffset.limited_diff
807 c.limited_diff = c.diffset.limited_diff
768
808
769 # calculate removed files that are bound to comments
809 # calculate removed files that are bound to comments
770 comment_deleted_files = [
810 comment_deleted_files = [
771 fname for fname in display_inline_comments
811 fname for fname in display_inline_comments
772 if fname not in c.diffset.file_stats]
812 if fname not in c.diffset.file_stats]
773
813
774 c.deleted_files_comments = collections.defaultdict(dict)
814 c.deleted_files_comments = collections.defaultdict(dict)
775 for fname, per_line_comments in display_inline_comments.items():
815 for fname, per_line_comments in display_inline_comments.items():
776 if fname in comment_deleted_files:
816 if fname in comment_deleted_files:
777 c.deleted_files_comments[fname]['stats'] = 0
817 c.deleted_files_comments[fname]['stats'] = 0
778 c.deleted_files_comments[fname]['comments'] = list()
818 c.deleted_files_comments[fname]['comments'] = list()
779 for lno, comments in per_line_comments.items():
819 for lno, comments in per_line_comments.items():
780 c.deleted_files_comments[fname]['comments'].extend(
820 c.deleted_files_comments[fname]['comments'].extend(
781 comments)
821 comments)
782
822
783 # this is a hack to properly display links, when creating PR, the
823 # this is a hack to properly display links, when creating PR, the
784 # compare view and others uses different notation, and
824 # compare view and others uses different notation, and
785 # compare_commits.mako renders links based on the target_repo.
825 # compare_commits.mako renders links based on the target_repo.
786 # We need to swap that here to generate it properly on the html side
826 # We need to swap that here to generate it properly on the html side
787 c.target_repo = c.source_repo
827 c.target_repo = c.source_repo
788
828
789 c.commit_statuses = ChangesetStatus.STATUSES
829 c.commit_statuses = ChangesetStatus.STATUSES
790
830
791 c.show_version_changes = not pr_closed
831 c.show_version_changes = not pr_closed
792 if c.show_version_changes:
832 if c.show_version_changes:
793 cur_obj = pull_request_at_ver
833 cur_obj = pull_request_at_ver
794 prev_obj = prev_pull_request_at_ver
834 prev_obj = prev_pull_request_at_ver
795
835
796 old_commit_ids = prev_obj.revisions
836 old_commit_ids = prev_obj.revisions
797 new_commit_ids = cur_obj.revisions
837 new_commit_ids = cur_obj.revisions
798 commit_changes = PullRequestModel()._calculate_commit_id_changes(
838 commit_changes = PullRequestModel()._calculate_commit_id_changes(
799 old_commit_ids, new_commit_ids)
839 old_commit_ids, new_commit_ids)
800 c.commit_changes_summary = commit_changes
840 c.commit_changes_summary = commit_changes
801
841
802 # calculate the diff for commits between versions
842 # calculate the diff for commits between versions
803 c.commit_changes = []
843 c.commit_changes = []
804 mark = lambda cs, fw: list(
844 mark = lambda cs, fw: list(
805 h.itertools.izip_longest([], cs, fillvalue=fw))
845 h.itertools.izip_longest([], cs, fillvalue=fw))
806 for c_type, raw_id in mark(commit_changes.added, 'a') \
846 for c_type, raw_id in mark(commit_changes.added, 'a') \
807 + mark(commit_changes.removed, 'r') \
847 + mark(commit_changes.removed, 'r') \
808 + mark(commit_changes.common, 'c'):
848 + mark(commit_changes.common, 'c'):
809
849
810 if raw_id in commit_cache:
850 if raw_id in commit_cache:
811 commit = commit_cache[raw_id]
851 commit = commit_cache[raw_id]
812 else:
852 else:
813 try:
853 try:
814 commit = commits_source_repo.get_commit(raw_id)
854 commit = commits_source_repo.get_commit(raw_id)
815 except CommitDoesNotExistError:
855 except CommitDoesNotExistError:
816 # in case we fail extracting still use "dummy" commit
856 # in case we fail extracting still use "dummy" commit
817 # for display in commit diff
857 # for display in commit diff
818 commit = h.AttributeDict(
858 commit = h.AttributeDict(
819 {'raw_id': raw_id,
859 {'raw_id': raw_id,
820 'message': 'EMPTY or MISSING COMMIT'})
860 'message': 'EMPTY or MISSING COMMIT'})
821 c.commit_changes.append([c_type, commit])
861 c.commit_changes.append([c_type, commit])
822
862
823 # current user review statuses for each version
863 # current user review statuses for each version
824 c.review_versions = {}
864 c.review_versions = {}
825 if c.rhodecode_user.user_id in allowed_reviewers:
865 if c.rhodecode_user.user_id in allowed_reviewers:
826 for co in general_comments:
866 for co in general_comments:
827 if co.author.user_id == c.rhodecode_user.user_id:
867 if co.author.user_id == c.rhodecode_user.user_id:
828 # each comment has a status change
868 # each comment has a status change
829 status = co.status_change
869 status = co.status_change
830 if status:
870 if status:
831 _ver_pr = status[0].comment.pull_request_version_id
871 _ver_pr = status[0].comment.pull_request_version_id
832 c.review_versions[_ver_pr] = status[0]
872 c.review_versions[_ver_pr] = status[0]
833
873
834 return render('/pullrequests/pullrequest_show.mako')
874 return render('/pullrequests/pullrequest_show.mako')
835
875
836 @LoginRequired()
876 @LoginRequired()
837 @NotAnonymous()
877 @NotAnonymous()
838 @HasRepoPermissionAnyDecorator(
878 @HasRepoPermissionAnyDecorator(
839 'repository.read', 'repository.write', 'repository.admin')
879 'repository.read', 'repository.write', 'repository.admin')
840 @auth.CSRFRequired()
880 @auth.CSRFRequired()
841 @jsonify
881 @jsonify
842 def comment(self, repo_name, pull_request_id):
882 def comment(self, repo_name, pull_request_id):
843 pull_request_id = safe_int(pull_request_id)
883 pull_request_id = safe_int(pull_request_id)
844 pull_request = PullRequest.get_or_404(pull_request_id)
884 pull_request = PullRequest.get_or_404(pull_request_id)
845 if pull_request.is_closed():
885 if pull_request.is_closed():
846 raise HTTPForbidden()
886 raise HTTPForbidden()
847
887
848 status = request.POST.get('changeset_status', None)
888 status = request.POST.get('changeset_status', None)
849 text = request.POST.get('text')
889 text = request.POST.get('text')
850 comment_type = request.POST.get('comment_type')
890 comment_type = request.POST.get('comment_type')
851 resolves_comment_id = request.POST.get('resolves_comment_id', None)
891 resolves_comment_id = request.POST.get('resolves_comment_id', None)
852 close_pull_request = request.POST.get('close_pull_request')
892 close_pull_request = request.POST.get('close_pull_request')
853
893
854 close_pr = False
894 close_pr = False
855 # only owner or admin or person with write permissions
895 # only owner or admin or person with write permissions
856 allowed_to_close = PullRequestModel().check_user_update(
896 allowed_to_close = PullRequestModel().check_user_update(
857 pull_request, c.rhodecode_user)
897 pull_request, c.rhodecode_user)
858
898
859 if close_pull_request and allowed_to_close:
899 if close_pull_request and allowed_to_close:
860 close_pr = True
900 close_pr = True
861 pull_request_review_status = pull_request.calculated_review_status()
901 pull_request_review_status = pull_request.calculated_review_status()
862 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
902 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
863 # approved only if we have voting consent
903 # approved only if we have voting consent
864 status = ChangesetStatus.STATUS_APPROVED
904 status = ChangesetStatus.STATUS_APPROVED
865 else:
905 else:
866 status = ChangesetStatus.STATUS_REJECTED
906 status = ChangesetStatus.STATUS_REJECTED
867
907
868 allowed_to_change_status = PullRequestModel().check_user_change_status(
908 allowed_to_change_status = PullRequestModel().check_user_change_status(
869 pull_request, c.rhodecode_user)
909 pull_request, c.rhodecode_user)
870
910
871 if status and allowed_to_change_status:
911 if status and allowed_to_change_status:
872 message = (_('Status change %(transition_icon)s %(status)s')
912 message = (_('Status change %(transition_icon)s %(status)s')
873 % {'transition_icon': '>',
913 % {'transition_icon': '>',
874 'status': ChangesetStatus.get_status_lbl(status)})
914 'status': ChangesetStatus.get_status_lbl(status)})
875 if close_pr:
915 if close_pr:
876 message = _('Closing with') + ' ' + message
916 message = _('Closing with') + ' ' + message
877 text = text or message
917 text = text or message
878 comm = CommentsModel().create(
918 comm = CommentsModel().create(
879 text=text,
919 text=text,
880 repo=c.rhodecode_db_repo.repo_id,
920 repo=c.rhodecode_db_repo.repo_id,
881 user=c.rhodecode_user.user_id,
921 user=c.rhodecode_user.user_id,
882 pull_request=pull_request_id,
922 pull_request=pull_request_id,
883 f_path=request.POST.get('f_path'),
923 f_path=request.POST.get('f_path'),
884 line_no=request.POST.get('line'),
924 line_no=request.POST.get('line'),
885 status_change=(ChangesetStatus.get_status_lbl(status)
925 status_change=(ChangesetStatus.get_status_lbl(status)
886 if status and allowed_to_change_status else None),
926 if status and allowed_to_change_status else None),
887 status_change_type=(status
927 status_change_type=(status
888 if status and allowed_to_change_status else None),
928 if status and allowed_to_change_status else None),
889 closing_pr=close_pr,
929 closing_pr=close_pr,
890 comment_type=comment_type,
930 comment_type=comment_type,
891 resolves_comment_id=resolves_comment_id
931 resolves_comment_id=resolves_comment_id
892 )
932 )
893
933
894 if allowed_to_change_status:
934 if allowed_to_change_status:
895 old_calculated_status = pull_request.calculated_review_status()
935 old_calculated_status = pull_request.calculated_review_status()
896 # get status if set !
936 # get status if set !
897 if status:
937 if status:
898 ChangesetStatusModel().set_status(
938 ChangesetStatusModel().set_status(
899 c.rhodecode_db_repo.repo_id,
939 c.rhodecode_db_repo.repo_id,
900 status,
940 status,
901 c.rhodecode_user.user_id,
941 c.rhodecode_user.user_id,
902 comm,
942 comm,
903 pull_request=pull_request_id
943 pull_request=pull_request_id
904 )
944 )
905
945
906 Session().flush()
946 Session().flush()
907 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
908 # we now calculate the status of pull request, and based on that
948 # we now calculate the status of pull request, and based on that
909 # calculation we set the commits status
949 # calculation we set the commits status
910 calculated_status = pull_request.calculated_review_status()
950 calculated_status = pull_request.calculated_review_status()
911 if old_calculated_status != calculated_status:
951 if old_calculated_status != calculated_status:
912 PullRequestModel()._trigger_pull_request_hook(
952 PullRequestModel()._trigger_pull_request_hook(
913 pull_request, c.rhodecode_user, 'review_status_change')
953 pull_request, c.rhodecode_user, 'review_status_change')
914
954
915 calculated_status_lbl = ChangesetStatus.get_status_lbl(
955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
916 calculated_status)
956 calculated_status)
917
957
918 if close_pr:
958 if close_pr:
919 status_completed = (
959 status_completed = (
920 calculated_status in [ChangesetStatus.STATUS_APPROVED,
960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
921 ChangesetStatus.STATUS_REJECTED])
961 ChangesetStatus.STATUS_REJECTED])
922 if close_pull_request or status_completed:
962 if close_pull_request or status_completed:
923 PullRequestModel().close_pull_request(
963 PullRequestModel().close_pull_request(
924 pull_request_id, c.rhodecode_user)
964 pull_request_id, c.rhodecode_user)
925 else:
965 else:
926 h.flash(_('Closing pull request on other statuses than '
966 h.flash(_('Closing pull request on other statuses than '
927 'rejected or approved is forbidden. '
967 'rejected or approved is forbidden. '
928 'Calculated status from all reviewers '
968 'Calculated status from all reviewers '
929 'is currently: %s') % calculated_status_lbl,
969 'is currently: %s') % calculated_status_lbl,
930 category='warning')
970 category='warning')
931
971
932 Session().commit()
972 Session().commit()
933
973
934 if not request.is_xhr:
974 if not request.is_xhr:
935 return redirect(h.url('pullrequest_show', repo_name=repo_name,
975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
936 pull_request_id=pull_request_id))
976 pull_request_id=pull_request_id))
937
977
938 data = {
978 data = {
939 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
940 }
980 }
941 if comm:
981 if comm:
942 c.co = comm
982 c.co = comm
943 c.inline_comment = True if comm.line_no else False
983 c.inline_comment = True if comm.line_no else False
944 data.update(comm.get_dict())
984 data.update(comm.get_dict())
945 data.update({'rendered_text':
985 data.update({'rendered_text':
946 render('changeset/changeset_comment_block.mako')})
986 render('changeset/changeset_comment_block.mako')})
947
987
948 return data
988 return data
949
989
950 @LoginRequired()
990 @LoginRequired()
951 @NotAnonymous()
991 @NotAnonymous()
952 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
953 'repository.admin')
993 'repository.admin')
954 @auth.CSRFRequired()
994 @auth.CSRFRequired()
955 @jsonify
995 @jsonify
956 def delete_comment(self, repo_name, comment_id):
996 def delete_comment(self, repo_name, comment_id):
957 return self._delete_comment(comment_id)
997 return self._delete_comment(comment_id)
958
998
959 def _delete_comment(self, comment_id):
999 def _delete_comment(self, comment_id):
960 comment_id = safe_int(comment_id)
1000 comment_id = safe_int(comment_id)
961 co = ChangesetComment.get_or_404(comment_id)
1001 co = ChangesetComment.get_or_404(comment_id)
962 if co.pull_request.is_closed():
1002 if co.pull_request.is_closed():
963 # don't allow deleting comments on closed pull request
1003 # don't allow deleting comments on closed pull request
964 raise HTTPForbidden()
1004 raise HTTPForbidden()
965
1005
966 is_owner = co.author.user_id == c.rhodecode_user.user_id
1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
967 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
968 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
969 old_calculated_status = co.pull_request.calculated_review_status()
1009 old_calculated_status = co.pull_request.calculated_review_status()
970 CommentsModel().delete(comment=co)
1010 CommentsModel().delete(comment=co)
971 Session().commit()
1011 Session().commit()
972 calculated_status = co.pull_request.calculated_review_status()
1012 calculated_status = co.pull_request.calculated_review_status()
973 if old_calculated_status != calculated_status:
1013 if old_calculated_status != calculated_status:
974 PullRequestModel()._trigger_pull_request_hook(
1014 PullRequestModel()._trigger_pull_request_hook(
975 co.pull_request, c.rhodecode_user, 'review_status_change')
1015 co.pull_request, c.rhodecode_user, 'review_status_change')
976 return True
1016 return True
977 else:
1017 else:
978 raise HTTPForbidden()
1018 raise HTTPForbidden()
@@ -1,109 +1,89 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Utilities to be shared by multiple controllers.
22 Utilities to be shared by multiple controllers.
23
23
24 Should only contain utilities to be shared in the controller layer.
24 Should only contain utilities to be shared in the controller layer.
25 """
25 """
26
26
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.vcs.exceptions import RepositoryError
28 from rhodecode.lib.vcs.exceptions import RepositoryError
29
29
30
30
31 def parse_path_ref(ref, default_path=None):
31 def parse_path_ref(ref, default_path=None):
32 """
32 """
33 Parse out a path and reference combination and return both parts of it.
33 Parse out a path and reference combination and return both parts of it.
34
34
35 This is used to allow support of path based comparisons for Subversion
35 This is used to allow support of path based comparisons for Subversion
36 as an iterim solution in parameter handling.
36 as an iterim solution in parameter handling.
37 """
37 """
38 if '@' in ref:
38 if '@' in ref:
39 return ref.rsplit('@', 1)
39 return ref.rsplit('@', 1)
40 else:
40 else:
41 return default_path, ref
41 return default_path, ref
42
42
43
43
44 def get_format_ref_id(repo):
44 def get_format_ref_id(repo):
45 """Returns a `repo` specific reference formatter function"""
45 """Returns a `repo` specific reference formatter function"""
46 if h.is_svn(repo):
46 if h.is_svn(repo):
47 return _format_ref_id_svn
47 return _format_ref_id_svn
48 else:
48 else:
49 return _format_ref_id
49 return _format_ref_id
50
50
51
51
52 def _format_ref_id(name, raw_id):
52 def _format_ref_id(name, raw_id):
53 """Default formatting of a given reference `name`"""
53 """Default formatting of a given reference `name`"""
54 return name
54 return name
55
55
56
56
57 def _format_ref_id_svn(name, raw_id):
57 def _format_ref_id_svn(name, raw_id):
58 """Special way of formatting a reference for Subversion including path"""
58 """Special way of formatting a reference for Subversion including path"""
59 return '%s@%s' % (name, raw_id)
59 return '%s@%s' % (name, raw_id)
60
60
61
61
62 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
62 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
63 """
63 """
64 Gets the commit for a `ref_name` taking into account `ref_type`.
64 Gets the commit for a `ref_name` taking into account `ref_type`.
65 Needed in case a bookmark / tag share the same name.
65 Needed in case a bookmark / tag share the same name.
66
66
67 :param repo: the repo instance
67 :param repo: the repo instance
68 :param ref_name: the name of the ref to get
68 :param ref_name: the name of the ref to get
69 :param ref_type: optional, used to disambiguate colliding refs
69 :param ref_type: optional, used to disambiguate colliding refs
70 """
70 """
71 repo_scm = repo.scm_instance()
71 repo_scm = repo.scm_instance()
72 ref_type_mapping = {
72 ref_type_mapping = {
73 'book': repo_scm.bookmarks,
73 'book': repo_scm.bookmarks,
74 'bookmark': repo_scm.bookmarks,
74 'bookmark': repo_scm.bookmarks,
75 'tag': repo_scm.tags,
75 'tag': repo_scm.tags,
76 'branch': repo_scm.branches,
76 'branch': repo_scm.branches,
77 }
77 }
78
78
79 commit_id = ref_name
79 commit_id = ref_name
80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
81 # the branch issue with svn is fixed
81 # the branch issue with svn is fixed
82 if ref_type and ref_type in ref_type_mapping:
82 if ref_type and ref_type in ref_type_mapping:
83 try:
83 try:
84 commit_id = ref_type_mapping[ref_type][ref_name]
84 commit_id = ref_type_mapping[ref_type][ref_name]
85 except KeyError:
85 except KeyError:
86 raise RepositoryError(
86 raise RepositoryError(
87 '%s "%s" does not exist' % (ref_type, ref_name))
87 '%s "%s" does not exist' % (ref_type, ref_name))
88
88
89 return repo_scm.get_commit(commit_id)
89 return repo_scm.get_commit(commit_id)
90
91
92 def reviewer_as_json(user, reasons, mandatory):
93 """
94 Returns json struct of a reviewer for frontend
95
96 :param user: the reviewer
97 :param reasons: list of strings of why they are reviewers
98 :param mandatory: bool, to set user as mandatory
99 """
100
101 return {
102 'user_id': user.user_id,
103 'reasons': reasons,
104 'mandatory': mandatory,
105 'username': user.username,
106 'firstname': user.firstname,
107 'lastname': user.lastname,
108 'gravatar_link': h.gravatar_url(user.email, 14),
109 }
@@ -1,268 +1,269 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Changeset status conttroller
22 Changeset status conttroller
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 from collections import defaultdict
27 from collections import defaultdict
28
28
29 from rhodecode.model import BaseModel
29 from rhodecode.model import BaseModel
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 from rhodecode.lib.markup_renderer import (
32 from rhodecode.lib.markup_renderer import (
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class ChangesetStatusModel(BaseModel):
38 class ChangesetStatusModel(BaseModel):
39
39
40 cls = ChangesetStatus
40 cls = ChangesetStatus
41
41
42 def __get_changeset_status(self, changeset_status):
42 def __get_changeset_status(self, changeset_status):
43 return self._get_instance(ChangesetStatus, changeset_status)
43 return self._get_instance(ChangesetStatus, changeset_status)
44
44
45 def __get_pull_request(self, pull_request):
45 def __get_pull_request(self, pull_request):
46 return self._get_instance(PullRequest, pull_request)
46 return self._get_instance(PullRequest, pull_request)
47
47
48 def _get_status_query(self, repo, revision, pull_request,
48 def _get_status_query(self, repo, revision, pull_request,
49 with_revisions=False):
49 with_revisions=False):
50 repo = self._get_repo(repo)
50 repo = self._get_repo(repo)
51
51
52 q = ChangesetStatus.query()\
52 q = ChangesetStatus.query()\
53 .filter(ChangesetStatus.repo == repo)
53 .filter(ChangesetStatus.repo == repo)
54 if not with_revisions:
54 if not with_revisions:
55 q = q.filter(ChangesetStatus.version == 0)
55 q = q.filter(ChangesetStatus.version == 0)
56
56
57 if revision:
57 if revision:
58 q = q.filter(ChangesetStatus.revision == revision)
58 q = q.filter(ChangesetStatus.revision == revision)
59 elif pull_request:
59 elif pull_request:
60 pull_request = self.__get_pull_request(pull_request)
60 pull_request = self.__get_pull_request(pull_request)
61 # TODO: johbo: Think about the impact of this join, there must
61 # TODO: johbo: Think about the impact of this join, there must
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 # to the pull request. Might be that we want to do the same for
63 # to the pull request. Might be that we want to do the same for
64 # the pull_request_version_id.
64 # the pull_request_version_id.
65 q = q.join(ChangesetComment).filter(
65 q = q.join(ChangesetComment).filter(
66 ChangesetStatus.pull_request == pull_request,
66 ChangesetStatus.pull_request == pull_request,
67 ChangesetComment.pull_request_version_id == None)
67 ChangesetComment.pull_request_version_id == None)
68 else:
68 else:
69 raise Exception('Please specify revision or pull_request')
69 raise Exception('Please specify revision or pull_request')
70 q = q.order_by(ChangesetStatus.version.asc())
70 q = q.order_by(ChangesetStatus.version.asc())
71 return q
71 return q
72
72
73 def calculate_status(self, statuses_by_reviewers):
73 def calculate_status(self, statuses_by_reviewers):
74 """
74 """
75 Given the approval statuses from reviewers, calculates final approval
75 Given the approval statuses from reviewers, calculates final approval
76 status. There can only be 3 results, all approved, all rejected. If
76 status. There can only be 3 results, all approved, all rejected. If
77 there is no consensus the PR is under review.
77 there is no consensus the PR is under review.
78
78
79 :param statuses_by_reviewers:
79 :param statuses_by_reviewers:
80 """
80 """
81 votes = defaultdict(int)
81 votes = defaultdict(int)
82 reviewers_number = len(statuses_by_reviewers)
82 reviewers_number = len(statuses_by_reviewers)
83 for user, reasons, statuses in statuses_by_reviewers:
83 for user, reasons, mandatory, statuses in statuses_by_reviewers:
84 if statuses:
84 if statuses:
85 ver, latest = statuses[0]
85 ver, latest = statuses[0]
86 votes[latest.status] += 1
86 votes[latest.status] += 1
87 else:
87 else:
88 votes[ChangesetStatus.DEFAULT] += 1
88 votes[ChangesetStatus.DEFAULT] += 1
89
89
90 # all approved
90 # all approved
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 return ChangesetStatus.STATUS_APPROVED
92 return ChangesetStatus.STATUS_APPROVED
93
93
94 # all rejected
94 # all rejected
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 return ChangesetStatus.STATUS_REJECTED
96 return ChangesetStatus.STATUS_REJECTED
97
97
98 return ChangesetStatus.STATUS_UNDER_REVIEW
98 return ChangesetStatus.STATUS_UNDER_REVIEW
99
99
100 def get_statuses(self, repo, revision=None, pull_request=None,
100 def get_statuses(self, repo, revision=None, pull_request=None,
101 with_revisions=False):
101 with_revisions=False):
102 q = self._get_status_query(repo, revision, pull_request,
102 q = self._get_status_query(repo, revision, pull_request,
103 with_revisions)
103 with_revisions)
104 return q.all()
104 return q.all()
105
105
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 """
107 """
108 Returns latest status of changeset for given revision or for given
108 Returns latest status of changeset for given revision or for given
109 pull request. Statuses are versioned inside a table itself and
109 pull request. Statuses are versioned inside a table itself and
110 version == 0 is always the current one
110 version == 0 is always the current one
111
111
112 :param repo:
112 :param repo:
113 :param revision: 40char hash or None
113 :param revision: 40char hash or None
114 :param pull_request: pull_request reference
114 :param pull_request: pull_request reference
115 :param as_str: return status as string not object
115 :param as_str: return status as string not object
116 """
116 """
117 q = self._get_status_query(repo, revision, pull_request)
117 q = self._get_status_query(repo, revision, pull_request)
118
118
119 # need to use first here since there can be multiple statuses
119 # need to use first here since there can be multiple statuses
120 # returned from pull_request
120 # returned from pull_request
121 status = q.first()
121 status = q.first()
122 if as_str:
122 if as_str:
123 status = status.status if status else status
123 status = status.status if status else status
124 st = status or ChangesetStatus.DEFAULT
124 st = status or ChangesetStatus.DEFAULT
125 return str(st)
125 return str(st)
126 return status
126 return status
127
127
128 def _render_auto_status_message(
128 def _render_auto_status_message(
129 self, status, commit_id=None, pull_request=None):
129 self, status, commit_id=None, pull_request=None):
130 """
130 """
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 so it's always looking the same disregarding on which default
132 so it's always looking the same disregarding on which default
133 renderer system is using.
133 renderer system is using.
134
134
135 :param status: status text to change into
135 :param status: status text to change into
136 :param commit_id: the commit_id we change the status for
136 :param commit_id: the commit_id we change the status for
137 :param pull_request: the pull request we change the status for
137 :param pull_request: the pull request we change the status for
138 """
138 """
139
139
140 new_status = ChangesetStatus.get_status_lbl(status)
140 new_status = ChangesetStatus.get_status_lbl(status)
141
141
142 params = {
142 params = {
143 'new_status_label': new_status,
143 'new_status_label': new_status,
144 'pull_request': pull_request,
144 'pull_request': pull_request,
145 'commit_id': commit_id,
145 'commit_id': commit_id,
146 }
146 }
147 renderer = RstTemplateRenderer()
147 renderer = RstTemplateRenderer()
148 return renderer.render('auto_status_change.mako', **params)
148 return renderer.render('auto_status_change.mako', **params)
149
149
150 def set_status(self, repo, status, user, comment=None, revision=None,
150 def set_status(self, repo, status, user, comment=None, revision=None,
151 pull_request=None, dont_allow_on_closed_pull_request=False):
151 pull_request=None, dont_allow_on_closed_pull_request=False):
152 """
152 """
153 Creates new status for changeset or updates the old ones bumping their
153 Creates new status for changeset or updates the old ones bumping their
154 version, leaving the current status at
154 version, leaving the current status at
155
155
156 :param repo:
156 :param repo:
157 :param revision:
157 :param revision:
158 :param status:
158 :param status:
159 :param user:
159 :param user:
160 :param comment:
160 :param comment:
161 :param dont_allow_on_closed_pull_request: don't allow a status change
161 :param dont_allow_on_closed_pull_request: don't allow a status change
162 if last status was for pull request and it's closed. We shouldn't
162 if last status was for pull request and it's closed. We shouldn't
163 mess around this manually
163 mess around this manually
164 """
164 """
165 repo = self._get_repo(repo)
165 repo = self._get_repo(repo)
166
166
167 q = ChangesetStatus.query()
167 q = ChangesetStatus.query()
168
168
169 if revision:
169 if revision:
170 q = q.filter(ChangesetStatus.repo == repo)
170 q = q.filter(ChangesetStatus.repo == repo)
171 q = q.filter(ChangesetStatus.revision == revision)
171 q = q.filter(ChangesetStatus.revision == revision)
172 elif pull_request:
172 elif pull_request:
173 pull_request = self.__get_pull_request(pull_request)
173 pull_request = self.__get_pull_request(pull_request)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 cur_statuses = q.all()
176 cur_statuses = q.all()
177
177
178 # if statuses exists and last is associated with a closed pull request
178 # if statuses exists and last is associated with a closed pull request
179 # we need to check if we can allow this status change
179 # we need to check if we can allow this status change
180 if (dont_allow_on_closed_pull_request and cur_statuses
180 if (dont_allow_on_closed_pull_request and cur_statuses
181 and getattr(cur_statuses[0].pull_request, 'status', '')
181 and getattr(cur_statuses[0].pull_request, 'status', '')
182 == PullRequest.STATUS_CLOSED):
182 == PullRequest.STATUS_CLOSED):
183 raise StatusChangeOnClosedPullRequestError(
183 raise StatusChangeOnClosedPullRequestError(
184 'Changing status on closed pull request is not allowed'
184 'Changing status on closed pull request is not allowed'
185 )
185 )
186
186
187 # update all current statuses with older version
187 # update all current statuses with older version
188 if cur_statuses:
188 if cur_statuses:
189 for st in cur_statuses:
189 for st in cur_statuses:
190 st.version += 1
190 st.version += 1
191 self.sa.add(st)
191 self.sa.add(st)
192
192
193 def _create_status(user, repo, status, comment, revision, pull_request):
193 def _create_status(user, repo, status, comment, revision, pull_request):
194 new_status = ChangesetStatus()
194 new_status = ChangesetStatus()
195 new_status.author = self._get_user(user)
195 new_status.author = self._get_user(user)
196 new_status.repo = self._get_repo(repo)
196 new_status.repo = self._get_repo(repo)
197 new_status.status = status
197 new_status.status = status
198 new_status.comment = comment
198 new_status.comment = comment
199 new_status.revision = revision
199 new_status.revision = revision
200 new_status.pull_request = pull_request
200 new_status.pull_request = pull_request
201 return new_status
201 return new_status
202
202
203 if not comment:
203 if not comment:
204 from rhodecode.model.comment import CommentsModel
204 from rhodecode.model.comment import CommentsModel
205 comment = CommentsModel().create(
205 comment = CommentsModel().create(
206 text=self._render_auto_status_message(
206 text=self._render_auto_status_message(
207 status, commit_id=revision, pull_request=pull_request),
207 status, commit_id=revision, pull_request=pull_request),
208 repo=repo,
208 repo=repo,
209 user=user,
209 user=user,
210 pull_request=pull_request,
210 pull_request=pull_request,
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 )
212 )
213
213
214 if revision:
214 if revision:
215 new_status = _create_status(
215 new_status = _create_status(
216 user=user, repo=repo, status=status, comment=comment,
216 user=user, repo=repo, status=status, comment=comment,
217 revision=revision, pull_request=pull_request)
217 revision=revision, pull_request=pull_request)
218 self.sa.add(new_status)
218 self.sa.add(new_status)
219 return new_status
219 return new_status
220 elif pull_request:
220 elif pull_request:
221 # pull request can have more than one revision associated to it
221 # pull request can have more than one revision associated to it
222 # we need to create new version for each one
222 # we need to create new version for each one
223 new_statuses = []
223 new_statuses = []
224 repo = pull_request.source_repo
224 repo = pull_request.source_repo
225 for rev in pull_request.revisions:
225 for rev in pull_request.revisions:
226 new_status = _create_status(
226 new_status = _create_status(
227 user=user, repo=repo, status=status, comment=comment,
227 user=user, repo=repo, status=status, comment=comment,
228 revision=rev, pull_request=pull_request)
228 revision=rev, pull_request=pull_request)
229 new_statuses.append(new_status)
229 new_statuses.append(new_status)
230 self.sa.add(new_status)
230 self.sa.add(new_status)
231 return new_statuses
231 return new_statuses
232
232
233 def reviewers_statuses(self, pull_request):
233 def reviewers_statuses(self, pull_request):
234 _commit_statuses = self.get_statuses(
234 _commit_statuses = self.get_statuses(
235 pull_request.source_repo,
235 pull_request.source_repo,
236 pull_request=pull_request,
236 pull_request=pull_request,
237 with_revisions=True)
237 with_revisions=True)
238
238
239 commit_statuses = defaultdict(list)
239 commit_statuses = defaultdict(list)
240 for st in _commit_statuses:
240 for st in _commit_statuses:
241 commit_statuses[st.author.username] += [st]
241 commit_statuses[st.author.username] += [st]
242
242
243 pull_request_reviewers = []
243 pull_request_reviewers = []
244
244
245 def version(commit_status):
245 def version(commit_status):
246 return commit_status.version
246 return commit_status.version
247
247
248 for o in pull_request.reviewers:
248 for o in pull_request.reviewers:
249 if not o.user:
249 if not o.user:
250 continue
250 continue
251 st = commit_statuses.get(o.user.username, None)
251 statuses = commit_statuses.get(o.user.username, None)
252 if st:
252 if statuses:
253 st = [(x, list(y)[0])
253 statuses = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
254 for x, y in (itertools.groupby(
255 version))]
255 sorted(statuses, key=version),version))]
256
256
257 pull_request_reviewers.append((o.user, o.reasons, st))
257 pull_request_reviewers.append(
258 (o.user, o.reasons, o.mandatory, statuses))
258 return pull_request_reviewers
259 return pull_request_reviewers
259
260
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 """
262 """
262 calculate pull request status based on reviewers, it should be a list
263 calculate pull request status based on reviewers, it should be a list
263 of two element lists.
264 of two element lists.
264
265
265 :param reviewers_statuses:
266 :param reviewers_statuses:
266 """
267 """
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 return self.calculate_status(reviewers)
269 return self.calculate_status(reviewers)
@@ -1,3993 +1,4015 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pylons import url
47 from pylons import url
48 from pylons.i18n.translation import lazy_ugettext as _
48 from pylons.i18n.translation import lazy_ugettext as _
49
49
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
209 def get_or_404(cls, id_, pyramid_exc=False):
210 if pyramid_exc:
210 if pyramid_exc:
211 # NOTE(marcink): backward compat, once migration to pyramid
211 # NOTE(marcink): backward compat, once migration to pyramid
212 # this should only use pyramid exceptions
212 # this should only use pyramid exceptions
213 from pyramid.httpexceptions import HTTPNotFound
213 from pyramid.httpexceptions import HTTPNotFound
214 else:
214 else:
215 from webob.exc import HTTPNotFound
215 from webob.exc import HTTPNotFound
216
216
217 try:
217 try:
218 id_ = int(id_)
218 id_ = int(id_)
219 except (TypeError, ValueError):
219 except (TypeError, ValueError):
220 raise HTTPNotFound
220 raise HTTPNotFound
221
221
222 res = cls.query().get(id_)
222 res = cls.query().get(id_)
223 if not res:
223 if not res:
224 raise HTTPNotFound
224 raise HTTPNotFound
225 return res
225 return res
226
226
227 @classmethod
227 @classmethod
228 def getAll(cls):
228 def getAll(cls):
229 # deprecated and left for backward compatibility
229 # deprecated and left for backward compatibility
230 return cls.get_all()
230 return cls.get_all()
231
231
232 @classmethod
232 @classmethod
233 def get_all(cls):
233 def get_all(cls):
234 return cls.query().all()
234 return cls.query().all()
235
235
236 @classmethod
236 @classmethod
237 def delete(cls, id_):
237 def delete(cls, id_):
238 obj = cls.query().get(id_)
238 obj = cls.query().get(id_)
239 Session().delete(obj)
239 Session().delete(obj)
240
240
241 @classmethod
241 @classmethod
242 def identity_cache(cls, session, attr_name, value):
242 def identity_cache(cls, session, attr_name, value):
243 exist_in_session = []
243 exist_in_session = []
244 for (item_cls, pkey), instance in session.identity_map.items():
244 for (item_cls, pkey), instance in session.identity_map.items():
245 if cls == item_cls and getattr(instance, attr_name) == value:
245 if cls == item_cls and getattr(instance, attr_name) == value:
246 exist_in_session.append(instance)
246 exist_in_session.append(instance)
247 if exist_in_session:
247 if exist_in_session:
248 if len(exist_in_session) == 1:
248 if len(exist_in_session) == 1:
249 return exist_in_session[0]
249 return exist_in_session[0]
250 log.exception(
250 log.exception(
251 'multiple objects with attr %s and '
251 'multiple objects with attr %s and '
252 'value %s found with same name: %r',
252 'value %s found with same name: %r',
253 attr_name, value, exist_in_session)
253 attr_name, value, exist_in_session)
254
254
255 def __repr__(self):
255 def __repr__(self):
256 if hasattr(self, '__unicode__'):
256 if hasattr(self, '__unicode__'):
257 # python repr needs to return str
257 # python repr needs to return str
258 try:
258 try:
259 return safe_str(self.__unicode__())
259 return safe_str(self.__unicode__())
260 except UnicodeDecodeError:
260 except UnicodeDecodeError:
261 pass
261 pass
262 return '<DB:%s>' % (self.__class__.__name__)
262 return '<DB:%s>' % (self.__class__.__name__)
263
263
264
264
265 class RhodeCodeSetting(Base, BaseModel):
265 class RhodeCodeSetting(Base, BaseModel):
266 __tablename__ = 'rhodecode_settings'
266 __tablename__ = 'rhodecode_settings'
267 __table_args__ = (
267 __table_args__ = (
268 UniqueConstraint('app_settings_name'),
268 UniqueConstraint('app_settings_name'),
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 )
271 )
272
272
273 SETTINGS_TYPES = {
273 SETTINGS_TYPES = {
274 'str': safe_str,
274 'str': safe_str,
275 'int': safe_int,
275 'int': safe_int,
276 'unicode': safe_unicode,
276 'unicode': safe_unicode,
277 'bool': str2bool,
277 'bool': str2bool,
278 'list': functools.partial(aslist, sep=',')
278 'list': functools.partial(aslist, sep=',')
279 }
279 }
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 GLOBAL_CONF_KEY = 'app_settings'
281 GLOBAL_CONF_KEY = 'app_settings'
282
282
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287
287
288 def __init__(self, key='', val='', type='unicode'):
288 def __init__(self, key='', val='', type='unicode'):
289 self.app_settings_name = key
289 self.app_settings_name = key
290 self.app_settings_type = type
290 self.app_settings_type = type
291 self.app_settings_value = val
291 self.app_settings_value = val
292
292
293 @validates('_app_settings_value')
293 @validates('_app_settings_value')
294 def validate_settings_value(self, key, val):
294 def validate_settings_value(self, key, val):
295 assert type(val) == unicode
295 assert type(val) == unicode
296 return val
296 return val
297
297
298 @hybrid_property
298 @hybrid_property
299 def app_settings_value(self):
299 def app_settings_value(self):
300 v = self._app_settings_value
300 v = self._app_settings_value
301 _type = self.app_settings_type
301 _type = self.app_settings_type
302 if _type:
302 if _type:
303 _type = self.app_settings_type.split('.')[0]
303 _type = self.app_settings_type.split('.')[0]
304 # decode the encrypted value
304 # decode the encrypted value
305 if 'encrypted' in self.app_settings_type:
305 if 'encrypted' in self.app_settings_type:
306 cipher = EncryptedTextValue()
306 cipher = EncryptedTextValue()
307 v = safe_unicode(cipher.process_result_value(v, None))
307 v = safe_unicode(cipher.process_result_value(v, None))
308
308
309 converter = self.SETTINGS_TYPES.get(_type) or \
309 converter = self.SETTINGS_TYPES.get(_type) or \
310 self.SETTINGS_TYPES['unicode']
310 self.SETTINGS_TYPES['unicode']
311 return converter(v)
311 return converter(v)
312
312
313 @app_settings_value.setter
313 @app_settings_value.setter
314 def app_settings_value(self, val):
314 def app_settings_value(self, val):
315 """
315 """
316 Setter that will always make sure we use unicode in app_settings_value
316 Setter that will always make sure we use unicode in app_settings_value
317
317
318 :param val:
318 :param val:
319 """
319 """
320 val = safe_unicode(val)
320 val = safe_unicode(val)
321 # encode the encrypted value
321 # encode the encrypted value
322 if 'encrypted' in self.app_settings_type:
322 if 'encrypted' in self.app_settings_type:
323 cipher = EncryptedTextValue()
323 cipher = EncryptedTextValue()
324 val = safe_unicode(cipher.process_bind_param(val, None))
324 val = safe_unicode(cipher.process_bind_param(val, None))
325 self._app_settings_value = val
325 self._app_settings_value = val
326
326
327 @hybrid_property
327 @hybrid_property
328 def app_settings_type(self):
328 def app_settings_type(self):
329 return self._app_settings_type
329 return self._app_settings_type
330
330
331 @app_settings_type.setter
331 @app_settings_type.setter
332 def app_settings_type(self, val):
332 def app_settings_type(self, val):
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 raise Exception('type must be one of %s got %s'
334 raise Exception('type must be one of %s got %s'
335 % (self.SETTINGS_TYPES.keys(), val))
335 % (self.SETTINGS_TYPES.keys(), val))
336 self._app_settings_type = val
336 self._app_settings_type = val
337
337
338 def __unicode__(self):
338 def __unicode__(self):
339 return u"<%s('%s:%s[%s]')>" % (
339 return u"<%s('%s:%s[%s]')>" % (
340 self.__class__.__name__,
340 self.__class__.__name__,
341 self.app_settings_name, self.app_settings_value,
341 self.app_settings_name, self.app_settings_value,
342 self.app_settings_type
342 self.app_settings_type
343 )
343 )
344
344
345
345
346 class RhodeCodeUi(Base, BaseModel):
346 class RhodeCodeUi(Base, BaseModel):
347 __tablename__ = 'rhodecode_ui'
347 __tablename__ = 'rhodecode_ui'
348 __table_args__ = (
348 __table_args__ = (
349 UniqueConstraint('ui_key'),
349 UniqueConstraint('ui_key'),
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 )
352 )
353
353
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 # HG
355 # HG
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PULL = 'outgoing.pull_logger'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 HOOK_PUSH = 'changegroup.push_logger'
360 HOOK_PUSH = 'changegroup.push_logger'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
362
362
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 # git part is currently hardcoded.
364 # git part is currently hardcoded.
365
365
366 # SVN PATTERNS
366 # SVN PATTERNS
367 SVN_BRANCH_ID = 'vcs_svn_branch'
367 SVN_BRANCH_ID = 'vcs_svn_branch'
368 SVN_TAG_ID = 'vcs_svn_tag'
368 SVN_TAG_ID = 'vcs_svn_tag'
369
369
370 ui_id = Column(
370 ui_id = Column(
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 primary_key=True)
372 primary_key=True)
373 ui_section = Column(
373 ui_section = Column(
374 "ui_section", String(255), nullable=True, unique=None, default=None)
374 "ui_section", String(255), nullable=True, unique=None, default=None)
375 ui_key = Column(
375 ui_key = Column(
376 "ui_key", String(255), nullable=True, unique=None, default=None)
376 "ui_key", String(255), nullable=True, unique=None, default=None)
377 ui_value = Column(
377 ui_value = Column(
378 "ui_value", String(255), nullable=True, unique=None, default=None)
378 "ui_value", String(255), nullable=True, unique=None, default=None)
379 ui_active = Column(
379 ui_active = Column(
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 self.ui_key, self.ui_value)
384 self.ui_key, self.ui_value)
385
385
386
386
387 class RepoRhodeCodeSetting(Base, BaseModel):
387 class RepoRhodeCodeSetting(Base, BaseModel):
388 __tablename__ = 'repo_rhodecode_settings'
388 __tablename__ = 'repo_rhodecode_settings'
389 __table_args__ = (
389 __table_args__ = (
390 UniqueConstraint(
390 UniqueConstraint(
391 'app_settings_name', 'repository_id',
391 'app_settings_name', 'repository_id',
392 name='uq_repo_rhodecode_setting_name_repo_id'),
392 name='uq_repo_rhodecode_setting_name_repo_id'),
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 )
395 )
396
396
397 repository_id = Column(
397 repository_id = Column(
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 nullable=False)
399 nullable=False)
400 app_settings_id = Column(
400 app_settings_id = Column(
401 "app_settings_id", Integer(), nullable=False, unique=True,
401 "app_settings_id", Integer(), nullable=False, unique=True,
402 default=None, primary_key=True)
402 default=None, primary_key=True)
403 app_settings_name = Column(
403 app_settings_name = Column(
404 "app_settings_name", String(255), nullable=True, unique=None,
404 "app_settings_name", String(255), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_value = Column(
406 _app_settings_value = Column(
407 "app_settings_value", String(4096), nullable=True, unique=None,
407 "app_settings_value", String(4096), nullable=True, unique=None,
408 default=None)
408 default=None)
409 _app_settings_type = Column(
409 _app_settings_type = Column(
410 "app_settings_type", String(255), nullable=True, unique=None,
410 "app_settings_type", String(255), nullable=True, unique=None,
411 default=None)
411 default=None)
412
412
413 repository = relationship('Repository')
413 repository = relationship('Repository')
414
414
415 def __init__(self, repository_id, key='', val='', type='unicode'):
415 def __init__(self, repository_id, key='', val='', type='unicode'):
416 self.repository_id = repository_id
416 self.repository_id = repository_id
417 self.app_settings_name = key
417 self.app_settings_name = key
418 self.app_settings_type = type
418 self.app_settings_type = type
419 self.app_settings_value = val
419 self.app_settings_value = val
420
420
421 @validates('_app_settings_value')
421 @validates('_app_settings_value')
422 def validate_settings_value(self, key, val):
422 def validate_settings_value(self, key, val):
423 assert type(val) == unicode
423 assert type(val) == unicode
424 return val
424 return val
425
425
426 @hybrid_property
426 @hybrid_property
427 def app_settings_value(self):
427 def app_settings_value(self):
428 v = self._app_settings_value
428 v = self._app_settings_value
429 type_ = self.app_settings_type
429 type_ = self.app_settings_type
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 return converter(v)
432 return converter(v)
433
433
434 @app_settings_value.setter
434 @app_settings_value.setter
435 def app_settings_value(self, val):
435 def app_settings_value(self, val):
436 """
436 """
437 Setter that will always make sure we use unicode in app_settings_value
437 Setter that will always make sure we use unicode in app_settings_value
438
438
439 :param val:
439 :param val:
440 """
440 """
441 self._app_settings_value = safe_unicode(val)
441 self._app_settings_value = safe_unicode(val)
442
442
443 @hybrid_property
443 @hybrid_property
444 def app_settings_type(self):
444 def app_settings_type(self):
445 return self._app_settings_type
445 return self._app_settings_type
446
446
447 @app_settings_type.setter
447 @app_settings_type.setter
448 def app_settings_type(self, val):
448 def app_settings_type(self, val):
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 if val not in SETTINGS_TYPES:
450 if val not in SETTINGS_TYPES:
451 raise Exception('type must be one of %s got %s'
451 raise Exception('type must be one of %s got %s'
452 % (SETTINGS_TYPES.keys(), val))
452 % (SETTINGS_TYPES.keys(), val))
453 self._app_settings_type = val
453 self._app_settings_type = val
454
454
455 def __unicode__(self):
455 def __unicode__(self):
456 return u"<%s('%s:%s:%s[%s]')>" % (
456 return u"<%s('%s:%s:%s[%s]')>" % (
457 self.__class__.__name__, self.repository.repo_name,
457 self.__class__.__name__, self.repository.repo_name,
458 self.app_settings_name, self.app_settings_value,
458 self.app_settings_name, self.app_settings_value,
459 self.app_settings_type
459 self.app_settings_type
460 )
460 )
461
461
462
462
463 class RepoRhodeCodeUi(Base, BaseModel):
463 class RepoRhodeCodeUi(Base, BaseModel):
464 __tablename__ = 'repo_rhodecode_ui'
464 __tablename__ = 'repo_rhodecode_ui'
465 __table_args__ = (
465 __table_args__ = (
466 UniqueConstraint(
466 UniqueConstraint(
467 'repository_id', 'ui_section', 'ui_key',
467 'repository_id', 'ui_section', 'ui_key',
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 )
471 )
472
472
473 repository_id = Column(
473 repository_id = Column(
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 nullable=False)
475 nullable=False)
476 ui_id = Column(
476 ui_id = Column(
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 primary_key=True)
478 primary_key=True)
479 ui_section = Column(
479 ui_section = Column(
480 "ui_section", String(255), nullable=True, unique=None, default=None)
480 "ui_section", String(255), nullable=True, unique=None, default=None)
481 ui_key = Column(
481 ui_key = Column(
482 "ui_key", String(255), nullable=True, unique=None, default=None)
482 "ui_key", String(255), nullable=True, unique=None, default=None)
483 ui_value = Column(
483 ui_value = Column(
484 "ui_value", String(255), nullable=True, unique=None, default=None)
484 "ui_value", String(255), nullable=True, unique=None, default=None)
485 ui_active = Column(
485 ui_active = Column(
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487
487
488 repository = relationship('Repository')
488 repository = relationship('Repository')
489
489
490 def __repr__(self):
490 def __repr__(self):
491 return '<%s[%s:%s]%s=>%s]>' % (
491 return '<%s[%s:%s]%s=>%s]>' % (
492 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
493 self.ui_section, self.ui_key, self.ui_value)
493 self.ui_section, self.ui_key, self.ui_value)
494
494
495
495
496 class User(Base, BaseModel):
496 class User(Base, BaseModel):
497 __tablename__ = 'users'
497 __tablename__ = 'users'
498 __table_args__ = (
498 __table_args__ = (
499 UniqueConstraint('username'), UniqueConstraint('email'),
499 UniqueConstraint('username'), UniqueConstraint('email'),
500 Index('u_username_idx', 'username'),
500 Index('u_username_idx', 'username'),
501 Index('u_email_idx', 'email'),
501 Index('u_email_idx', 'email'),
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 )
504 )
505 DEFAULT_USER = 'default'
505 DEFAULT_USER = 'default'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508
508
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519
519
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526
526
527 user_log = relationship('UserLog')
527 user_log = relationship('UserLog')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529
529
530 repositories = relationship('Repository')
530 repositories = relationship('Repository')
531 repository_groups = relationship('RepoGroup')
531 repository_groups = relationship('RepoGroup')
532 user_groups = relationship('UserGroup')
532 user_groups = relationship('UserGroup')
533
533
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536
536
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540
540
541 group_member = relationship('UserGroupMember', cascade='all')
541 group_member = relationship('UserGroupMember', cascade='all')
542
542
543 notifications = relationship('UserNotification', cascade='all')
543 notifications = relationship('UserNotification', cascade='all')
544 # notifications assigned to this user
544 # notifications assigned to this user
545 user_created_notifications = relationship('Notification', cascade='all')
545 user_created_notifications = relationship('Notification', cascade='all')
546 # comments created by this user
546 # comments created by this user
547 user_comments = relationship('ChangesetComment', cascade='all')
547 user_comments = relationship('ChangesetComment', cascade='all')
548 # user profile extra info
548 # user profile extra info
549 user_emails = relationship('UserEmailMap', cascade='all')
549 user_emails = relationship('UserEmailMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 # gists
552 # gists
553 user_gists = relationship('Gist', cascade='all')
553 user_gists = relationship('Gist', cascade='all')
554 # user pull requests
554 # user pull requests
555 user_pull_requests = relationship('PullRequest', cascade='all')
555 user_pull_requests = relationship('PullRequest', cascade='all')
556 # external identities
556 # external identities
557 extenal_identities = relationship(
557 extenal_identities = relationship(
558 'ExternalIdentity',
558 'ExternalIdentity',
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 cascade='all')
560 cascade='all')
561
561
562 def __unicode__(self):
562 def __unicode__(self):
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 self.user_id, self.username)
564 self.user_id, self.username)
565
565
566 @hybrid_property
566 @hybrid_property
567 def email(self):
567 def email(self):
568 return self._email
568 return self._email
569
569
570 @email.setter
570 @email.setter
571 def email(self, val):
571 def email(self, val):
572 self._email = val.lower() if val else None
572 self._email = val.lower() if val else None
573
573
574 @hybrid_property
574 @hybrid_property
575 def api_key(self):
575 def api_key(self):
576 """
576 """
577 Fetch if exist an auth-token with role ALL connected to this user
577 Fetch if exist an auth-token with role ALL connected to this user
578 """
578 """
579 user_auth_token = UserApiKeys.query()\
579 user_auth_token = UserApiKeys.query()\
580 .filter(UserApiKeys.user_id == self.user_id)\
580 .filter(UserApiKeys.user_id == self.user_id)\
581 .filter(or_(UserApiKeys.expires == -1,
581 .filter(or_(UserApiKeys.expires == -1,
582 UserApiKeys.expires >= time.time()))\
582 UserApiKeys.expires >= time.time()))\
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
584 if user_auth_token:
584 if user_auth_token:
585 user_auth_token = user_auth_token.api_key
585 user_auth_token = user_auth_token.api_key
586
586
587 return user_auth_token
587 return user_auth_token
588
588
589 @api_key.setter
589 @api_key.setter
590 def api_key(self, val):
590 def api_key(self, val):
591 # don't allow to set API key this is deprecated for now
591 # don't allow to set API key this is deprecated for now
592 self._api_key = None
592 self._api_key = None
593
593
594 @property
594 @property
595 def firstname(self):
595 def firstname(self):
596 # alias for future
596 # alias for future
597 return self.name
597 return self.name
598
598
599 @property
599 @property
600 def emails(self):
600 def emails(self):
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
602 return [self.email] + [x.email for x in other]
602 return [self.email] + [x.email for x in other]
603
603
604 @property
604 @property
605 def auth_tokens(self):
605 def auth_tokens(self):
606 return [x.api_key for x in self.extra_auth_tokens]
606 return [x.api_key for x in self.extra_auth_tokens]
607
607
608 @property
608 @property
609 def extra_auth_tokens(self):
609 def extra_auth_tokens(self):
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
611
611
612 @property
612 @property
613 def feed_token(self):
613 def feed_token(self):
614 return self.get_feed_token()
614 return self.get_feed_token()
615
615
616 def get_feed_token(self):
616 def get_feed_token(self):
617 feed_tokens = UserApiKeys.query()\
617 feed_tokens = UserApiKeys.query()\
618 .filter(UserApiKeys.user == self)\
618 .filter(UserApiKeys.user == self)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
620 .all()
620 .all()
621 if feed_tokens:
621 if feed_tokens:
622 return feed_tokens[0].api_key
622 return feed_tokens[0].api_key
623 return 'NO_FEED_TOKEN_AVAILABLE'
623 return 'NO_FEED_TOKEN_AVAILABLE'
624
624
625 @classmethod
625 @classmethod
626 def extra_valid_auth_tokens(cls, user, role=None):
626 def extra_valid_auth_tokens(cls, user, role=None):
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
628 .filter(or_(UserApiKeys.expires == -1,
628 .filter(or_(UserApiKeys.expires == -1,
629 UserApiKeys.expires >= time.time()))
629 UserApiKeys.expires >= time.time()))
630 if role:
630 if role:
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
633 return tokens.all()
633 return tokens.all()
634
634
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
636 from rhodecode.lib import auth
636 from rhodecode.lib import auth
637
637
638 log.debug('Trying to authenticate user: %s via auth-token, '
638 log.debug('Trying to authenticate user: %s via auth-token, '
639 'and roles: %s', self, roles)
639 'and roles: %s', self, roles)
640
640
641 if not auth_token:
641 if not auth_token:
642 return False
642 return False
643
643
644 crypto_backend = auth.crypto_backend()
644 crypto_backend = auth.crypto_backend()
645
645
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
647 tokens_q = UserApiKeys.query()\
647 tokens_q = UserApiKeys.query()\
648 .filter(UserApiKeys.user_id == self.user_id)\
648 .filter(UserApiKeys.user_id == self.user_id)\
649 .filter(or_(UserApiKeys.expires == -1,
649 .filter(or_(UserApiKeys.expires == -1,
650 UserApiKeys.expires >= time.time()))
650 UserApiKeys.expires >= time.time()))
651
651
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
653
653
654 plain_tokens = []
654 plain_tokens = []
655 hash_tokens = []
655 hash_tokens = []
656
656
657 for token in tokens_q.all():
657 for token in tokens_q.all():
658 # verify scope first
658 # verify scope first
659 if token.repo_id:
659 if token.repo_id:
660 # token has a scope, we need to verify it
660 # token has a scope, we need to verify it
661 if scope_repo_id != token.repo_id:
661 if scope_repo_id != token.repo_id:
662 log.debug(
662 log.debug(
663 'Scope mismatch: token has a set repo scope: %s, '
663 'Scope mismatch: token has a set repo scope: %s, '
664 'and calling scope is:%s, skipping further checks',
664 'and calling scope is:%s, skipping further checks',
665 token.repo, scope_repo_id)
665 token.repo, scope_repo_id)
666 # token has a scope, and it doesn't match, skip token
666 # token has a scope, and it doesn't match, skip token
667 continue
667 continue
668
668
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
670 hash_tokens.append(token.api_key)
670 hash_tokens.append(token.api_key)
671 else:
671 else:
672 plain_tokens.append(token.api_key)
672 plain_tokens.append(token.api_key)
673
673
674 is_plain_match = auth_token in plain_tokens
674 is_plain_match = auth_token in plain_tokens
675 if is_plain_match:
675 if is_plain_match:
676 return True
676 return True
677
677
678 for hashed in hash_tokens:
678 for hashed in hash_tokens:
679 # TODO(marcink): this is expensive to calculate, but most secure
679 # TODO(marcink): this is expensive to calculate, but most secure
680 match = crypto_backend.hash_check(auth_token, hashed)
680 match = crypto_backend.hash_check(auth_token, hashed)
681 if match:
681 if match:
682 return True
682 return True
683
683
684 return False
684 return False
685
685
686 @property
686 @property
687 def ip_addresses(self):
687 def ip_addresses(self):
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
689 return [x.ip_addr for x in ret]
689 return [x.ip_addr for x in ret]
690
690
691 @property
691 @property
692 def username_and_name(self):
692 def username_and_name(self):
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
694
694
695 @property
695 @property
696 def username_or_name_or_email(self):
696 def username_or_name_or_email(self):
697 full_name = self.full_name if self.full_name is not ' ' else None
697 full_name = self.full_name if self.full_name is not ' ' else None
698 return self.username or full_name or self.email
698 return self.username or full_name or self.email
699
699
700 @property
700 @property
701 def full_name(self):
701 def full_name(self):
702 return '%s %s' % (self.firstname, self.lastname)
702 return '%s %s' % (self.firstname, self.lastname)
703
703
704 @property
704 @property
705 def full_name_or_username(self):
705 def full_name_or_username(self):
706 return ('%s %s' % (self.firstname, self.lastname)
706 return ('%s %s' % (self.firstname, self.lastname)
707 if (self.firstname and self.lastname) else self.username)
707 if (self.firstname and self.lastname) else self.username)
708
708
709 @property
709 @property
710 def full_contact(self):
710 def full_contact(self):
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
712
712
713 @property
713 @property
714 def short_contact(self):
714 def short_contact(self):
715 return '%s %s' % (self.firstname, self.lastname)
715 return '%s %s' % (self.firstname, self.lastname)
716
716
717 @property
717 @property
718 def is_admin(self):
718 def is_admin(self):
719 return self.admin
719 return self.admin
720
720
721 @property
721 @property
722 def AuthUser(self):
722 def AuthUser(self):
723 """
723 """
724 Returns instance of AuthUser for this user
724 Returns instance of AuthUser for this user
725 """
725 """
726 from rhodecode.lib.auth import AuthUser
726 from rhodecode.lib.auth import AuthUser
727 return AuthUser(user_id=self.user_id, username=self.username)
727 return AuthUser(user_id=self.user_id, username=self.username)
728
728
729 @hybrid_property
729 @hybrid_property
730 def user_data(self):
730 def user_data(self):
731 if not self._user_data:
731 if not self._user_data:
732 return {}
732 return {}
733
733
734 try:
734 try:
735 return json.loads(self._user_data)
735 return json.loads(self._user_data)
736 except TypeError:
736 except TypeError:
737 return {}
737 return {}
738
738
739 @user_data.setter
739 @user_data.setter
740 def user_data(self, val):
740 def user_data(self, val):
741 if not isinstance(val, dict):
741 if not isinstance(val, dict):
742 raise Exception('user_data must be dict, got %s' % type(val))
742 raise Exception('user_data must be dict, got %s' % type(val))
743 try:
743 try:
744 self._user_data = json.dumps(val)
744 self._user_data = json.dumps(val)
745 except Exception:
745 except Exception:
746 log.error(traceback.format_exc())
746 log.error(traceback.format_exc())
747
747
748 @classmethod
748 @classmethod
749 def get_by_username(cls, username, case_insensitive=False,
749 def get_by_username(cls, username, case_insensitive=False,
750 cache=False, identity_cache=False):
750 cache=False, identity_cache=False):
751 session = Session()
751 session = Session()
752
752
753 if case_insensitive:
753 if case_insensitive:
754 q = cls.query().filter(
754 q = cls.query().filter(
755 func.lower(cls.username) == func.lower(username))
755 func.lower(cls.username) == func.lower(username))
756 else:
756 else:
757 q = cls.query().filter(cls.username == username)
757 q = cls.query().filter(cls.username == username)
758
758
759 if cache:
759 if cache:
760 if identity_cache:
760 if identity_cache:
761 val = cls.identity_cache(session, 'username', username)
761 val = cls.identity_cache(session, 'username', username)
762 if val:
762 if val:
763 return val
763 return val
764 else:
764 else:
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
766 q = q.options(
766 q = q.options(
767 FromCache("sql_cache_short", cache_key))
767 FromCache("sql_cache_short", cache_key))
768
768
769 return q.scalar()
769 return q.scalar()
770
770
771 @classmethod
771 @classmethod
772 def get_by_auth_token(cls, auth_token, cache=False):
772 def get_by_auth_token(cls, auth_token, cache=False):
773 q = UserApiKeys.query()\
773 q = UserApiKeys.query()\
774 .filter(UserApiKeys.api_key == auth_token)\
774 .filter(UserApiKeys.api_key == auth_token)\
775 .filter(or_(UserApiKeys.expires == -1,
775 .filter(or_(UserApiKeys.expires == -1,
776 UserApiKeys.expires >= time.time()))
776 UserApiKeys.expires >= time.time()))
777 if cache:
777 if cache:
778 q = q.options(
778 q = q.options(
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
780
780
781 match = q.first()
781 match = q.first()
782 if match:
782 if match:
783 return match.user
783 return match.user
784
784
785 @classmethod
785 @classmethod
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
787
787
788 if case_insensitive:
788 if case_insensitive:
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
790
790
791 else:
791 else:
792 q = cls.query().filter(cls.email == email)
792 q = cls.query().filter(cls.email == email)
793
793
794 email_key = _hash_key(email)
794 email_key = _hash_key(email)
795 if cache:
795 if cache:
796 q = q.options(
796 q = q.options(
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
798
798
799 ret = q.scalar()
799 ret = q.scalar()
800 if ret is None:
800 if ret is None:
801 q = UserEmailMap.query()
801 q = UserEmailMap.query()
802 # try fetching in alternate email map
802 # try fetching in alternate email map
803 if case_insensitive:
803 if case_insensitive:
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
805 else:
805 else:
806 q = q.filter(UserEmailMap.email == email)
806 q = q.filter(UserEmailMap.email == email)
807 q = q.options(joinedload(UserEmailMap.user))
807 q = q.options(joinedload(UserEmailMap.user))
808 if cache:
808 if cache:
809 q = q.options(
809 q = q.options(
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
811 ret = getattr(q.scalar(), 'user', None)
811 ret = getattr(q.scalar(), 'user', None)
812
812
813 return ret
813 return ret
814
814
815 @classmethod
815 @classmethod
816 def get_from_cs_author(cls, author):
816 def get_from_cs_author(cls, author):
817 """
817 """
818 Tries to get User objects out of commit author string
818 Tries to get User objects out of commit author string
819
819
820 :param author:
820 :param author:
821 """
821 """
822 from rhodecode.lib.helpers import email, author_name
822 from rhodecode.lib.helpers import email, author_name
823 # Valid email in the attribute passed, see if they're in the system
823 # Valid email in the attribute passed, see if they're in the system
824 _email = email(author)
824 _email = email(author)
825 if _email:
825 if _email:
826 user = cls.get_by_email(_email, case_insensitive=True)
826 user = cls.get_by_email(_email, case_insensitive=True)
827 if user:
827 if user:
828 return user
828 return user
829 # Maybe we can match by username?
829 # Maybe we can match by username?
830 _author = author_name(author)
830 _author = author_name(author)
831 user = cls.get_by_username(_author, case_insensitive=True)
831 user = cls.get_by_username(_author, case_insensitive=True)
832 if user:
832 if user:
833 return user
833 return user
834
834
835 def update_userdata(self, **kwargs):
835 def update_userdata(self, **kwargs):
836 usr = self
836 usr = self
837 old = usr.user_data
837 old = usr.user_data
838 old.update(**kwargs)
838 old.update(**kwargs)
839 usr.user_data = old
839 usr.user_data = old
840 Session().add(usr)
840 Session().add(usr)
841 log.debug('updated userdata with ', kwargs)
841 log.debug('updated userdata with ', kwargs)
842
842
843 def update_lastlogin(self):
843 def update_lastlogin(self):
844 """Update user lastlogin"""
844 """Update user lastlogin"""
845 self.last_login = datetime.datetime.now()
845 self.last_login = datetime.datetime.now()
846 Session().add(self)
846 Session().add(self)
847 log.debug('updated user %s lastlogin', self.username)
847 log.debug('updated user %s lastlogin', self.username)
848
848
849 def update_lastactivity(self):
849 def update_lastactivity(self):
850 """Update user lastactivity"""
850 """Update user lastactivity"""
851 self.last_activity = datetime.datetime.now()
851 self.last_activity = datetime.datetime.now()
852 Session().add(self)
852 Session().add(self)
853 log.debug('updated user %s lastactivity', self.username)
853 log.debug('updated user %s lastactivity', self.username)
854
854
855 def update_password(self, new_password):
855 def update_password(self, new_password):
856 from rhodecode.lib.auth import get_crypt_password
856 from rhodecode.lib.auth import get_crypt_password
857
857
858 self.password = get_crypt_password(new_password)
858 self.password = get_crypt_password(new_password)
859 Session().add(self)
859 Session().add(self)
860
860
861 @classmethod
861 @classmethod
862 def get_first_super_admin(cls):
862 def get_first_super_admin(cls):
863 user = User.query().filter(User.admin == true()).first()
863 user = User.query().filter(User.admin == true()).first()
864 if user is None:
864 if user is None:
865 raise Exception('FATAL: Missing administrative account!')
865 raise Exception('FATAL: Missing administrative account!')
866 return user
866 return user
867
867
868 @classmethod
868 @classmethod
869 def get_all_super_admins(cls):
869 def get_all_super_admins(cls):
870 """
870 """
871 Returns all admin accounts sorted by username
871 Returns all admin accounts sorted by username
872 """
872 """
873 return User.query().filter(User.admin == true())\
873 return User.query().filter(User.admin == true())\
874 .order_by(User.username.asc()).all()
874 .order_by(User.username.asc()).all()
875
875
876 @classmethod
876 @classmethod
877 def get_default_user(cls, cache=False, refresh=False):
877 def get_default_user(cls, cache=False, refresh=False):
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
879 if user is None:
879 if user is None:
880 raise Exception('FATAL: Missing default account!')
880 raise Exception('FATAL: Missing default account!')
881 if refresh:
881 if refresh:
882 # The default user might be based on outdated state which
882 # The default user might be based on outdated state which
883 # has been loaded from the cache.
883 # has been loaded from the cache.
884 # A call to refresh() ensures that the
884 # A call to refresh() ensures that the
885 # latest state from the database is used.
885 # latest state from the database is used.
886 Session().refresh(user)
886 Session().refresh(user)
887 return user
887 return user
888
888
889 def _get_default_perms(self, user, suffix=''):
889 def _get_default_perms(self, user, suffix=''):
890 from rhodecode.model.permission import PermissionModel
890 from rhodecode.model.permission import PermissionModel
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
892
892
893 def get_default_perms(self, suffix=''):
893 def get_default_perms(self, suffix=''):
894 return self._get_default_perms(self, suffix)
894 return self._get_default_perms(self, suffix)
895
895
896 def get_api_data(self, include_secrets=False, details='full'):
896 def get_api_data(self, include_secrets=False, details='full'):
897 """
897 """
898 Common function for generating user related data for API
898 Common function for generating user related data for API
899
899
900 :param include_secrets: By default secrets in the API data will be replaced
900 :param include_secrets: By default secrets in the API data will be replaced
901 by a placeholder value to prevent exposing this data by accident. In case
901 by a placeholder value to prevent exposing this data by accident. In case
902 this data shall be exposed, set this flag to ``True``.
902 this data shall be exposed, set this flag to ``True``.
903
903
904 :param details: details can be 'basic|full' basic gives only a subset of
904 :param details: details can be 'basic|full' basic gives only a subset of
905 the available user information that includes user_id, name and emails.
905 the available user information that includes user_id, name and emails.
906 """
906 """
907 user = self
907 user = self
908 user_data = self.user_data
908 user_data = self.user_data
909 data = {
909 data = {
910 'user_id': user.user_id,
910 'user_id': user.user_id,
911 'username': user.username,
911 'username': user.username,
912 'firstname': user.name,
912 'firstname': user.name,
913 'lastname': user.lastname,
913 'lastname': user.lastname,
914 'email': user.email,
914 'email': user.email,
915 'emails': user.emails,
915 'emails': user.emails,
916 }
916 }
917 if details == 'basic':
917 if details == 'basic':
918 return data
918 return data
919
919
920 api_key_length = 40
920 api_key_length = 40
921 api_key_replacement = '*' * api_key_length
921 api_key_replacement = '*' * api_key_length
922
922
923 extras = {
923 extras = {
924 'api_keys': [api_key_replacement],
924 'api_keys': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
926 'active': user.active,
926 'active': user.active,
927 'admin': user.admin,
927 'admin': user.admin,
928 'extern_type': user.extern_type,
928 'extern_type': user.extern_type,
929 'extern_name': user.extern_name,
929 'extern_name': user.extern_name,
930 'last_login': user.last_login,
930 'last_login': user.last_login,
931 'last_activity': user.last_activity,
931 'last_activity': user.last_activity,
932 'ip_addresses': user.ip_addresses,
932 'ip_addresses': user.ip_addresses,
933 'language': user_data.get('language')
933 'language': user_data.get('language')
934 }
934 }
935 data.update(extras)
935 data.update(extras)
936
936
937 if include_secrets:
937 if include_secrets:
938 data['api_keys'] = user.auth_tokens
938 data['api_keys'] = user.auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
940 return data
940 return data
941
941
942 def __json__(self):
942 def __json__(self):
943 data = {
943 data = {
944 'full_name': self.full_name,
944 'full_name': self.full_name,
945 'full_name_or_username': self.full_name_or_username,
945 'full_name_or_username': self.full_name_or_username,
946 'short_contact': self.short_contact,
946 'short_contact': self.short_contact,
947 'full_contact': self.full_contact,
947 'full_contact': self.full_contact,
948 }
948 }
949 data.update(self.get_api_data())
949 data.update(self.get_api_data())
950 return data
950 return data
951
951
952
952
953 class UserApiKeys(Base, BaseModel):
953 class UserApiKeys(Base, BaseModel):
954 __tablename__ = 'user_api_keys'
954 __tablename__ = 'user_api_keys'
955 __table_args__ = (
955 __table_args__ = (
956 Index('uak_api_key_idx', 'api_key'),
956 Index('uak_api_key_idx', 'api_key'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
958 UniqueConstraint('api_key'),
958 UniqueConstraint('api_key'),
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
961 )
961 )
962 __mapper_args__ = {}
962 __mapper_args__ = {}
963
963
964 # ApiKey role
964 # ApiKey role
965 ROLE_ALL = 'token_role_all'
965 ROLE_ALL = 'token_role_all'
966 ROLE_HTTP = 'token_role_http'
966 ROLE_HTTP = 'token_role_http'
967 ROLE_VCS = 'token_role_vcs'
967 ROLE_VCS = 'token_role_vcs'
968 ROLE_API = 'token_role_api'
968 ROLE_API = 'token_role_api'
969 ROLE_FEED = 'token_role_feed'
969 ROLE_FEED = 'token_role_feed'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
971
971
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
973
973
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
978 expires = Column('expires', Float(53), nullable=False)
978 expires = Column('expires', Float(53), nullable=False)
979 role = Column('role', String(255), nullable=True)
979 role = Column('role', String(255), nullable=True)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981
981
982 # scope columns
982 # scope columns
983 repo_id = Column(
983 repo_id = Column(
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
985 nullable=True, unique=None, default=None)
985 nullable=True, unique=None, default=None)
986 repo = relationship('Repository', lazy='joined')
986 repo = relationship('Repository', lazy='joined')
987
987
988 repo_group_id = Column(
988 repo_group_id = Column(
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
990 nullable=True, unique=None, default=None)
990 nullable=True, unique=None, default=None)
991 repo_group = relationship('RepoGroup', lazy='joined')
991 repo_group = relationship('RepoGroup', lazy='joined')
992
992
993 user = relationship('User', lazy='joined')
993 user = relationship('User', lazy='joined')
994
994
995 def __unicode__(self):
995 def __unicode__(self):
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
997
997
998 def __json__(self):
998 def __json__(self):
999 data = {
999 data = {
1000 'auth_token': self.api_key,
1000 'auth_token': self.api_key,
1001 'role': self.role,
1001 'role': self.role,
1002 'scope': self.scope_humanized,
1002 'scope': self.scope_humanized,
1003 'expired': self.expired
1003 'expired': self.expired
1004 }
1004 }
1005 return data
1005 return data
1006
1006
1007 @property
1007 @property
1008 def expired(self):
1008 def expired(self):
1009 if self.expires == -1:
1009 if self.expires == -1:
1010 return False
1010 return False
1011 return time.time() > self.expires
1011 return time.time() > self.expires
1012
1012
1013 @classmethod
1013 @classmethod
1014 def _get_role_name(cls, role):
1014 def _get_role_name(cls, role):
1015 return {
1015 return {
1016 cls.ROLE_ALL: _('all'),
1016 cls.ROLE_ALL: _('all'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1019 cls.ROLE_API: _('api calls'),
1019 cls.ROLE_API: _('api calls'),
1020 cls.ROLE_FEED: _('feed access'),
1020 cls.ROLE_FEED: _('feed access'),
1021 }.get(role, role)
1021 }.get(role, role)
1022
1022
1023 @property
1023 @property
1024 def role_humanized(self):
1024 def role_humanized(self):
1025 return self._get_role_name(self.role)
1025 return self._get_role_name(self.role)
1026
1026
1027 def _get_scope(self):
1027 def _get_scope(self):
1028 if self.repo:
1028 if self.repo:
1029 return repr(self.repo)
1029 return repr(self.repo)
1030 if self.repo_group:
1030 if self.repo_group:
1031 return repr(self.repo_group) + ' (recursive)'
1031 return repr(self.repo_group) + ' (recursive)'
1032 return 'global'
1032 return 'global'
1033
1033
1034 @property
1034 @property
1035 def scope_humanized(self):
1035 def scope_humanized(self):
1036 return self._get_scope()
1036 return self._get_scope()
1037
1037
1038
1038
1039 class UserEmailMap(Base, BaseModel):
1039 class UserEmailMap(Base, BaseModel):
1040 __tablename__ = 'user_email_map'
1040 __tablename__ = 'user_email_map'
1041 __table_args__ = (
1041 __table_args__ = (
1042 Index('uem_email_idx', 'email'),
1042 Index('uem_email_idx', 'email'),
1043 UniqueConstraint('email'),
1043 UniqueConstraint('email'),
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1046 )
1046 )
1047 __mapper_args__ = {}
1047 __mapper_args__ = {}
1048
1048
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1052 user = relationship('User', lazy='joined')
1052 user = relationship('User', lazy='joined')
1053
1053
1054 @validates('_email')
1054 @validates('_email')
1055 def validate_email(self, key, email):
1055 def validate_email(self, key, email):
1056 # check if this email is not main one
1056 # check if this email is not main one
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1058 if main_email is not None:
1058 if main_email is not None:
1059 raise AttributeError('email %s is present is user table' % email)
1059 raise AttributeError('email %s is present is user table' % email)
1060 return email
1060 return email
1061
1061
1062 @hybrid_property
1062 @hybrid_property
1063 def email(self):
1063 def email(self):
1064 return self._email
1064 return self._email
1065
1065
1066 @email.setter
1066 @email.setter
1067 def email(self, val):
1067 def email(self, val):
1068 self._email = val.lower() if val else None
1068 self._email = val.lower() if val else None
1069
1069
1070
1070
1071 class UserIpMap(Base, BaseModel):
1071 class UserIpMap(Base, BaseModel):
1072 __tablename__ = 'user_ip_map'
1072 __tablename__ = 'user_ip_map'
1073 __table_args__ = (
1073 __table_args__ = (
1074 UniqueConstraint('user_id', 'ip_addr'),
1074 UniqueConstraint('user_id', 'ip_addr'),
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 )
1077 )
1078 __mapper_args__ = {}
1078 __mapper_args__ = {}
1079
1079
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1085 user = relationship('User', lazy='joined')
1085 user = relationship('User', lazy='joined')
1086
1086
1087 @classmethod
1087 @classmethod
1088 def _get_ip_range(cls, ip_addr):
1088 def _get_ip_range(cls, ip_addr):
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1090 return [str(net.network_address), str(net.broadcast_address)]
1090 return [str(net.network_address), str(net.broadcast_address)]
1091
1091
1092 def __json__(self):
1092 def __json__(self):
1093 return {
1093 return {
1094 'ip_addr': self.ip_addr,
1094 'ip_addr': self.ip_addr,
1095 'ip_range': self._get_ip_range(self.ip_addr),
1095 'ip_range': self._get_ip_range(self.ip_addr),
1096 }
1096 }
1097
1097
1098 def __unicode__(self):
1098 def __unicode__(self):
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1100 self.user_id, self.ip_addr)
1100 self.user_id, self.ip_addr)
1101
1101
1102
1102
1103 class UserLog(Base, BaseModel):
1103 class UserLog(Base, BaseModel):
1104 __tablename__ = 'user_logs'
1104 __tablename__ = 'user_logs'
1105 __table_args__ = (
1105 __table_args__ = (
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1108 )
1108 )
1109 VERSION_1 = 'v1'
1109 VERSION_1 = 'v1'
1110 VERSION_2 = 'v2'
1110 VERSION_2 = 'v2'
1111 VERSIONS = [VERSION_1, VERSION_2]
1111 VERSIONS = [VERSION_1, VERSION_2]
1112
1112
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1121
1121
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1125
1125
1126 def __unicode__(self):
1126 def __unicode__(self):
1127 return u"<%s('id:%s:%s')>" % (
1127 return u"<%s('id:%s:%s')>" % (
1128 self.__class__.__name__, self.repository_name, self.action)
1128 self.__class__.__name__, self.repository_name, self.action)
1129
1129
1130 def __json__(self):
1130 def __json__(self):
1131 return {
1131 return {
1132 'user_id': self.user_id,
1132 'user_id': self.user_id,
1133 'username': self.username,
1133 'username': self.username,
1134 'repository_id': self.repository_id,
1134 'repository_id': self.repository_id,
1135 'repository_name': self.repository_name,
1135 'repository_name': self.repository_name,
1136 'user_ip': self.user_ip,
1136 'user_ip': self.user_ip,
1137 'action_date': self.action_date,
1137 'action_date': self.action_date,
1138 'action': self.action,
1138 'action': self.action,
1139 }
1139 }
1140
1140
1141 @property
1141 @property
1142 def action_as_day(self):
1142 def action_as_day(self):
1143 return datetime.date(*self.action_date.timetuple()[:3])
1143 return datetime.date(*self.action_date.timetuple()[:3])
1144
1144
1145 user = relationship('User')
1145 user = relationship('User')
1146 repository = relationship('Repository', cascade='')
1146 repository = relationship('Repository', cascade='')
1147
1147
1148
1148
1149 class UserGroup(Base, BaseModel):
1149 class UserGroup(Base, BaseModel):
1150 __tablename__ = 'users_groups'
1150 __tablename__ = 'users_groups'
1151 __table_args__ = (
1151 __table_args__ = (
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1154 )
1154 )
1155
1155
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1164
1164
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1171
1171
1172 user = relationship('User')
1172 user = relationship('User')
1173
1173
1174 @hybrid_property
1174 @hybrid_property
1175 def group_data(self):
1175 def group_data(self):
1176 if not self._group_data:
1176 if not self._group_data:
1177 return {}
1177 return {}
1178
1178
1179 try:
1179 try:
1180 return json.loads(self._group_data)
1180 return json.loads(self._group_data)
1181 except TypeError:
1181 except TypeError:
1182 return {}
1182 return {}
1183
1183
1184 @group_data.setter
1184 @group_data.setter
1185 def group_data(self, val):
1185 def group_data(self, val):
1186 try:
1186 try:
1187 self._group_data = json.dumps(val)
1187 self._group_data = json.dumps(val)
1188 except Exception:
1188 except Exception:
1189 log.error(traceback.format_exc())
1189 log.error(traceback.format_exc())
1190
1190
1191 def __unicode__(self):
1191 def __unicode__(self):
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1193 self.users_group_id,
1193 self.users_group_id,
1194 self.users_group_name)
1194 self.users_group_name)
1195
1195
1196 @classmethod
1196 @classmethod
1197 def get_by_group_name(cls, group_name, cache=False,
1197 def get_by_group_name(cls, group_name, cache=False,
1198 case_insensitive=False):
1198 case_insensitive=False):
1199 if case_insensitive:
1199 if case_insensitive:
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1201 func.lower(group_name))
1201 func.lower(group_name))
1202
1202
1203 else:
1203 else:
1204 q = cls.query().filter(cls.users_group_name == group_name)
1204 q = cls.query().filter(cls.users_group_name == group_name)
1205 if cache:
1205 if cache:
1206 q = q.options(
1206 q = q.options(
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1208 return q.scalar()
1208 return q.scalar()
1209
1209
1210 @classmethod
1210 @classmethod
1211 def get(cls, user_group_id, cache=False):
1211 def get(cls, user_group_id, cache=False):
1212 user_group = cls.query()
1212 user_group = cls.query()
1213 if cache:
1213 if cache:
1214 user_group = user_group.options(
1214 user_group = user_group.options(
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1216 return user_group.get(user_group_id)
1216 return user_group.get(user_group_id)
1217
1217
1218 def permissions(self, with_admins=True, with_owner=True):
1218 def permissions(self, with_admins=True, with_owner=True):
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1221 joinedload(UserUserGroupToPerm.user),
1221 joinedload(UserUserGroupToPerm.user),
1222 joinedload(UserUserGroupToPerm.permission),)
1222 joinedload(UserUserGroupToPerm.permission),)
1223
1223
1224 # get owners and admins and permissions. We do a trick of re-writing
1224 # get owners and admins and permissions. We do a trick of re-writing
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1226 # has a global reference and changing one object propagates to all
1226 # has a global reference and changing one object propagates to all
1227 # others. This means if admin is also an owner admin_row that change
1227 # others. This means if admin is also an owner admin_row that change
1228 # would propagate to both objects
1228 # would propagate to both objects
1229 perm_rows = []
1229 perm_rows = []
1230 for _usr in q.all():
1230 for _usr in q.all():
1231 usr = AttributeDict(_usr.user.get_dict())
1231 usr = AttributeDict(_usr.user.get_dict())
1232 usr.permission = _usr.permission.permission_name
1232 usr.permission = _usr.permission.permission_name
1233 perm_rows.append(usr)
1233 perm_rows.append(usr)
1234
1234
1235 # filter the perm rows by 'default' first and then sort them by
1235 # filter the perm rows by 'default' first and then sort them by
1236 # admin,write,read,none permissions sorted again alphabetically in
1236 # admin,write,read,none permissions sorted again alphabetically in
1237 # each group
1237 # each group
1238 perm_rows = sorted(perm_rows, key=display_sort)
1238 perm_rows = sorted(perm_rows, key=display_sort)
1239
1239
1240 _admin_perm = 'usergroup.admin'
1240 _admin_perm = 'usergroup.admin'
1241 owner_row = []
1241 owner_row = []
1242 if with_owner:
1242 if with_owner:
1243 usr = AttributeDict(self.user.get_dict())
1243 usr = AttributeDict(self.user.get_dict())
1244 usr.owner_row = True
1244 usr.owner_row = True
1245 usr.permission = _admin_perm
1245 usr.permission = _admin_perm
1246 owner_row.append(usr)
1246 owner_row.append(usr)
1247
1247
1248 super_admin_rows = []
1248 super_admin_rows = []
1249 if with_admins:
1249 if with_admins:
1250 for usr in User.get_all_super_admins():
1250 for usr in User.get_all_super_admins():
1251 # if this admin is also owner, don't double the record
1251 # if this admin is also owner, don't double the record
1252 if usr.user_id == owner_row[0].user_id:
1252 if usr.user_id == owner_row[0].user_id:
1253 owner_row[0].admin_row = True
1253 owner_row[0].admin_row = True
1254 else:
1254 else:
1255 usr = AttributeDict(usr.get_dict())
1255 usr = AttributeDict(usr.get_dict())
1256 usr.admin_row = True
1256 usr.admin_row = True
1257 usr.permission = _admin_perm
1257 usr.permission = _admin_perm
1258 super_admin_rows.append(usr)
1258 super_admin_rows.append(usr)
1259
1259
1260 return super_admin_rows + owner_row + perm_rows
1260 return super_admin_rows + owner_row + perm_rows
1261
1261
1262 def permission_user_groups(self):
1262 def permission_user_groups(self):
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1267
1267
1268 perm_rows = []
1268 perm_rows = []
1269 for _user_group in q.all():
1269 for _user_group in q.all():
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1271 usr.permission = _user_group.permission.permission_name
1271 usr.permission = _user_group.permission.permission_name
1272 perm_rows.append(usr)
1272 perm_rows.append(usr)
1273
1273
1274 return perm_rows
1274 return perm_rows
1275
1275
1276 def _get_default_perms(self, user_group, suffix=''):
1276 def _get_default_perms(self, user_group, suffix=''):
1277 from rhodecode.model.permission import PermissionModel
1277 from rhodecode.model.permission import PermissionModel
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1279
1279
1280 def get_default_perms(self, suffix=''):
1280 def get_default_perms(self, suffix=''):
1281 return self._get_default_perms(self, suffix)
1281 return self._get_default_perms(self, suffix)
1282
1282
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1284 """
1284 """
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1286 basically forwarded.
1286 basically forwarded.
1287
1287
1288 """
1288 """
1289 user_group = self
1289 user_group = self
1290 data = {
1290 data = {
1291 'users_group_id': user_group.users_group_id,
1291 'users_group_id': user_group.users_group_id,
1292 'group_name': user_group.users_group_name,
1292 'group_name': user_group.users_group_name,
1293 'group_description': user_group.user_group_description,
1293 'group_description': user_group.user_group_description,
1294 'active': user_group.users_group_active,
1294 'active': user_group.users_group_active,
1295 'owner': user_group.user.username,
1295 'owner': user_group.user.username,
1296 'owner_email': user_group.user.email,
1296 'owner_email': user_group.user.email,
1297 }
1297 }
1298
1298
1299 if with_group_members:
1299 if with_group_members:
1300 users = []
1300 users = []
1301 for user in user_group.members:
1301 for user in user_group.members:
1302 user = user.user
1302 user = user.user
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1304 data['users'] = users
1304 data['users'] = users
1305
1305
1306 return data
1306 return data
1307
1307
1308
1308
1309 class UserGroupMember(Base, BaseModel):
1309 class UserGroupMember(Base, BaseModel):
1310 __tablename__ = 'users_groups_members'
1310 __tablename__ = 'users_groups_members'
1311 __table_args__ = (
1311 __table_args__ = (
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1314 )
1314 )
1315
1315
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1319
1319
1320 user = relationship('User', lazy='joined')
1320 user = relationship('User', lazy='joined')
1321 users_group = relationship('UserGroup')
1321 users_group = relationship('UserGroup')
1322
1322
1323 def __init__(self, gr_id='', u_id=''):
1323 def __init__(self, gr_id='', u_id=''):
1324 self.users_group_id = gr_id
1324 self.users_group_id = gr_id
1325 self.user_id = u_id
1325 self.user_id = u_id
1326
1326
1327
1327
1328 class RepositoryField(Base, BaseModel):
1328 class RepositoryField(Base, BaseModel):
1329 __tablename__ = 'repositories_fields'
1329 __tablename__ = 'repositories_fields'
1330 __table_args__ = (
1330 __table_args__ = (
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1334 )
1334 )
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1336
1336
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1339 field_key = Column("field_key", String(250))
1339 field_key = Column("field_key", String(250))
1340 field_label = Column("field_label", String(1024), nullable=False)
1340 field_label = Column("field_label", String(1024), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1345
1345
1346 repository = relationship('Repository')
1346 repository = relationship('Repository')
1347
1347
1348 @property
1348 @property
1349 def field_key_prefixed(self):
1349 def field_key_prefixed(self):
1350 return 'ex_%s' % self.field_key
1350 return 'ex_%s' % self.field_key
1351
1351
1352 @classmethod
1352 @classmethod
1353 def un_prefix_key(cls, key):
1353 def un_prefix_key(cls, key):
1354 if key.startswith(cls.PREFIX):
1354 if key.startswith(cls.PREFIX):
1355 return key[len(cls.PREFIX):]
1355 return key[len(cls.PREFIX):]
1356 return key
1356 return key
1357
1357
1358 @classmethod
1358 @classmethod
1359 def get_by_key_name(cls, key, repo):
1359 def get_by_key_name(cls, key, repo):
1360 row = cls.query()\
1360 row = cls.query()\
1361 .filter(cls.repository == repo)\
1361 .filter(cls.repository == repo)\
1362 .filter(cls.field_key == key).scalar()
1362 .filter(cls.field_key == key).scalar()
1363 return row
1363 return row
1364
1364
1365
1365
1366 class Repository(Base, BaseModel):
1366 class Repository(Base, BaseModel):
1367 __tablename__ = 'repositories'
1367 __tablename__ = 'repositories'
1368 __table_args__ = (
1368 __table_args__ = (
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1372 )
1372 )
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1375
1375
1376 STATE_CREATED = 'repo_state_created'
1376 STATE_CREATED = 'repo_state_created'
1377 STATE_PENDING = 'repo_state_pending'
1377 STATE_PENDING = 'repo_state_pending'
1378 STATE_ERROR = 'repo_state_error'
1378 STATE_ERROR = 'repo_state_error'
1379
1379
1380 LOCK_AUTOMATIC = 'lock_auto'
1380 LOCK_AUTOMATIC = 'lock_auto'
1381 LOCK_API = 'lock_api'
1381 LOCK_API = 'lock_api'
1382 LOCK_WEB = 'lock_web'
1382 LOCK_WEB = 'lock_web'
1383 LOCK_PULL = 'lock_pull'
1383 LOCK_PULL = 'lock_pull'
1384
1384
1385 NAME_SEP = URL_SEP
1385 NAME_SEP = URL_SEP
1386
1386
1387 repo_id = Column(
1387 repo_id = Column(
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1389 primary_key=True)
1389 primary_key=True)
1390 _repo_name = Column(
1390 _repo_name = Column(
1391 "repo_name", Text(), nullable=False, default=None)
1391 "repo_name", Text(), nullable=False, default=None)
1392 _repo_name_hash = Column(
1392 _repo_name_hash = Column(
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1395
1395
1396 clone_uri = Column(
1396 clone_uri = Column(
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1398 default=None)
1398 default=None)
1399 repo_type = Column(
1399 repo_type = Column(
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1401 user_id = Column(
1401 user_id = Column(
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1403 unique=False, default=None)
1403 unique=False, default=None)
1404 private = Column(
1404 private = Column(
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1406 enable_statistics = Column(
1406 enable_statistics = Column(
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1408 enable_downloads = Column(
1408 enable_downloads = Column(
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1410 description = Column(
1410 description = Column(
1411 "description", String(10000), nullable=True, unique=None, default=None)
1411 "description", String(10000), nullable=True, unique=None, default=None)
1412 created_on = Column(
1412 created_on = Column(
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1414 default=datetime.datetime.now)
1414 default=datetime.datetime.now)
1415 updated_on = Column(
1415 updated_on = Column(
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1417 default=datetime.datetime.now)
1417 default=datetime.datetime.now)
1418 _landing_revision = Column(
1418 _landing_revision = Column(
1419 "landing_revision", String(255), nullable=False, unique=False,
1419 "landing_revision", String(255), nullable=False, unique=False,
1420 default=None)
1420 default=None)
1421 enable_locking = Column(
1421 enable_locking = Column(
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1423 default=False)
1423 default=False)
1424 _locked = Column(
1424 _locked = Column(
1425 "locked", String(255), nullable=True, unique=False, default=None)
1425 "locked", String(255), nullable=True, unique=False, default=None)
1426 _changeset_cache = Column(
1426 _changeset_cache = Column(
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1428
1428
1429 fork_id = Column(
1429 fork_id = Column(
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1431 nullable=True, unique=False, default=None)
1431 nullable=True, unique=False, default=None)
1432 group_id = Column(
1432 group_id = Column(
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1434 unique=False, default=None)
1434 unique=False, default=None)
1435
1435
1436 user = relationship('User', lazy='joined')
1436 user = relationship('User', lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1439 repo_to_perm = relationship(
1439 repo_to_perm = relationship(
1440 'UserRepoToPerm', cascade='all',
1440 'UserRepoToPerm', cascade='all',
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1444
1444
1445 followers = relationship(
1445 followers = relationship(
1446 'UserFollowing',
1446 'UserFollowing',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1448 cascade='all')
1448 cascade='all')
1449 extra_fields = relationship(
1449 extra_fields = relationship(
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1451 logs = relationship('UserLog')
1451 logs = relationship('UserLog')
1452 comments = relationship(
1452 comments = relationship(
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1454 pull_requests_source = relationship(
1454 pull_requests_source = relationship(
1455 'PullRequest',
1455 'PullRequest',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1457 cascade="all, delete, delete-orphan")
1457 cascade="all, delete, delete-orphan")
1458 pull_requests_target = relationship(
1458 pull_requests_target = relationship(
1459 'PullRequest',
1459 'PullRequest',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1461 cascade="all, delete, delete-orphan")
1461 cascade="all, delete, delete-orphan")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1464 integrations = relationship('Integration',
1464 integrations = relationship('Integration',
1465 cascade="all, delete, delete-orphan")
1465 cascade="all, delete, delete-orphan")
1466
1466
1467 def __unicode__(self):
1467 def __unicode__(self):
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1469 safe_unicode(self.repo_name))
1469 safe_unicode(self.repo_name))
1470
1470
1471 @hybrid_property
1471 @hybrid_property
1472 def landing_rev(self):
1472 def landing_rev(self):
1473 # always should return [rev_type, rev]
1473 # always should return [rev_type, rev]
1474 if self._landing_revision:
1474 if self._landing_revision:
1475 _rev_info = self._landing_revision.split(':')
1475 _rev_info = self._landing_revision.split(':')
1476 if len(_rev_info) < 2:
1476 if len(_rev_info) < 2:
1477 _rev_info.insert(0, 'rev')
1477 _rev_info.insert(0, 'rev')
1478 return [_rev_info[0], _rev_info[1]]
1478 return [_rev_info[0], _rev_info[1]]
1479 return [None, None]
1479 return [None, None]
1480
1480
1481 @landing_rev.setter
1481 @landing_rev.setter
1482 def landing_rev(self, val):
1482 def landing_rev(self, val):
1483 if ':' not in val:
1483 if ':' not in val:
1484 raise ValueError('value must be delimited with `:` and consist '
1484 raise ValueError('value must be delimited with `:` and consist '
1485 'of <rev_type>:<rev>, got %s instead' % val)
1485 'of <rev_type>:<rev>, got %s instead' % val)
1486 self._landing_revision = val
1486 self._landing_revision = val
1487
1487
1488 @hybrid_property
1488 @hybrid_property
1489 def locked(self):
1489 def locked(self):
1490 if self._locked:
1490 if self._locked:
1491 user_id, timelocked, reason = self._locked.split(':')
1491 user_id, timelocked, reason = self._locked.split(':')
1492 lock_values = int(user_id), timelocked, reason
1492 lock_values = int(user_id), timelocked, reason
1493 else:
1493 else:
1494 lock_values = [None, None, None]
1494 lock_values = [None, None, None]
1495 return lock_values
1495 return lock_values
1496
1496
1497 @locked.setter
1497 @locked.setter
1498 def locked(self, val):
1498 def locked(self, val):
1499 if val and isinstance(val, (list, tuple)):
1499 if val and isinstance(val, (list, tuple)):
1500 self._locked = ':'.join(map(str, val))
1500 self._locked = ':'.join(map(str, val))
1501 else:
1501 else:
1502 self._locked = None
1502 self._locked = None
1503
1503
1504 @hybrid_property
1504 @hybrid_property
1505 def changeset_cache(self):
1505 def changeset_cache(self):
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1507 dummy = EmptyCommit().__json__()
1507 dummy = EmptyCommit().__json__()
1508 if not self._changeset_cache:
1508 if not self._changeset_cache:
1509 return dummy
1509 return dummy
1510 try:
1510 try:
1511 return json.loads(self._changeset_cache)
1511 return json.loads(self._changeset_cache)
1512 except TypeError:
1512 except TypeError:
1513 return dummy
1513 return dummy
1514 except Exception:
1514 except Exception:
1515 log.error(traceback.format_exc())
1515 log.error(traceback.format_exc())
1516 return dummy
1516 return dummy
1517
1517
1518 @changeset_cache.setter
1518 @changeset_cache.setter
1519 def changeset_cache(self, val):
1519 def changeset_cache(self, val):
1520 try:
1520 try:
1521 self._changeset_cache = json.dumps(val)
1521 self._changeset_cache = json.dumps(val)
1522 except Exception:
1522 except Exception:
1523 log.error(traceback.format_exc())
1523 log.error(traceback.format_exc())
1524
1524
1525 @hybrid_property
1525 @hybrid_property
1526 def repo_name(self):
1526 def repo_name(self):
1527 return self._repo_name
1527 return self._repo_name
1528
1528
1529 @repo_name.setter
1529 @repo_name.setter
1530 def repo_name(self, value):
1530 def repo_name(self, value):
1531 self._repo_name = value
1531 self._repo_name = value
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1533
1533
1534 @classmethod
1534 @classmethod
1535 def normalize_repo_name(cls, repo_name):
1535 def normalize_repo_name(cls, repo_name):
1536 """
1536 """
1537 Normalizes os specific repo_name to the format internally stored inside
1537 Normalizes os specific repo_name to the format internally stored inside
1538 database using URL_SEP
1538 database using URL_SEP
1539
1539
1540 :param cls:
1540 :param cls:
1541 :param repo_name:
1541 :param repo_name:
1542 """
1542 """
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1544
1544
1545 @classmethod
1545 @classmethod
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1547 session = Session()
1547 session = Session()
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1549
1549
1550 if cache:
1550 if cache:
1551 if identity_cache:
1551 if identity_cache:
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1553 if val:
1553 if val:
1554 return val
1554 return val
1555 else:
1555 else:
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1557 q = q.options(
1557 q = q.options(
1558 FromCache("sql_cache_short", cache_key))
1558 FromCache("sql_cache_short", cache_key))
1559
1559
1560 return q.scalar()
1560 return q.scalar()
1561
1561
1562 @classmethod
1562 @classmethod
1563 def get_by_full_path(cls, repo_full_path):
1563 def get_by_full_path(cls, repo_full_path):
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1565 repo_name = cls.normalize_repo_name(repo_name)
1565 repo_name = cls.normalize_repo_name(repo_name)
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1567
1567
1568 @classmethod
1568 @classmethod
1569 def get_repo_forks(cls, repo_id):
1569 def get_repo_forks(cls, repo_id):
1570 return cls.query().filter(Repository.fork_id == repo_id)
1570 return cls.query().filter(Repository.fork_id == repo_id)
1571
1571
1572 @classmethod
1572 @classmethod
1573 def base_path(cls):
1573 def base_path(cls):
1574 """
1574 """
1575 Returns base path when all repos are stored
1575 Returns base path when all repos are stored
1576
1576
1577 :param cls:
1577 :param cls:
1578 """
1578 """
1579 q = Session().query(RhodeCodeUi)\
1579 q = Session().query(RhodeCodeUi)\
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 return q.one().ui_value
1582 return q.one().ui_value
1583
1583
1584 @classmethod
1584 @classmethod
1585 def is_valid(cls, repo_name):
1585 def is_valid(cls, repo_name):
1586 """
1586 """
1587 returns True if given repo name is a valid filesystem repository
1587 returns True if given repo name is a valid filesystem repository
1588
1588
1589 :param cls:
1589 :param cls:
1590 :param repo_name:
1590 :param repo_name:
1591 """
1591 """
1592 from rhodecode.lib.utils import is_valid_repo
1592 from rhodecode.lib.utils import is_valid_repo
1593
1593
1594 return is_valid_repo(repo_name, cls.base_path())
1594 return is_valid_repo(repo_name, cls.base_path())
1595
1595
1596 @classmethod
1596 @classmethod
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1598 case_insensitive=True):
1598 case_insensitive=True):
1599 q = Repository.query()
1599 q = Repository.query()
1600
1600
1601 if not isinstance(user_id, Optional):
1601 if not isinstance(user_id, Optional):
1602 q = q.filter(Repository.user_id == user_id)
1602 q = q.filter(Repository.user_id == user_id)
1603
1603
1604 if not isinstance(group_id, Optional):
1604 if not isinstance(group_id, Optional):
1605 q = q.filter(Repository.group_id == group_id)
1605 q = q.filter(Repository.group_id == group_id)
1606
1606
1607 if case_insensitive:
1607 if case_insensitive:
1608 q = q.order_by(func.lower(Repository.repo_name))
1608 q = q.order_by(func.lower(Repository.repo_name))
1609 else:
1609 else:
1610 q = q.order_by(Repository.repo_name)
1610 q = q.order_by(Repository.repo_name)
1611 return q.all()
1611 return q.all()
1612
1612
1613 @property
1613 @property
1614 def forks(self):
1614 def forks(self):
1615 """
1615 """
1616 Return forks of this repo
1616 Return forks of this repo
1617 """
1617 """
1618 return Repository.get_repo_forks(self.repo_id)
1618 return Repository.get_repo_forks(self.repo_id)
1619
1619
1620 @property
1620 @property
1621 def parent(self):
1621 def parent(self):
1622 """
1622 """
1623 Returns fork parent
1623 Returns fork parent
1624 """
1624 """
1625 return self.fork
1625 return self.fork
1626
1626
1627 @property
1627 @property
1628 def just_name(self):
1628 def just_name(self):
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1630
1630
1631 @property
1631 @property
1632 def groups_with_parents(self):
1632 def groups_with_parents(self):
1633 groups = []
1633 groups = []
1634 if self.group is None:
1634 if self.group is None:
1635 return groups
1635 return groups
1636
1636
1637 cur_gr = self.group
1637 cur_gr = self.group
1638 groups.insert(0, cur_gr)
1638 groups.insert(0, cur_gr)
1639 while 1:
1639 while 1:
1640 gr = getattr(cur_gr, 'parent_group', None)
1640 gr = getattr(cur_gr, 'parent_group', None)
1641 cur_gr = cur_gr.parent_group
1641 cur_gr = cur_gr.parent_group
1642 if gr is None:
1642 if gr is None:
1643 break
1643 break
1644 groups.insert(0, gr)
1644 groups.insert(0, gr)
1645
1645
1646 return groups
1646 return groups
1647
1647
1648 @property
1648 @property
1649 def groups_and_repo(self):
1649 def groups_and_repo(self):
1650 return self.groups_with_parents, self
1650 return self.groups_with_parents, self
1651
1651
1652 @LazyProperty
1652 @LazyProperty
1653 def repo_path(self):
1653 def repo_path(self):
1654 """
1654 """
1655 Returns base full path for that repository means where it actually
1655 Returns base full path for that repository means where it actually
1656 exists on a filesystem
1656 exists on a filesystem
1657 """
1657 """
1658 q = Session().query(RhodeCodeUi).filter(
1658 q = Session().query(RhodeCodeUi).filter(
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1661 return q.one().ui_value
1661 return q.one().ui_value
1662
1662
1663 @property
1663 @property
1664 def repo_full_path(self):
1664 def repo_full_path(self):
1665 p = [self.repo_path]
1665 p = [self.repo_path]
1666 # we need to split the name by / since this is how we store the
1666 # we need to split the name by / since this is how we store the
1667 # names in the database, but that eventually needs to be converted
1667 # names in the database, but that eventually needs to be converted
1668 # into a valid system path
1668 # into a valid system path
1669 p += self.repo_name.split(self.NAME_SEP)
1669 p += self.repo_name.split(self.NAME_SEP)
1670 return os.path.join(*map(safe_unicode, p))
1670 return os.path.join(*map(safe_unicode, p))
1671
1671
1672 @property
1672 @property
1673 def cache_keys(self):
1673 def cache_keys(self):
1674 """
1674 """
1675 Returns associated cache keys for that repo
1675 Returns associated cache keys for that repo
1676 """
1676 """
1677 return CacheKey.query()\
1677 return CacheKey.query()\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1679 .order_by(CacheKey.cache_key)\
1679 .order_by(CacheKey.cache_key)\
1680 .all()
1680 .all()
1681
1681
1682 def get_new_name(self, repo_name):
1682 def get_new_name(self, repo_name):
1683 """
1683 """
1684 returns new full repository name based on assigned group and new new
1684 returns new full repository name based on assigned group and new new
1685
1685
1686 :param group_name:
1686 :param group_name:
1687 """
1687 """
1688 path_prefix = self.group.full_path_splitted if self.group else []
1688 path_prefix = self.group.full_path_splitted if self.group else []
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1690
1690
1691 @property
1691 @property
1692 def _config(self):
1692 def _config(self):
1693 """
1693 """
1694 Returns db based config object.
1694 Returns db based config object.
1695 """
1695 """
1696 from rhodecode.lib.utils import make_db_config
1696 from rhodecode.lib.utils import make_db_config
1697 return make_db_config(clear_session=False, repo=self)
1697 return make_db_config(clear_session=False, repo=self)
1698
1698
1699 def permissions(self, with_admins=True, with_owner=True):
1699 def permissions(self, with_admins=True, with_owner=True):
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1702 joinedload(UserRepoToPerm.user),
1702 joinedload(UserRepoToPerm.user),
1703 joinedload(UserRepoToPerm.permission),)
1703 joinedload(UserRepoToPerm.permission),)
1704
1704
1705 # get owners and admins and permissions. We do a trick of re-writing
1705 # get owners and admins and permissions. We do a trick of re-writing
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1707 # has a global reference and changing one object propagates to all
1707 # has a global reference and changing one object propagates to all
1708 # others. This means if admin is also an owner admin_row that change
1708 # others. This means if admin is also an owner admin_row that change
1709 # would propagate to both objects
1709 # would propagate to both objects
1710 perm_rows = []
1710 perm_rows = []
1711 for _usr in q.all():
1711 for _usr in q.all():
1712 usr = AttributeDict(_usr.user.get_dict())
1712 usr = AttributeDict(_usr.user.get_dict())
1713 usr.permission = _usr.permission.permission_name
1713 usr.permission = _usr.permission.permission_name
1714 perm_rows.append(usr)
1714 perm_rows.append(usr)
1715
1715
1716 # filter the perm rows by 'default' first and then sort them by
1716 # filter the perm rows by 'default' first and then sort them by
1717 # admin,write,read,none permissions sorted again alphabetically in
1717 # admin,write,read,none permissions sorted again alphabetically in
1718 # each group
1718 # each group
1719 perm_rows = sorted(perm_rows, key=display_sort)
1719 perm_rows = sorted(perm_rows, key=display_sort)
1720
1720
1721 _admin_perm = 'repository.admin'
1721 _admin_perm = 'repository.admin'
1722 owner_row = []
1722 owner_row = []
1723 if with_owner:
1723 if with_owner:
1724 usr = AttributeDict(self.user.get_dict())
1724 usr = AttributeDict(self.user.get_dict())
1725 usr.owner_row = True
1725 usr.owner_row = True
1726 usr.permission = _admin_perm
1726 usr.permission = _admin_perm
1727 owner_row.append(usr)
1727 owner_row.append(usr)
1728
1728
1729 super_admin_rows = []
1729 super_admin_rows = []
1730 if with_admins:
1730 if with_admins:
1731 for usr in User.get_all_super_admins():
1731 for usr in User.get_all_super_admins():
1732 # if this admin is also owner, don't double the record
1732 # if this admin is also owner, don't double the record
1733 if usr.user_id == owner_row[0].user_id:
1733 if usr.user_id == owner_row[0].user_id:
1734 owner_row[0].admin_row = True
1734 owner_row[0].admin_row = True
1735 else:
1735 else:
1736 usr = AttributeDict(usr.get_dict())
1736 usr = AttributeDict(usr.get_dict())
1737 usr.admin_row = True
1737 usr.admin_row = True
1738 usr.permission = _admin_perm
1738 usr.permission = _admin_perm
1739 super_admin_rows.append(usr)
1739 super_admin_rows.append(usr)
1740
1740
1741 return super_admin_rows + owner_row + perm_rows
1741 return super_admin_rows + owner_row + perm_rows
1742
1742
1743 def permission_user_groups(self):
1743 def permission_user_groups(self):
1744 q = UserGroupRepoToPerm.query().filter(
1744 q = UserGroupRepoToPerm.query().filter(
1745 UserGroupRepoToPerm.repository == self)
1745 UserGroupRepoToPerm.repository == self)
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1747 joinedload(UserGroupRepoToPerm.users_group),
1747 joinedload(UserGroupRepoToPerm.users_group),
1748 joinedload(UserGroupRepoToPerm.permission),)
1748 joinedload(UserGroupRepoToPerm.permission),)
1749
1749
1750 perm_rows = []
1750 perm_rows = []
1751 for _user_group in q.all():
1751 for _user_group in q.all():
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1753 usr.permission = _user_group.permission.permission_name
1753 usr.permission = _user_group.permission.permission_name
1754 perm_rows.append(usr)
1754 perm_rows.append(usr)
1755
1755
1756 return perm_rows
1756 return perm_rows
1757
1757
1758 def get_api_data(self, include_secrets=False):
1758 def get_api_data(self, include_secrets=False):
1759 """
1759 """
1760 Common function for generating repo api data
1760 Common function for generating repo api data
1761
1761
1762 :param include_secrets: See :meth:`User.get_api_data`.
1762 :param include_secrets: See :meth:`User.get_api_data`.
1763
1763
1764 """
1764 """
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1766 # move this methods on models level.
1766 # move this methods on models level.
1767 from rhodecode.model.settings import SettingsModel
1767 from rhodecode.model.settings import SettingsModel
1768
1768
1769 repo = self
1769 repo = self
1770 _user_id, _time, _reason = self.locked
1770 _user_id, _time, _reason = self.locked
1771
1771
1772 data = {
1772 data = {
1773 'repo_id': repo.repo_id,
1773 'repo_id': repo.repo_id,
1774 'repo_name': repo.repo_name,
1774 'repo_name': repo.repo_name,
1775 'repo_type': repo.repo_type,
1775 'repo_type': repo.repo_type,
1776 'clone_uri': repo.clone_uri or '',
1776 'clone_uri': repo.clone_uri or '',
1777 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1777 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1778 'private': repo.private,
1778 'private': repo.private,
1779 'created_on': repo.created_on,
1779 'created_on': repo.created_on,
1780 'description': repo.description,
1780 'description': repo.description,
1781 'landing_rev': repo.landing_rev,
1781 'landing_rev': repo.landing_rev,
1782 'owner': repo.user.username,
1782 'owner': repo.user.username,
1783 'fork_of': repo.fork.repo_name if repo.fork else None,
1783 'fork_of': repo.fork.repo_name if repo.fork else None,
1784 'enable_statistics': repo.enable_statistics,
1784 'enable_statistics': repo.enable_statistics,
1785 'enable_locking': repo.enable_locking,
1785 'enable_locking': repo.enable_locking,
1786 'enable_downloads': repo.enable_downloads,
1786 'enable_downloads': repo.enable_downloads,
1787 'last_changeset': repo.changeset_cache,
1787 'last_changeset': repo.changeset_cache,
1788 'locked_by': User.get(_user_id).get_api_data(
1788 'locked_by': User.get(_user_id).get_api_data(
1789 include_secrets=include_secrets) if _user_id else None,
1789 include_secrets=include_secrets) if _user_id else None,
1790 'locked_date': time_to_datetime(_time) if _time else None,
1790 'locked_date': time_to_datetime(_time) if _time else None,
1791 'lock_reason': _reason if _reason else None,
1791 'lock_reason': _reason if _reason else None,
1792 }
1792 }
1793
1793
1794 # TODO: mikhail: should be per-repo settings here
1794 # TODO: mikhail: should be per-repo settings here
1795 rc_config = SettingsModel().get_all_settings()
1795 rc_config = SettingsModel().get_all_settings()
1796 repository_fields = str2bool(
1796 repository_fields = str2bool(
1797 rc_config.get('rhodecode_repository_fields'))
1797 rc_config.get('rhodecode_repository_fields'))
1798 if repository_fields:
1798 if repository_fields:
1799 for f in self.extra_fields:
1799 for f in self.extra_fields:
1800 data[f.field_key_prefixed] = f.field_value
1800 data[f.field_key_prefixed] = f.field_value
1801
1801
1802 return data
1802 return data
1803
1803
1804 @classmethod
1804 @classmethod
1805 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1805 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1806 if not lock_time:
1806 if not lock_time:
1807 lock_time = time.time()
1807 lock_time = time.time()
1808 if not lock_reason:
1808 if not lock_reason:
1809 lock_reason = cls.LOCK_AUTOMATIC
1809 lock_reason = cls.LOCK_AUTOMATIC
1810 repo.locked = [user_id, lock_time, lock_reason]
1810 repo.locked = [user_id, lock_time, lock_reason]
1811 Session().add(repo)
1811 Session().add(repo)
1812 Session().commit()
1812 Session().commit()
1813
1813
1814 @classmethod
1814 @classmethod
1815 def unlock(cls, repo):
1815 def unlock(cls, repo):
1816 repo.locked = None
1816 repo.locked = None
1817 Session().add(repo)
1817 Session().add(repo)
1818 Session().commit()
1818 Session().commit()
1819
1819
1820 @classmethod
1820 @classmethod
1821 def getlock(cls, repo):
1821 def getlock(cls, repo):
1822 return repo.locked
1822 return repo.locked
1823
1823
1824 def is_user_lock(self, user_id):
1824 def is_user_lock(self, user_id):
1825 if self.lock[0]:
1825 if self.lock[0]:
1826 lock_user_id = safe_int(self.lock[0])
1826 lock_user_id = safe_int(self.lock[0])
1827 user_id = safe_int(user_id)
1827 user_id = safe_int(user_id)
1828 # both are ints, and they are equal
1828 # both are ints, and they are equal
1829 return all([lock_user_id, user_id]) and lock_user_id == user_id
1829 return all([lock_user_id, user_id]) and lock_user_id == user_id
1830
1830
1831 return False
1831 return False
1832
1832
1833 def get_locking_state(self, action, user_id, only_when_enabled=True):
1833 def get_locking_state(self, action, user_id, only_when_enabled=True):
1834 """
1834 """
1835 Checks locking on this repository, if locking is enabled and lock is
1835 Checks locking on this repository, if locking is enabled and lock is
1836 present returns a tuple of make_lock, locked, locked_by.
1836 present returns a tuple of make_lock, locked, locked_by.
1837 make_lock can have 3 states None (do nothing) True, make lock
1837 make_lock can have 3 states None (do nothing) True, make lock
1838 False release lock, This value is later propagated to hooks, which
1838 False release lock, This value is later propagated to hooks, which
1839 do the locking. Think about this as signals passed to hooks what to do.
1839 do the locking. Think about this as signals passed to hooks what to do.
1840
1840
1841 """
1841 """
1842 # TODO: johbo: This is part of the business logic and should be moved
1842 # TODO: johbo: This is part of the business logic and should be moved
1843 # into the RepositoryModel.
1843 # into the RepositoryModel.
1844
1844
1845 if action not in ('push', 'pull'):
1845 if action not in ('push', 'pull'):
1846 raise ValueError("Invalid action value: %s" % repr(action))
1846 raise ValueError("Invalid action value: %s" % repr(action))
1847
1847
1848 # defines if locked error should be thrown to user
1848 # defines if locked error should be thrown to user
1849 currently_locked = False
1849 currently_locked = False
1850 # defines if new lock should be made, tri-state
1850 # defines if new lock should be made, tri-state
1851 make_lock = None
1851 make_lock = None
1852 repo = self
1852 repo = self
1853 user = User.get(user_id)
1853 user = User.get(user_id)
1854
1854
1855 lock_info = repo.locked
1855 lock_info = repo.locked
1856
1856
1857 if repo and (repo.enable_locking or not only_when_enabled):
1857 if repo and (repo.enable_locking or not only_when_enabled):
1858 if action == 'push':
1858 if action == 'push':
1859 # check if it's already locked !, if it is compare users
1859 # check if it's already locked !, if it is compare users
1860 locked_by_user_id = lock_info[0]
1860 locked_by_user_id = lock_info[0]
1861 if user.user_id == locked_by_user_id:
1861 if user.user_id == locked_by_user_id:
1862 log.debug(
1862 log.debug(
1863 'Got `push` action from user %s, now unlocking', user)
1863 'Got `push` action from user %s, now unlocking', user)
1864 # unlock if we have push from user who locked
1864 # unlock if we have push from user who locked
1865 make_lock = False
1865 make_lock = False
1866 else:
1866 else:
1867 # we're not the same user who locked, ban with
1867 # we're not the same user who locked, ban with
1868 # code defined in settings (default is 423 HTTP Locked) !
1868 # code defined in settings (default is 423 HTTP Locked) !
1869 log.debug('Repo %s is currently locked by %s', repo, user)
1869 log.debug('Repo %s is currently locked by %s', repo, user)
1870 currently_locked = True
1870 currently_locked = True
1871 elif action == 'pull':
1871 elif action == 'pull':
1872 # [0] user [1] date
1872 # [0] user [1] date
1873 if lock_info[0] and lock_info[1]:
1873 if lock_info[0] and lock_info[1]:
1874 log.debug('Repo %s is currently locked by %s', repo, user)
1874 log.debug('Repo %s is currently locked by %s', repo, user)
1875 currently_locked = True
1875 currently_locked = True
1876 else:
1876 else:
1877 log.debug('Setting lock on repo %s by %s', repo, user)
1877 log.debug('Setting lock on repo %s by %s', repo, user)
1878 make_lock = True
1878 make_lock = True
1879
1879
1880 else:
1880 else:
1881 log.debug('Repository %s do not have locking enabled', repo)
1881 log.debug('Repository %s do not have locking enabled', repo)
1882
1882
1883 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1883 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1884 make_lock, currently_locked, lock_info)
1884 make_lock, currently_locked, lock_info)
1885
1885
1886 from rhodecode.lib.auth import HasRepoPermissionAny
1886 from rhodecode.lib.auth import HasRepoPermissionAny
1887 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1887 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1888 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1888 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1889 # if we don't have at least write permission we cannot make a lock
1889 # if we don't have at least write permission we cannot make a lock
1890 log.debug('lock state reset back to FALSE due to lack '
1890 log.debug('lock state reset back to FALSE due to lack '
1891 'of at least read permission')
1891 'of at least read permission')
1892 make_lock = False
1892 make_lock = False
1893
1893
1894 return make_lock, currently_locked, lock_info
1894 return make_lock, currently_locked, lock_info
1895
1895
1896 @property
1896 @property
1897 def last_db_change(self):
1897 def last_db_change(self):
1898 return self.updated_on
1898 return self.updated_on
1899
1899
1900 @property
1900 @property
1901 def clone_uri_hidden(self):
1901 def clone_uri_hidden(self):
1902 clone_uri = self.clone_uri
1902 clone_uri = self.clone_uri
1903 if clone_uri:
1903 if clone_uri:
1904 import urlobject
1904 import urlobject
1905 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1905 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1906 if url_obj.password:
1906 if url_obj.password:
1907 clone_uri = url_obj.with_password('*****')
1907 clone_uri = url_obj.with_password('*****')
1908 return clone_uri
1908 return clone_uri
1909
1909
1910 def clone_url(self, **override):
1910 def clone_url(self, **override):
1911 qualified_home_url = url('home', qualified=True)
1911 qualified_home_url = url('home', qualified=True)
1912
1912
1913 uri_tmpl = None
1913 uri_tmpl = None
1914 if 'with_id' in override:
1914 if 'with_id' in override:
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1916 del override['with_id']
1916 del override['with_id']
1917
1917
1918 if 'uri_tmpl' in override:
1918 if 'uri_tmpl' in override:
1919 uri_tmpl = override['uri_tmpl']
1919 uri_tmpl = override['uri_tmpl']
1920 del override['uri_tmpl']
1920 del override['uri_tmpl']
1921
1921
1922 # we didn't override our tmpl from **overrides
1922 # we didn't override our tmpl from **overrides
1923 if not uri_tmpl:
1923 if not uri_tmpl:
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1925 try:
1925 try:
1926 from pylons import tmpl_context as c
1926 from pylons import tmpl_context as c
1927 uri_tmpl = c.clone_uri_tmpl
1927 uri_tmpl = c.clone_uri_tmpl
1928 except Exception:
1928 except Exception:
1929 # in any case if we call this outside of request context,
1929 # in any case if we call this outside of request context,
1930 # ie, not having tmpl_context set up
1930 # ie, not having tmpl_context set up
1931 pass
1931 pass
1932
1932
1933 return get_clone_url(uri_tmpl=uri_tmpl,
1933 return get_clone_url(uri_tmpl=uri_tmpl,
1934 qualifed_home_url=qualified_home_url,
1934 qualifed_home_url=qualified_home_url,
1935 repo_name=self.repo_name,
1935 repo_name=self.repo_name,
1936 repo_id=self.repo_id, **override)
1936 repo_id=self.repo_id, **override)
1937
1937
1938 def set_state(self, state):
1938 def set_state(self, state):
1939 self.repo_state = state
1939 self.repo_state = state
1940 Session().add(self)
1940 Session().add(self)
1941 #==========================================================================
1941 #==========================================================================
1942 # SCM PROPERTIES
1942 # SCM PROPERTIES
1943 #==========================================================================
1943 #==========================================================================
1944
1944
1945 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1945 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1946 return get_commit_safe(
1946 return get_commit_safe(
1947 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1947 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1948
1948
1949 def get_changeset(self, rev=None, pre_load=None):
1949 def get_changeset(self, rev=None, pre_load=None):
1950 warnings.warn("Use get_commit", DeprecationWarning)
1950 warnings.warn("Use get_commit", DeprecationWarning)
1951 commit_id = None
1951 commit_id = None
1952 commit_idx = None
1952 commit_idx = None
1953 if isinstance(rev, basestring):
1953 if isinstance(rev, basestring):
1954 commit_id = rev
1954 commit_id = rev
1955 else:
1955 else:
1956 commit_idx = rev
1956 commit_idx = rev
1957 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1957 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1958 pre_load=pre_load)
1958 pre_load=pre_load)
1959
1959
1960 def get_landing_commit(self):
1960 def get_landing_commit(self):
1961 """
1961 """
1962 Returns landing commit, or if that doesn't exist returns the tip
1962 Returns landing commit, or if that doesn't exist returns the tip
1963 """
1963 """
1964 _rev_type, _rev = self.landing_rev
1964 _rev_type, _rev = self.landing_rev
1965 commit = self.get_commit(_rev)
1965 commit = self.get_commit(_rev)
1966 if isinstance(commit, EmptyCommit):
1966 if isinstance(commit, EmptyCommit):
1967 return self.get_commit()
1967 return self.get_commit()
1968 return commit
1968 return commit
1969
1969
1970 def update_commit_cache(self, cs_cache=None, config=None):
1970 def update_commit_cache(self, cs_cache=None, config=None):
1971 """
1971 """
1972 Update cache of last changeset for repository, keys should be::
1972 Update cache of last changeset for repository, keys should be::
1973
1973
1974 short_id
1974 short_id
1975 raw_id
1975 raw_id
1976 revision
1976 revision
1977 parents
1977 parents
1978 message
1978 message
1979 date
1979 date
1980 author
1980 author
1981
1981
1982 :param cs_cache:
1982 :param cs_cache:
1983 """
1983 """
1984 from rhodecode.lib.vcs.backends.base import BaseChangeset
1984 from rhodecode.lib.vcs.backends.base import BaseChangeset
1985 if cs_cache is None:
1985 if cs_cache is None:
1986 # use no-cache version here
1986 # use no-cache version here
1987 scm_repo = self.scm_instance(cache=False, config=config)
1987 scm_repo = self.scm_instance(cache=False, config=config)
1988 if scm_repo:
1988 if scm_repo:
1989 cs_cache = scm_repo.get_commit(
1989 cs_cache = scm_repo.get_commit(
1990 pre_load=["author", "date", "message", "parents"])
1990 pre_load=["author", "date", "message", "parents"])
1991 else:
1991 else:
1992 cs_cache = EmptyCommit()
1992 cs_cache = EmptyCommit()
1993
1993
1994 if isinstance(cs_cache, BaseChangeset):
1994 if isinstance(cs_cache, BaseChangeset):
1995 cs_cache = cs_cache.__json__()
1995 cs_cache = cs_cache.__json__()
1996
1996
1997 def is_outdated(new_cs_cache):
1997 def is_outdated(new_cs_cache):
1998 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1998 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1999 new_cs_cache['revision'] != self.changeset_cache['revision']):
1999 new_cs_cache['revision'] != self.changeset_cache['revision']):
2000 return True
2000 return True
2001 return False
2001 return False
2002
2002
2003 # check if we have maybe already latest cached revision
2003 # check if we have maybe already latest cached revision
2004 if is_outdated(cs_cache) or not self.changeset_cache:
2004 if is_outdated(cs_cache) or not self.changeset_cache:
2005 _default = datetime.datetime.fromtimestamp(0)
2005 _default = datetime.datetime.fromtimestamp(0)
2006 last_change = cs_cache.get('date') or _default
2006 last_change = cs_cache.get('date') or _default
2007 log.debug('updated repo %s with new cs cache %s',
2007 log.debug('updated repo %s with new cs cache %s',
2008 self.repo_name, cs_cache)
2008 self.repo_name, cs_cache)
2009 self.updated_on = last_change
2009 self.updated_on = last_change
2010 self.changeset_cache = cs_cache
2010 self.changeset_cache = cs_cache
2011 Session().add(self)
2011 Session().add(self)
2012 Session().commit()
2012 Session().commit()
2013 else:
2013 else:
2014 log.debug('Skipping update_commit_cache for repo:`%s` '
2014 log.debug('Skipping update_commit_cache for repo:`%s` '
2015 'commit already with latest changes', self.repo_name)
2015 'commit already with latest changes', self.repo_name)
2016
2016
2017 @property
2017 @property
2018 def tip(self):
2018 def tip(self):
2019 return self.get_commit('tip')
2019 return self.get_commit('tip')
2020
2020
2021 @property
2021 @property
2022 def author(self):
2022 def author(self):
2023 return self.tip.author
2023 return self.tip.author
2024
2024
2025 @property
2025 @property
2026 def last_change(self):
2026 def last_change(self):
2027 return self.scm_instance().last_change
2027 return self.scm_instance().last_change
2028
2028
2029 def get_comments(self, revisions=None):
2029 def get_comments(self, revisions=None):
2030 """
2030 """
2031 Returns comments for this repository grouped by revisions
2031 Returns comments for this repository grouped by revisions
2032
2032
2033 :param revisions: filter query by revisions only
2033 :param revisions: filter query by revisions only
2034 """
2034 """
2035 cmts = ChangesetComment.query()\
2035 cmts = ChangesetComment.query()\
2036 .filter(ChangesetComment.repo == self)
2036 .filter(ChangesetComment.repo == self)
2037 if revisions:
2037 if revisions:
2038 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2038 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2039 grouped = collections.defaultdict(list)
2039 grouped = collections.defaultdict(list)
2040 for cmt in cmts.all():
2040 for cmt in cmts.all():
2041 grouped[cmt.revision].append(cmt)
2041 grouped[cmt.revision].append(cmt)
2042 return grouped
2042 return grouped
2043
2043
2044 def statuses(self, revisions=None):
2044 def statuses(self, revisions=None):
2045 """
2045 """
2046 Returns statuses for this repository
2046 Returns statuses for this repository
2047
2047
2048 :param revisions: list of revisions to get statuses for
2048 :param revisions: list of revisions to get statuses for
2049 """
2049 """
2050 statuses = ChangesetStatus.query()\
2050 statuses = ChangesetStatus.query()\
2051 .filter(ChangesetStatus.repo == self)\
2051 .filter(ChangesetStatus.repo == self)\
2052 .filter(ChangesetStatus.version == 0)
2052 .filter(ChangesetStatus.version == 0)
2053
2053
2054 if revisions:
2054 if revisions:
2055 # Try doing the filtering in chunks to avoid hitting limits
2055 # Try doing the filtering in chunks to avoid hitting limits
2056 size = 500
2056 size = 500
2057 status_results = []
2057 status_results = []
2058 for chunk in xrange(0, len(revisions), size):
2058 for chunk in xrange(0, len(revisions), size):
2059 status_results += statuses.filter(
2059 status_results += statuses.filter(
2060 ChangesetStatus.revision.in_(
2060 ChangesetStatus.revision.in_(
2061 revisions[chunk: chunk+size])
2061 revisions[chunk: chunk+size])
2062 ).all()
2062 ).all()
2063 else:
2063 else:
2064 status_results = statuses.all()
2064 status_results = statuses.all()
2065
2065
2066 grouped = {}
2066 grouped = {}
2067
2067
2068 # maybe we have open new pullrequest without a status?
2068 # maybe we have open new pullrequest without a status?
2069 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2069 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2070 status_lbl = ChangesetStatus.get_status_lbl(stat)
2070 status_lbl = ChangesetStatus.get_status_lbl(stat)
2071 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2071 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2072 for rev in pr.revisions:
2072 for rev in pr.revisions:
2073 pr_id = pr.pull_request_id
2073 pr_id = pr.pull_request_id
2074 pr_repo = pr.target_repo.repo_name
2074 pr_repo = pr.target_repo.repo_name
2075 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2075 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2076
2076
2077 for stat in status_results:
2077 for stat in status_results:
2078 pr_id = pr_repo = None
2078 pr_id = pr_repo = None
2079 if stat.pull_request:
2079 if stat.pull_request:
2080 pr_id = stat.pull_request.pull_request_id
2080 pr_id = stat.pull_request.pull_request_id
2081 pr_repo = stat.pull_request.target_repo.repo_name
2081 pr_repo = stat.pull_request.target_repo.repo_name
2082 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2082 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2083 pr_id, pr_repo]
2083 pr_id, pr_repo]
2084 return grouped
2084 return grouped
2085
2085
2086 # ==========================================================================
2086 # ==========================================================================
2087 # SCM CACHE INSTANCE
2087 # SCM CACHE INSTANCE
2088 # ==========================================================================
2088 # ==========================================================================
2089
2089
2090 def scm_instance(self, **kwargs):
2090 def scm_instance(self, **kwargs):
2091 import rhodecode
2091 import rhodecode
2092
2092
2093 # Passing a config will not hit the cache currently only used
2093 # Passing a config will not hit the cache currently only used
2094 # for repo2dbmapper
2094 # for repo2dbmapper
2095 config = kwargs.pop('config', None)
2095 config = kwargs.pop('config', None)
2096 cache = kwargs.pop('cache', None)
2096 cache = kwargs.pop('cache', None)
2097 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2097 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2098 # if cache is NOT defined use default global, else we have a full
2098 # if cache is NOT defined use default global, else we have a full
2099 # control over cache behaviour
2099 # control over cache behaviour
2100 if cache is None and full_cache and not config:
2100 if cache is None and full_cache and not config:
2101 return self._get_instance_cached()
2101 return self._get_instance_cached()
2102 return self._get_instance(cache=bool(cache), config=config)
2102 return self._get_instance(cache=bool(cache), config=config)
2103
2103
2104 def _get_instance_cached(self):
2104 def _get_instance_cached(self):
2105 @cache_region('long_term')
2105 @cache_region('long_term')
2106 def _get_repo(cache_key):
2106 def _get_repo(cache_key):
2107 return self._get_instance()
2107 return self._get_instance()
2108
2108
2109 invalidator_context = CacheKey.repo_context_cache(
2109 invalidator_context = CacheKey.repo_context_cache(
2110 _get_repo, self.repo_name, None, thread_scoped=True)
2110 _get_repo, self.repo_name, None, thread_scoped=True)
2111
2111
2112 with invalidator_context as context:
2112 with invalidator_context as context:
2113 context.invalidate()
2113 context.invalidate()
2114 repo = context.compute()
2114 repo = context.compute()
2115
2115
2116 return repo
2116 return repo
2117
2117
2118 def _get_instance(self, cache=True, config=None):
2118 def _get_instance(self, cache=True, config=None):
2119 config = config or self._config
2119 config = config or self._config
2120 custom_wire = {
2120 custom_wire = {
2121 'cache': cache # controls the vcs.remote cache
2121 'cache': cache # controls the vcs.remote cache
2122 }
2122 }
2123 repo = get_vcs_instance(
2123 repo = get_vcs_instance(
2124 repo_path=safe_str(self.repo_full_path),
2124 repo_path=safe_str(self.repo_full_path),
2125 config=config,
2125 config=config,
2126 with_wire=custom_wire,
2126 with_wire=custom_wire,
2127 create=False,
2127 create=False,
2128 _vcs_alias=self.repo_type)
2128 _vcs_alias=self.repo_type)
2129
2129
2130 return repo
2130 return repo
2131
2131
2132 def __json__(self):
2132 def __json__(self):
2133 return {'landing_rev': self.landing_rev}
2133 return {'landing_rev': self.landing_rev}
2134
2134
2135 def get_dict(self):
2135 def get_dict(self):
2136
2136
2137 # Since we transformed `repo_name` to a hybrid property, we need to
2137 # Since we transformed `repo_name` to a hybrid property, we need to
2138 # keep compatibility with the code which uses `repo_name` field.
2138 # keep compatibility with the code which uses `repo_name` field.
2139
2139
2140 result = super(Repository, self).get_dict()
2140 result = super(Repository, self).get_dict()
2141 result['repo_name'] = result.pop('_repo_name', None)
2141 result['repo_name'] = result.pop('_repo_name', None)
2142 return result
2142 return result
2143
2143
2144
2144
2145 class RepoGroup(Base, BaseModel):
2145 class RepoGroup(Base, BaseModel):
2146 __tablename__ = 'groups'
2146 __tablename__ = 'groups'
2147 __table_args__ = (
2147 __table_args__ = (
2148 UniqueConstraint('group_name', 'group_parent_id'),
2148 UniqueConstraint('group_name', 'group_parent_id'),
2149 CheckConstraint('group_id != group_parent_id'),
2149 CheckConstraint('group_id != group_parent_id'),
2150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2152 )
2152 )
2153 __mapper_args__ = {'order_by': 'group_name'}
2153 __mapper_args__ = {'order_by': 'group_name'}
2154
2154
2155 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2155 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2156
2156
2157 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2157 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2158 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2158 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2159 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2159 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2160 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2160 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2161 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2161 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2162 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2162 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2163 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2163 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2164 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2164 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2165
2165
2166 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2166 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2167 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2167 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2168 parent_group = relationship('RepoGroup', remote_side=group_id)
2168 parent_group = relationship('RepoGroup', remote_side=group_id)
2169 user = relationship('User')
2169 user = relationship('User')
2170 integrations = relationship('Integration',
2170 integrations = relationship('Integration',
2171 cascade="all, delete, delete-orphan")
2171 cascade="all, delete, delete-orphan")
2172
2172
2173 def __init__(self, group_name='', parent_group=None):
2173 def __init__(self, group_name='', parent_group=None):
2174 self.group_name = group_name
2174 self.group_name = group_name
2175 self.parent_group = parent_group
2175 self.parent_group = parent_group
2176
2176
2177 def __unicode__(self):
2177 def __unicode__(self):
2178 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2178 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2179 self.group_name)
2179 self.group_name)
2180
2180
2181 @classmethod
2181 @classmethod
2182 def _generate_choice(cls, repo_group):
2182 def _generate_choice(cls, repo_group):
2183 from webhelpers.html import literal as _literal
2183 from webhelpers.html import literal as _literal
2184 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2184 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2185 return repo_group.group_id, _name(repo_group.full_path_splitted)
2185 return repo_group.group_id, _name(repo_group.full_path_splitted)
2186
2186
2187 @classmethod
2187 @classmethod
2188 def groups_choices(cls, groups=None, show_empty_group=True):
2188 def groups_choices(cls, groups=None, show_empty_group=True):
2189 if not groups:
2189 if not groups:
2190 groups = cls.query().all()
2190 groups = cls.query().all()
2191
2191
2192 repo_groups = []
2192 repo_groups = []
2193 if show_empty_group:
2193 if show_empty_group:
2194 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2194 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2195
2195
2196 repo_groups.extend([cls._generate_choice(x) for x in groups])
2196 repo_groups.extend([cls._generate_choice(x) for x in groups])
2197
2197
2198 repo_groups = sorted(
2198 repo_groups = sorted(
2199 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2199 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2200 return repo_groups
2200 return repo_groups
2201
2201
2202 @classmethod
2202 @classmethod
2203 def url_sep(cls):
2203 def url_sep(cls):
2204 return URL_SEP
2204 return URL_SEP
2205
2205
2206 @classmethod
2206 @classmethod
2207 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2207 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2208 if case_insensitive:
2208 if case_insensitive:
2209 gr = cls.query().filter(func.lower(cls.group_name)
2209 gr = cls.query().filter(func.lower(cls.group_name)
2210 == func.lower(group_name))
2210 == func.lower(group_name))
2211 else:
2211 else:
2212 gr = cls.query().filter(cls.group_name == group_name)
2212 gr = cls.query().filter(cls.group_name == group_name)
2213 if cache:
2213 if cache:
2214 name_key = _hash_key(group_name)
2214 name_key = _hash_key(group_name)
2215 gr = gr.options(
2215 gr = gr.options(
2216 FromCache("sql_cache_short", "get_group_%s" % name_key))
2216 FromCache("sql_cache_short", "get_group_%s" % name_key))
2217 return gr.scalar()
2217 return gr.scalar()
2218
2218
2219 @classmethod
2219 @classmethod
2220 def get_user_personal_repo_group(cls, user_id):
2220 def get_user_personal_repo_group(cls, user_id):
2221 user = User.get(user_id)
2221 user = User.get(user_id)
2222 if user.username == User.DEFAULT_USER:
2222 if user.username == User.DEFAULT_USER:
2223 return None
2223 return None
2224
2224
2225 return cls.query()\
2225 return cls.query()\
2226 .filter(cls.personal == true()) \
2226 .filter(cls.personal == true()) \
2227 .filter(cls.user == user).scalar()
2227 .filter(cls.user == user).scalar()
2228
2228
2229 @classmethod
2229 @classmethod
2230 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2230 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2231 case_insensitive=True):
2231 case_insensitive=True):
2232 q = RepoGroup.query()
2232 q = RepoGroup.query()
2233
2233
2234 if not isinstance(user_id, Optional):
2234 if not isinstance(user_id, Optional):
2235 q = q.filter(RepoGroup.user_id == user_id)
2235 q = q.filter(RepoGroup.user_id == user_id)
2236
2236
2237 if not isinstance(group_id, Optional):
2237 if not isinstance(group_id, Optional):
2238 q = q.filter(RepoGroup.group_parent_id == group_id)
2238 q = q.filter(RepoGroup.group_parent_id == group_id)
2239
2239
2240 if case_insensitive:
2240 if case_insensitive:
2241 q = q.order_by(func.lower(RepoGroup.group_name))
2241 q = q.order_by(func.lower(RepoGroup.group_name))
2242 else:
2242 else:
2243 q = q.order_by(RepoGroup.group_name)
2243 q = q.order_by(RepoGroup.group_name)
2244 return q.all()
2244 return q.all()
2245
2245
2246 @property
2246 @property
2247 def parents(self):
2247 def parents(self):
2248 parents_recursion_limit = 10
2248 parents_recursion_limit = 10
2249 groups = []
2249 groups = []
2250 if self.parent_group is None:
2250 if self.parent_group is None:
2251 return groups
2251 return groups
2252 cur_gr = self.parent_group
2252 cur_gr = self.parent_group
2253 groups.insert(0, cur_gr)
2253 groups.insert(0, cur_gr)
2254 cnt = 0
2254 cnt = 0
2255 while 1:
2255 while 1:
2256 cnt += 1
2256 cnt += 1
2257 gr = getattr(cur_gr, 'parent_group', None)
2257 gr = getattr(cur_gr, 'parent_group', None)
2258 cur_gr = cur_gr.parent_group
2258 cur_gr = cur_gr.parent_group
2259 if gr is None:
2259 if gr is None:
2260 break
2260 break
2261 if cnt == parents_recursion_limit:
2261 if cnt == parents_recursion_limit:
2262 # this will prevent accidental infinit loops
2262 # this will prevent accidental infinit loops
2263 log.error(('more than %s parents found for group %s, stopping '
2263 log.error(('more than %s parents found for group %s, stopping '
2264 'recursive parent fetching' % (parents_recursion_limit, self)))
2264 'recursive parent fetching' % (parents_recursion_limit, self)))
2265 break
2265 break
2266
2266
2267 groups.insert(0, gr)
2267 groups.insert(0, gr)
2268 return groups
2268 return groups
2269
2269
2270 @property
2270 @property
2271 def children(self):
2271 def children(self):
2272 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2272 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2273
2273
2274 @property
2274 @property
2275 def name(self):
2275 def name(self):
2276 return self.group_name.split(RepoGroup.url_sep())[-1]
2276 return self.group_name.split(RepoGroup.url_sep())[-1]
2277
2277
2278 @property
2278 @property
2279 def full_path(self):
2279 def full_path(self):
2280 return self.group_name
2280 return self.group_name
2281
2281
2282 @property
2282 @property
2283 def full_path_splitted(self):
2283 def full_path_splitted(self):
2284 return self.group_name.split(RepoGroup.url_sep())
2284 return self.group_name.split(RepoGroup.url_sep())
2285
2285
2286 @property
2286 @property
2287 def repositories(self):
2287 def repositories(self):
2288 return Repository.query()\
2288 return Repository.query()\
2289 .filter(Repository.group == self)\
2289 .filter(Repository.group == self)\
2290 .order_by(Repository.repo_name)
2290 .order_by(Repository.repo_name)
2291
2291
2292 @property
2292 @property
2293 def repositories_recursive_count(self):
2293 def repositories_recursive_count(self):
2294 cnt = self.repositories.count()
2294 cnt = self.repositories.count()
2295
2295
2296 def children_count(group):
2296 def children_count(group):
2297 cnt = 0
2297 cnt = 0
2298 for child in group.children:
2298 for child in group.children:
2299 cnt += child.repositories.count()
2299 cnt += child.repositories.count()
2300 cnt += children_count(child)
2300 cnt += children_count(child)
2301 return cnt
2301 return cnt
2302
2302
2303 return cnt + children_count(self)
2303 return cnt + children_count(self)
2304
2304
2305 def _recursive_objects(self, include_repos=True):
2305 def _recursive_objects(self, include_repos=True):
2306 all_ = []
2306 all_ = []
2307
2307
2308 def _get_members(root_gr):
2308 def _get_members(root_gr):
2309 if include_repos:
2309 if include_repos:
2310 for r in root_gr.repositories:
2310 for r in root_gr.repositories:
2311 all_.append(r)
2311 all_.append(r)
2312 childs = root_gr.children.all()
2312 childs = root_gr.children.all()
2313 if childs:
2313 if childs:
2314 for gr in childs:
2314 for gr in childs:
2315 all_.append(gr)
2315 all_.append(gr)
2316 _get_members(gr)
2316 _get_members(gr)
2317
2317
2318 _get_members(self)
2318 _get_members(self)
2319 return [self] + all_
2319 return [self] + all_
2320
2320
2321 def recursive_groups_and_repos(self):
2321 def recursive_groups_and_repos(self):
2322 """
2322 """
2323 Recursive return all groups, with repositories in those groups
2323 Recursive return all groups, with repositories in those groups
2324 """
2324 """
2325 return self._recursive_objects()
2325 return self._recursive_objects()
2326
2326
2327 def recursive_groups(self):
2327 def recursive_groups(self):
2328 """
2328 """
2329 Returns all children groups for this group including children of children
2329 Returns all children groups for this group including children of children
2330 """
2330 """
2331 return self._recursive_objects(include_repos=False)
2331 return self._recursive_objects(include_repos=False)
2332
2332
2333 def get_new_name(self, group_name):
2333 def get_new_name(self, group_name):
2334 """
2334 """
2335 returns new full group name based on parent and new name
2335 returns new full group name based on parent and new name
2336
2336
2337 :param group_name:
2337 :param group_name:
2338 """
2338 """
2339 path_prefix = (self.parent_group.full_path_splitted if
2339 path_prefix = (self.parent_group.full_path_splitted if
2340 self.parent_group else [])
2340 self.parent_group else [])
2341 return RepoGroup.url_sep().join(path_prefix + [group_name])
2341 return RepoGroup.url_sep().join(path_prefix + [group_name])
2342
2342
2343 def permissions(self, with_admins=True, with_owner=True):
2343 def permissions(self, with_admins=True, with_owner=True):
2344 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2344 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2345 q = q.options(joinedload(UserRepoGroupToPerm.group),
2345 q = q.options(joinedload(UserRepoGroupToPerm.group),
2346 joinedload(UserRepoGroupToPerm.user),
2346 joinedload(UserRepoGroupToPerm.user),
2347 joinedload(UserRepoGroupToPerm.permission),)
2347 joinedload(UserRepoGroupToPerm.permission),)
2348
2348
2349 # get owners and admins and permissions. We do a trick of re-writing
2349 # get owners and admins and permissions. We do a trick of re-writing
2350 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2350 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2351 # has a global reference and changing one object propagates to all
2351 # has a global reference and changing one object propagates to all
2352 # others. This means if admin is also an owner admin_row that change
2352 # others. This means if admin is also an owner admin_row that change
2353 # would propagate to both objects
2353 # would propagate to both objects
2354 perm_rows = []
2354 perm_rows = []
2355 for _usr in q.all():
2355 for _usr in q.all():
2356 usr = AttributeDict(_usr.user.get_dict())
2356 usr = AttributeDict(_usr.user.get_dict())
2357 usr.permission = _usr.permission.permission_name
2357 usr.permission = _usr.permission.permission_name
2358 perm_rows.append(usr)
2358 perm_rows.append(usr)
2359
2359
2360 # filter the perm rows by 'default' first and then sort them by
2360 # filter the perm rows by 'default' first and then sort them by
2361 # admin,write,read,none permissions sorted again alphabetically in
2361 # admin,write,read,none permissions sorted again alphabetically in
2362 # each group
2362 # each group
2363 perm_rows = sorted(perm_rows, key=display_sort)
2363 perm_rows = sorted(perm_rows, key=display_sort)
2364
2364
2365 _admin_perm = 'group.admin'
2365 _admin_perm = 'group.admin'
2366 owner_row = []
2366 owner_row = []
2367 if with_owner:
2367 if with_owner:
2368 usr = AttributeDict(self.user.get_dict())
2368 usr = AttributeDict(self.user.get_dict())
2369 usr.owner_row = True
2369 usr.owner_row = True
2370 usr.permission = _admin_perm
2370 usr.permission = _admin_perm
2371 owner_row.append(usr)
2371 owner_row.append(usr)
2372
2372
2373 super_admin_rows = []
2373 super_admin_rows = []
2374 if with_admins:
2374 if with_admins:
2375 for usr in User.get_all_super_admins():
2375 for usr in User.get_all_super_admins():
2376 # if this admin is also owner, don't double the record
2376 # if this admin is also owner, don't double the record
2377 if usr.user_id == owner_row[0].user_id:
2377 if usr.user_id == owner_row[0].user_id:
2378 owner_row[0].admin_row = True
2378 owner_row[0].admin_row = True
2379 else:
2379 else:
2380 usr = AttributeDict(usr.get_dict())
2380 usr = AttributeDict(usr.get_dict())
2381 usr.admin_row = True
2381 usr.admin_row = True
2382 usr.permission = _admin_perm
2382 usr.permission = _admin_perm
2383 super_admin_rows.append(usr)
2383 super_admin_rows.append(usr)
2384
2384
2385 return super_admin_rows + owner_row + perm_rows
2385 return super_admin_rows + owner_row + perm_rows
2386
2386
2387 def permission_user_groups(self):
2387 def permission_user_groups(self):
2388 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2388 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2389 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2389 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2390 joinedload(UserGroupRepoGroupToPerm.users_group),
2390 joinedload(UserGroupRepoGroupToPerm.users_group),
2391 joinedload(UserGroupRepoGroupToPerm.permission),)
2391 joinedload(UserGroupRepoGroupToPerm.permission),)
2392
2392
2393 perm_rows = []
2393 perm_rows = []
2394 for _user_group in q.all():
2394 for _user_group in q.all():
2395 usr = AttributeDict(_user_group.users_group.get_dict())
2395 usr = AttributeDict(_user_group.users_group.get_dict())
2396 usr.permission = _user_group.permission.permission_name
2396 usr.permission = _user_group.permission.permission_name
2397 perm_rows.append(usr)
2397 perm_rows.append(usr)
2398
2398
2399 return perm_rows
2399 return perm_rows
2400
2400
2401 def get_api_data(self):
2401 def get_api_data(self):
2402 """
2402 """
2403 Common function for generating api data
2403 Common function for generating api data
2404
2404
2405 """
2405 """
2406 group = self
2406 group = self
2407 data = {
2407 data = {
2408 'group_id': group.group_id,
2408 'group_id': group.group_id,
2409 'group_name': group.group_name,
2409 'group_name': group.group_name,
2410 'group_description': group.group_description,
2410 'group_description': group.group_description,
2411 'parent_group': group.parent_group.group_name if group.parent_group else None,
2411 'parent_group': group.parent_group.group_name if group.parent_group else None,
2412 'repositories': [x.repo_name for x in group.repositories],
2412 'repositories': [x.repo_name for x in group.repositories],
2413 'owner': group.user.username,
2413 'owner': group.user.username,
2414 }
2414 }
2415 return data
2415 return data
2416
2416
2417
2417
2418 class Permission(Base, BaseModel):
2418 class Permission(Base, BaseModel):
2419 __tablename__ = 'permissions'
2419 __tablename__ = 'permissions'
2420 __table_args__ = (
2420 __table_args__ = (
2421 Index('p_perm_name_idx', 'permission_name'),
2421 Index('p_perm_name_idx', 'permission_name'),
2422 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2422 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2423 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2423 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2424 )
2424 )
2425 PERMS = [
2425 PERMS = [
2426 ('hg.admin', _('RhodeCode Super Administrator')),
2426 ('hg.admin', _('RhodeCode Super Administrator')),
2427
2427
2428 ('repository.none', _('Repository no access')),
2428 ('repository.none', _('Repository no access')),
2429 ('repository.read', _('Repository read access')),
2429 ('repository.read', _('Repository read access')),
2430 ('repository.write', _('Repository write access')),
2430 ('repository.write', _('Repository write access')),
2431 ('repository.admin', _('Repository admin access')),
2431 ('repository.admin', _('Repository admin access')),
2432
2432
2433 ('group.none', _('Repository group no access')),
2433 ('group.none', _('Repository group no access')),
2434 ('group.read', _('Repository group read access')),
2434 ('group.read', _('Repository group read access')),
2435 ('group.write', _('Repository group write access')),
2435 ('group.write', _('Repository group write access')),
2436 ('group.admin', _('Repository group admin access')),
2436 ('group.admin', _('Repository group admin access')),
2437
2437
2438 ('usergroup.none', _('User group no access')),
2438 ('usergroup.none', _('User group no access')),
2439 ('usergroup.read', _('User group read access')),
2439 ('usergroup.read', _('User group read access')),
2440 ('usergroup.write', _('User group write access')),
2440 ('usergroup.write', _('User group write access')),
2441 ('usergroup.admin', _('User group admin access')),
2441 ('usergroup.admin', _('User group admin access')),
2442
2442
2443 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2443 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2444 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2444 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2445
2445
2446 ('hg.usergroup.create.false', _('User Group creation disabled')),
2446 ('hg.usergroup.create.false', _('User Group creation disabled')),
2447 ('hg.usergroup.create.true', _('User Group creation enabled')),
2447 ('hg.usergroup.create.true', _('User Group creation enabled')),
2448
2448
2449 ('hg.create.none', _('Repository creation disabled')),
2449 ('hg.create.none', _('Repository creation disabled')),
2450 ('hg.create.repository', _('Repository creation enabled')),
2450 ('hg.create.repository', _('Repository creation enabled')),
2451 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2451 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2452 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2452 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2453
2453
2454 ('hg.fork.none', _('Repository forking disabled')),
2454 ('hg.fork.none', _('Repository forking disabled')),
2455 ('hg.fork.repository', _('Repository forking enabled')),
2455 ('hg.fork.repository', _('Repository forking enabled')),
2456
2456
2457 ('hg.register.none', _('Registration disabled')),
2457 ('hg.register.none', _('Registration disabled')),
2458 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2458 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2459 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2459 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2460
2460
2461 ('hg.password_reset.enabled', _('Password reset enabled')),
2461 ('hg.password_reset.enabled', _('Password reset enabled')),
2462 ('hg.password_reset.hidden', _('Password reset hidden')),
2462 ('hg.password_reset.hidden', _('Password reset hidden')),
2463 ('hg.password_reset.disabled', _('Password reset disabled')),
2463 ('hg.password_reset.disabled', _('Password reset disabled')),
2464
2464
2465 ('hg.extern_activate.manual', _('Manual activation of external account')),
2465 ('hg.extern_activate.manual', _('Manual activation of external account')),
2466 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2466 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2467
2467
2468 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2468 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2469 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2469 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2470 ]
2470 ]
2471
2471
2472 # definition of system default permissions for DEFAULT user
2472 # definition of system default permissions for DEFAULT user
2473 DEFAULT_USER_PERMISSIONS = [
2473 DEFAULT_USER_PERMISSIONS = [
2474 'repository.read',
2474 'repository.read',
2475 'group.read',
2475 'group.read',
2476 'usergroup.read',
2476 'usergroup.read',
2477 'hg.create.repository',
2477 'hg.create.repository',
2478 'hg.repogroup.create.false',
2478 'hg.repogroup.create.false',
2479 'hg.usergroup.create.false',
2479 'hg.usergroup.create.false',
2480 'hg.create.write_on_repogroup.true',
2480 'hg.create.write_on_repogroup.true',
2481 'hg.fork.repository',
2481 'hg.fork.repository',
2482 'hg.register.manual_activate',
2482 'hg.register.manual_activate',
2483 'hg.password_reset.enabled',
2483 'hg.password_reset.enabled',
2484 'hg.extern_activate.auto',
2484 'hg.extern_activate.auto',
2485 'hg.inherit_default_perms.true',
2485 'hg.inherit_default_perms.true',
2486 ]
2486 ]
2487
2487
2488 # defines which permissions are more important higher the more important
2488 # defines which permissions are more important higher the more important
2489 # Weight defines which permissions are more important.
2489 # Weight defines which permissions are more important.
2490 # The higher number the more important.
2490 # The higher number the more important.
2491 PERM_WEIGHTS = {
2491 PERM_WEIGHTS = {
2492 'repository.none': 0,
2492 'repository.none': 0,
2493 'repository.read': 1,
2493 'repository.read': 1,
2494 'repository.write': 3,
2494 'repository.write': 3,
2495 'repository.admin': 4,
2495 'repository.admin': 4,
2496
2496
2497 'group.none': 0,
2497 'group.none': 0,
2498 'group.read': 1,
2498 'group.read': 1,
2499 'group.write': 3,
2499 'group.write': 3,
2500 'group.admin': 4,
2500 'group.admin': 4,
2501
2501
2502 'usergroup.none': 0,
2502 'usergroup.none': 0,
2503 'usergroup.read': 1,
2503 'usergroup.read': 1,
2504 'usergroup.write': 3,
2504 'usergroup.write': 3,
2505 'usergroup.admin': 4,
2505 'usergroup.admin': 4,
2506
2506
2507 'hg.repogroup.create.false': 0,
2507 'hg.repogroup.create.false': 0,
2508 'hg.repogroup.create.true': 1,
2508 'hg.repogroup.create.true': 1,
2509
2509
2510 'hg.usergroup.create.false': 0,
2510 'hg.usergroup.create.false': 0,
2511 'hg.usergroup.create.true': 1,
2511 'hg.usergroup.create.true': 1,
2512
2512
2513 'hg.fork.none': 0,
2513 'hg.fork.none': 0,
2514 'hg.fork.repository': 1,
2514 'hg.fork.repository': 1,
2515 'hg.create.none': 0,
2515 'hg.create.none': 0,
2516 'hg.create.repository': 1
2516 'hg.create.repository': 1
2517 }
2517 }
2518
2518
2519 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2519 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2520 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2521 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2521 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2522
2522
2523 def __unicode__(self):
2523 def __unicode__(self):
2524 return u"<%s('%s:%s')>" % (
2524 return u"<%s('%s:%s')>" % (
2525 self.__class__.__name__, self.permission_id, self.permission_name
2525 self.__class__.__name__, self.permission_id, self.permission_name
2526 )
2526 )
2527
2527
2528 @classmethod
2528 @classmethod
2529 def get_by_key(cls, key):
2529 def get_by_key(cls, key):
2530 return cls.query().filter(cls.permission_name == key).scalar()
2530 return cls.query().filter(cls.permission_name == key).scalar()
2531
2531
2532 @classmethod
2532 @classmethod
2533 def get_default_repo_perms(cls, user_id, repo_id=None):
2533 def get_default_repo_perms(cls, user_id, repo_id=None):
2534 q = Session().query(UserRepoToPerm, Repository, Permission)\
2534 q = Session().query(UserRepoToPerm, Repository, Permission)\
2535 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2535 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2536 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2536 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2537 .filter(UserRepoToPerm.user_id == user_id)
2537 .filter(UserRepoToPerm.user_id == user_id)
2538 if repo_id:
2538 if repo_id:
2539 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2539 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2540 return q.all()
2540 return q.all()
2541
2541
2542 @classmethod
2542 @classmethod
2543 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2543 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2544 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2544 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2545 .join(
2545 .join(
2546 Permission,
2546 Permission,
2547 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2547 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2548 .join(
2548 .join(
2549 Repository,
2549 Repository,
2550 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2550 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2551 .join(
2551 .join(
2552 UserGroup,
2552 UserGroup,
2553 UserGroupRepoToPerm.users_group_id ==
2553 UserGroupRepoToPerm.users_group_id ==
2554 UserGroup.users_group_id)\
2554 UserGroup.users_group_id)\
2555 .join(
2555 .join(
2556 UserGroupMember,
2556 UserGroupMember,
2557 UserGroupRepoToPerm.users_group_id ==
2557 UserGroupRepoToPerm.users_group_id ==
2558 UserGroupMember.users_group_id)\
2558 UserGroupMember.users_group_id)\
2559 .filter(
2559 .filter(
2560 UserGroupMember.user_id == user_id,
2560 UserGroupMember.user_id == user_id,
2561 UserGroup.users_group_active == true())
2561 UserGroup.users_group_active == true())
2562 if repo_id:
2562 if repo_id:
2563 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2563 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2564 return q.all()
2564 return q.all()
2565
2565
2566 @classmethod
2566 @classmethod
2567 def get_default_group_perms(cls, user_id, repo_group_id=None):
2567 def get_default_group_perms(cls, user_id, repo_group_id=None):
2568 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2568 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2569 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2569 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2570 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2570 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2571 .filter(UserRepoGroupToPerm.user_id == user_id)
2571 .filter(UserRepoGroupToPerm.user_id == user_id)
2572 if repo_group_id:
2572 if repo_group_id:
2573 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2573 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2574 return q.all()
2574 return q.all()
2575
2575
2576 @classmethod
2576 @classmethod
2577 def get_default_group_perms_from_user_group(
2577 def get_default_group_perms_from_user_group(
2578 cls, user_id, repo_group_id=None):
2578 cls, user_id, repo_group_id=None):
2579 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2579 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2580 .join(
2580 .join(
2581 Permission,
2581 Permission,
2582 UserGroupRepoGroupToPerm.permission_id ==
2582 UserGroupRepoGroupToPerm.permission_id ==
2583 Permission.permission_id)\
2583 Permission.permission_id)\
2584 .join(
2584 .join(
2585 RepoGroup,
2585 RepoGroup,
2586 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2586 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2587 .join(
2587 .join(
2588 UserGroup,
2588 UserGroup,
2589 UserGroupRepoGroupToPerm.users_group_id ==
2589 UserGroupRepoGroupToPerm.users_group_id ==
2590 UserGroup.users_group_id)\
2590 UserGroup.users_group_id)\
2591 .join(
2591 .join(
2592 UserGroupMember,
2592 UserGroupMember,
2593 UserGroupRepoGroupToPerm.users_group_id ==
2593 UserGroupRepoGroupToPerm.users_group_id ==
2594 UserGroupMember.users_group_id)\
2594 UserGroupMember.users_group_id)\
2595 .filter(
2595 .filter(
2596 UserGroupMember.user_id == user_id,
2596 UserGroupMember.user_id == user_id,
2597 UserGroup.users_group_active == true())
2597 UserGroup.users_group_active == true())
2598 if repo_group_id:
2598 if repo_group_id:
2599 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2599 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2600 return q.all()
2600 return q.all()
2601
2601
2602 @classmethod
2602 @classmethod
2603 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2603 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2604 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2604 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2605 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2605 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2606 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2606 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2607 .filter(UserUserGroupToPerm.user_id == user_id)
2607 .filter(UserUserGroupToPerm.user_id == user_id)
2608 if user_group_id:
2608 if user_group_id:
2609 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2609 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2610 return q.all()
2610 return q.all()
2611
2611
2612 @classmethod
2612 @classmethod
2613 def get_default_user_group_perms_from_user_group(
2613 def get_default_user_group_perms_from_user_group(
2614 cls, user_id, user_group_id=None):
2614 cls, user_id, user_group_id=None):
2615 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2615 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2616 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2616 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2617 .join(
2617 .join(
2618 Permission,
2618 Permission,
2619 UserGroupUserGroupToPerm.permission_id ==
2619 UserGroupUserGroupToPerm.permission_id ==
2620 Permission.permission_id)\
2620 Permission.permission_id)\
2621 .join(
2621 .join(
2622 TargetUserGroup,
2622 TargetUserGroup,
2623 UserGroupUserGroupToPerm.target_user_group_id ==
2623 UserGroupUserGroupToPerm.target_user_group_id ==
2624 TargetUserGroup.users_group_id)\
2624 TargetUserGroup.users_group_id)\
2625 .join(
2625 .join(
2626 UserGroup,
2626 UserGroup,
2627 UserGroupUserGroupToPerm.user_group_id ==
2627 UserGroupUserGroupToPerm.user_group_id ==
2628 UserGroup.users_group_id)\
2628 UserGroup.users_group_id)\
2629 .join(
2629 .join(
2630 UserGroupMember,
2630 UserGroupMember,
2631 UserGroupUserGroupToPerm.user_group_id ==
2631 UserGroupUserGroupToPerm.user_group_id ==
2632 UserGroupMember.users_group_id)\
2632 UserGroupMember.users_group_id)\
2633 .filter(
2633 .filter(
2634 UserGroupMember.user_id == user_id,
2634 UserGroupMember.user_id == user_id,
2635 UserGroup.users_group_active == true())
2635 UserGroup.users_group_active == true())
2636 if user_group_id:
2636 if user_group_id:
2637 q = q.filter(
2637 q = q.filter(
2638 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2638 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2639
2639
2640 return q.all()
2640 return q.all()
2641
2641
2642
2642
2643 class UserRepoToPerm(Base, BaseModel):
2643 class UserRepoToPerm(Base, BaseModel):
2644 __tablename__ = 'repo_to_perm'
2644 __tablename__ = 'repo_to_perm'
2645 __table_args__ = (
2645 __table_args__ = (
2646 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2646 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2648 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2648 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2649 )
2649 )
2650 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2650 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2651 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2651 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2652 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2652 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2653 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2653 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2654
2654
2655 user = relationship('User')
2655 user = relationship('User')
2656 repository = relationship('Repository')
2656 repository = relationship('Repository')
2657 permission = relationship('Permission')
2657 permission = relationship('Permission')
2658
2658
2659 @classmethod
2659 @classmethod
2660 def create(cls, user, repository, permission):
2660 def create(cls, user, repository, permission):
2661 n = cls()
2661 n = cls()
2662 n.user = user
2662 n.user = user
2663 n.repository = repository
2663 n.repository = repository
2664 n.permission = permission
2664 n.permission = permission
2665 Session().add(n)
2665 Session().add(n)
2666 return n
2666 return n
2667
2667
2668 def __unicode__(self):
2668 def __unicode__(self):
2669 return u'<%s => %s >' % (self.user, self.repository)
2669 return u'<%s => %s >' % (self.user, self.repository)
2670
2670
2671
2671
2672 class UserUserGroupToPerm(Base, BaseModel):
2672 class UserUserGroupToPerm(Base, BaseModel):
2673 __tablename__ = 'user_user_group_to_perm'
2673 __tablename__ = 'user_user_group_to_perm'
2674 __table_args__ = (
2674 __table_args__ = (
2675 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2675 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 )
2678 )
2679 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2679 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2680 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2680 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2681 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2681 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2682 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2683
2683
2684 user = relationship('User')
2684 user = relationship('User')
2685 user_group = relationship('UserGroup')
2685 user_group = relationship('UserGroup')
2686 permission = relationship('Permission')
2686 permission = relationship('Permission')
2687
2687
2688 @classmethod
2688 @classmethod
2689 def create(cls, user, user_group, permission):
2689 def create(cls, user, user_group, permission):
2690 n = cls()
2690 n = cls()
2691 n.user = user
2691 n.user = user
2692 n.user_group = user_group
2692 n.user_group = user_group
2693 n.permission = permission
2693 n.permission = permission
2694 Session().add(n)
2694 Session().add(n)
2695 return n
2695 return n
2696
2696
2697 def __unicode__(self):
2697 def __unicode__(self):
2698 return u'<%s => %s >' % (self.user, self.user_group)
2698 return u'<%s => %s >' % (self.user, self.user_group)
2699
2699
2700
2700
2701 class UserToPerm(Base, BaseModel):
2701 class UserToPerm(Base, BaseModel):
2702 __tablename__ = 'user_to_perm'
2702 __tablename__ = 'user_to_perm'
2703 __table_args__ = (
2703 __table_args__ = (
2704 UniqueConstraint('user_id', 'permission_id'),
2704 UniqueConstraint('user_id', 'permission_id'),
2705 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2705 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2706 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2706 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2707 )
2707 )
2708 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2708 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2710 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2710 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2711
2711
2712 user = relationship('User')
2712 user = relationship('User')
2713 permission = relationship('Permission', lazy='joined')
2713 permission = relationship('Permission', lazy='joined')
2714
2714
2715 def __unicode__(self):
2715 def __unicode__(self):
2716 return u'<%s => %s >' % (self.user, self.permission)
2716 return u'<%s => %s >' % (self.user, self.permission)
2717
2717
2718
2718
2719 class UserGroupRepoToPerm(Base, BaseModel):
2719 class UserGroupRepoToPerm(Base, BaseModel):
2720 __tablename__ = 'users_group_repo_to_perm'
2720 __tablename__ = 'users_group_repo_to_perm'
2721 __table_args__ = (
2721 __table_args__ = (
2722 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2722 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 )
2725 )
2726 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2726 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2727 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2728 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2728 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2729 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2729 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2730
2730
2731 users_group = relationship('UserGroup')
2731 users_group = relationship('UserGroup')
2732 permission = relationship('Permission')
2732 permission = relationship('Permission')
2733 repository = relationship('Repository')
2733 repository = relationship('Repository')
2734
2734
2735 @classmethod
2735 @classmethod
2736 def create(cls, users_group, repository, permission):
2736 def create(cls, users_group, repository, permission):
2737 n = cls()
2737 n = cls()
2738 n.users_group = users_group
2738 n.users_group = users_group
2739 n.repository = repository
2739 n.repository = repository
2740 n.permission = permission
2740 n.permission = permission
2741 Session().add(n)
2741 Session().add(n)
2742 return n
2742 return n
2743
2743
2744 def __unicode__(self):
2744 def __unicode__(self):
2745 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2745 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2746
2746
2747
2747
2748 class UserGroupUserGroupToPerm(Base, BaseModel):
2748 class UserGroupUserGroupToPerm(Base, BaseModel):
2749 __tablename__ = 'user_group_user_group_to_perm'
2749 __tablename__ = 'user_group_user_group_to_perm'
2750 __table_args__ = (
2750 __table_args__ = (
2751 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2751 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2752 CheckConstraint('target_user_group_id != user_group_id'),
2752 CheckConstraint('target_user_group_id != user_group_id'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2755 )
2755 )
2756 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2756 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2757 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2758 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2758 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2759 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2759 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2760
2760
2761 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2761 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2762 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2762 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2763 permission = relationship('Permission')
2763 permission = relationship('Permission')
2764
2764
2765 @classmethod
2765 @classmethod
2766 def create(cls, target_user_group, user_group, permission):
2766 def create(cls, target_user_group, user_group, permission):
2767 n = cls()
2767 n = cls()
2768 n.target_user_group = target_user_group
2768 n.target_user_group = target_user_group
2769 n.user_group = user_group
2769 n.user_group = user_group
2770 n.permission = permission
2770 n.permission = permission
2771 Session().add(n)
2771 Session().add(n)
2772 return n
2772 return n
2773
2773
2774 def __unicode__(self):
2774 def __unicode__(self):
2775 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2775 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2776
2776
2777
2777
2778 class UserGroupToPerm(Base, BaseModel):
2778 class UserGroupToPerm(Base, BaseModel):
2779 __tablename__ = 'users_group_to_perm'
2779 __tablename__ = 'users_group_to_perm'
2780 __table_args__ = (
2780 __table_args__ = (
2781 UniqueConstraint('users_group_id', 'permission_id',),
2781 UniqueConstraint('users_group_id', 'permission_id',),
2782 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2782 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2783 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2783 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2784 )
2784 )
2785 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2785 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2786 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2786 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2787 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2787 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2788
2788
2789 users_group = relationship('UserGroup')
2789 users_group = relationship('UserGroup')
2790 permission = relationship('Permission')
2790 permission = relationship('Permission')
2791
2791
2792
2792
2793 class UserRepoGroupToPerm(Base, BaseModel):
2793 class UserRepoGroupToPerm(Base, BaseModel):
2794 __tablename__ = 'user_repo_group_to_perm'
2794 __tablename__ = 'user_repo_group_to_perm'
2795 __table_args__ = (
2795 __table_args__ = (
2796 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2796 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2799 )
2799 )
2800
2800
2801 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2801 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2803 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2803 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2804 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2804 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2805
2805
2806 user = relationship('User')
2806 user = relationship('User')
2807 group = relationship('RepoGroup')
2807 group = relationship('RepoGroup')
2808 permission = relationship('Permission')
2808 permission = relationship('Permission')
2809
2809
2810 @classmethod
2810 @classmethod
2811 def create(cls, user, repository_group, permission):
2811 def create(cls, user, repository_group, permission):
2812 n = cls()
2812 n = cls()
2813 n.user = user
2813 n.user = user
2814 n.group = repository_group
2814 n.group = repository_group
2815 n.permission = permission
2815 n.permission = permission
2816 Session().add(n)
2816 Session().add(n)
2817 return n
2817 return n
2818
2818
2819
2819
2820 class UserGroupRepoGroupToPerm(Base, BaseModel):
2820 class UserGroupRepoGroupToPerm(Base, BaseModel):
2821 __tablename__ = 'users_group_repo_group_to_perm'
2821 __tablename__ = 'users_group_repo_group_to_perm'
2822 __table_args__ = (
2822 __table_args__ = (
2823 UniqueConstraint('users_group_id', 'group_id'),
2823 UniqueConstraint('users_group_id', 'group_id'),
2824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2826 )
2826 )
2827
2827
2828 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2828 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2829 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2829 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2830 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2830 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2831 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2831 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2832
2832
2833 users_group = relationship('UserGroup')
2833 users_group = relationship('UserGroup')
2834 permission = relationship('Permission')
2834 permission = relationship('Permission')
2835 group = relationship('RepoGroup')
2835 group = relationship('RepoGroup')
2836
2836
2837 @classmethod
2837 @classmethod
2838 def create(cls, user_group, repository_group, permission):
2838 def create(cls, user_group, repository_group, permission):
2839 n = cls()
2839 n = cls()
2840 n.users_group = user_group
2840 n.users_group = user_group
2841 n.group = repository_group
2841 n.group = repository_group
2842 n.permission = permission
2842 n.permission = permission
2843 Session().add(n)
2843 Session().add(n)
2844 return n
2844 return n
2845
2845
2846 def __unicode__(self):
2846 def __unicode__(self):
2847 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2847 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2848
2848
2849
2849
2850 class Statistics(Base, BaseModel):
2850 class Statistics(Base, BaseModel):
2851 __tablename__ = 'statistics'
2851 __tablename__ = 'statistics'
2852 __table_args__ = (
2852 __table_args__ = (
2853 UniqueConstraint('repository_id'),
2853 UniqueConstraint('repository_id'),
2854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2856 )
2856 )
2857 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2857 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2859 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2859 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2860 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2860 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2861 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2861 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2862 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2862 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2863
2863
2864 repository = relationship('Repository', single_parent=True)
2864 repository = relationship('Repository', single_parent=True)
2865
2865
2866
2866
2867 class UserFollowing(Base, BaseModel):
2867 class UserFollowing(Base, BaseModel):
2868 __tablename__ = 'user_followings'
2868 __tablename__ = 'user_followings'
2869 __table_args__ = (
2869 __table_args__ = (
2870 UniqueConstraint('user_id', 'follows_repository_id'),
2870 UniqueConstraint('user_id', 'follows_repository_id'),
2871 UniqueConstraint('user_id', 'follows_user_id'),
2871 UniqueConstraint('user_id', 'follows_user_id'),
2872 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2872 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2873 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2873 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2874 )
2874 )
2875
2875
2876 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2876 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2877 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2877 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2878 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2878 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2879 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2879 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2880 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2880 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2881
2881
2882 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2882 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2883
2883
2884 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2884 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2885 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2885 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2886
2886
2887 @classmethod
2887 @classmethod
2888 def get_repo_followers(cls, repo_id):
2888 def get_repo_followers(cls, repo_id):
2889 return cls.query().filter(cls.follows_repo_id == repo_id)
2889 return cls.query().filter(cls.follows_repo_id == repo_id)
2890
2890
2891
2891
2892 class CacheKey(Base, BaseModel):
2892 class CacheKey(Base, BaseModel):
2893 __tablename__ = 'cache_invalidation'
2893 __tablename__ = 'cache_invalidation'
2894 __table_args__ = (
2894 __table_args__ = (
2895 UniqueConstraint('cache_key'),
2895 UniqueConstraint('cache_key'),
2896 Index('key_idx', 'cache_key'),
2896 Index('key_idx', 'cache_key'),
2897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2899 )
2899 )
2900 CACHE_TYPE_ATOM = 'ATOM'
2900 CACHE_TYPE_ATOM = 'ATOM'
2901 CACHE_TYPE_RSS = 'RSS'
2901 CACHE_TYPE_RSS = 'RSS'
2902 CACHE_TYPE_README = 'README'
2902 CACHE_TYPE_README = 'README'
2903
2903
2904 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2904 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2905 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2905 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2906 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2906 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2907 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2907 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2908
2908
2909 def __init__(self, cache_key, cache_args=''):
2909 def __init__(self, cache_key, cache_args=''):
2910 self.cache_key = cache_key
2910 self.cache_key = cache_key
2911 self.cache_args = cache_args
2911 self.cache_args = cache_args
2912 self.cache_active = False
2912 self.cache_active = False
2913
2913
2914 def __unicode__(self):
2914 def __unicode__(self):
2915 return u"<%s('%s:%s[%s]')>" % (
2915 return u"<%s('%s:%s[%s]')>" % (
2916 self.__class__.__name__,
2916 self.__class__.__name__,
2917 self.cache_id, self.cache_key, self.cache_active)
2917 self.cache_id, self.cache_key, self.cache_active)
2918
2918
2919 def _cache_key_partition(self):
2919 def _cache_key_partition(self):
2920 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2920 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2921 return prefix, repo_name, suffix
2921 return prefix, repo_name, suffix
2922
2922
2923 def get_prefix(self):
2923 def get_prefix(self):
2924 """
2924 """
2925 Try to extract prefix from existing cache key. The key could consist
2925 Try to extract prefix from existing cache key. The key could consist
2926 of prefix, repo_name, suffix
2926 of prefix, repo_name, suffix
2927 """
2927 """
2928 # this returns prefix, repo_name, suffix
2928 # this returns prefix, repo_name, suffix
2929 return self._cache_key_partition()[0]
2929 return self._cache_key_partition()[0]
2930
2930
2931 def get_suffix(self):
2931 def get_suffix(self):
2932 """
2932 """
2933 get suffix that might have been used in _get_cache_key to
2933 get suffix that might have been used in _get_cache_key to
2934 generate self.cache_key. Only used for informational purposes
2934 generate self.cache_key. Only used for informational purposes
2935 in repo_edit.mako.
2935 in repo_edit.mako.
2936 """
2936 """
2937 # prefix, repo_name, suffix
2937 # prefix, repo_name, suffix
2938 return self._cache_key_partition()[2]
2938 return self._cache_key_partition()[2]
2939
2939
2940 @classmethod
2940 @classmethod
2941 def delete_all_cache(cls):
2941 def delete_all_cache(cls):
2942 """
2942 """
2943 Delete all cache keys from database.
2943 Delete all cache keys from database.
2944 Should only be run when all instances are down and all entries
2944 Should only be run when all instances are down and all entries
2945 thus stale.
2945 thus stale.
2946 """
2946 """
2947 cls.query().delete()
2947 cls.query().delete()
2948 Session().commit()
2948 Session().commit()
2949
2949
2950 @classmethod
2950 @classmethod
2951 def get_cache_key(cls, repo_name, cache_type):
2951 def get_cache_key(cls, repo_name, cache_type):
2952 """
2952 """
2953
2953
2954 Generate a cache key for this process of RhodeCode instance.
2954 Generate a cache key for this process of RhodeCode instance.
2955 Prefix most likely will be process id or maybe explicitly set
2955 Prefix most likely will be process id or maybe explicitly set
2956 instance_id from .ini file.
2956 instance_id from .ini file.
2957 """
2957 """
2958 import rhodecode
2958 import rhodecode
2959 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2959 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2960
2960
2961 repo_as_unicode = safe_unicode(repo_name)
2961 repo_as_unicode = safe_unicode(repo_name)
2962 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2962 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2963 if cache_type else repo_as_unicode
2963 if cache_type else repo_as_unicode
2964
2964
2965 return u'{}{}'.format(prefix, key)
2965 return u'{}{}'.format(prefix, key)
2966
2966
2967 @classmethod
2967 @classmethod
2968 def set_invalidate(cls, repo_name, delete=False):
2968 def set_invalidate(cls, repo_name, delete=False):
2969 """
2969 """
2970 Mark all caches of a repo as invalid in the database.
2970 Mark all caches of a repo as invalid in the database.
2971 """
2971 """
2972
2972
2973 try:
2973 try:
2974 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2974 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2975 if delete:
2975 if delete:
2976 log.debug('cache objects deleted for repo %s',
2976 log.debug('cache objects deleted for repo %s',
2977 safe_str(repo_name))
2977 safe_str(repo_name))
2978 qry.delete()
2978 qry.delete()
2979 else:
2979 else:
2980 log.debug('cache objects marked as invalid for repo %s',
2980 log.debug('cache objects marked as invalid for repo %s',
2981 safe_str(repo_name))
2981 safe_str(repo_name))
2982 qry.update({"cache_active": False})
2982 qry.update({"cache_active": False})
2983
2983
2984 Session().commit()
2984 Session().commit()
2985 except Exception:
2985 except Exception:
2986 log.exception(
2986 log.exception(
2987 'Cache key invalidation failed for repository %s',
2987 'Cache key invalidation failed for repository %s',
2988 safe_str(repo_name))
2988 safe_str(repo_name))
2989 Session().rollback()
2989 Session().rollback()
2990
2990
2991 @classmethod
2991 @classmethod
2992 def get_active_cache(cls, cache_key):
2992 def get_active_cache(cls, cache_key):
2993 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2993 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2994 if inv_obj:
2994 if inv_obj:
2995 return inv_obj
2995 return inv_obj
2996 return None
2996 return None
2997
2997
2998 @classmethod
2998 @classmethod
2999 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2999 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3000 thread_scoped=False):
3000 thread_scoped=False):
3001 """
3001 """
3002 @cache_region('long_term')
3002 @cache_region('long_term')
3003 def _heavy_calculation(cache_key):
3003 def _heavy_calculation(cache_key):
3004 return 'result'
3004 return 'result'
3005
3005
3006 cache_context = CacheKey.repo_context_cache(
3006 cache_context = CacheKey.repo_context_cache(
3007 _heavy_calculation, repo_name, cache_type)
3007 _heavy_calculation, repo_name, cache_type)
3008
3008
3009 with cache_context as context:
3009 with cache_context as context:
3010 context.invalidate()
3010 context.invalidate()
3011 computed = context.compute()
3011 computed = context.compute()
3012
3012
3013 assert computed == 'result'
3013 assert computed == 'result'
3014 """
3014 """
3015 from rhodecode.lib import caches
3015 from rhodecode.lib import caches
3016 return caches.InvalidationContext(
3016 return caches.InvalidationContext(
3017 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3017 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3018
3018
3019
3019
3020 class ChangesetComment(Base, BaseModel):
3020 class ChangesetComment(Base, BaseModel):
3021 __tablename__ = 'changeset_comments'
3021 __tablename__ = 'changeset_comments'
3022 __table_args__ = (
3022 __table_args__ = (
3023 Index('cc_revision_idx', 'revision'),
3023 Index('cc_revision_idx', 'revision'),
3024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3026 )
3026 )
3027
3027
3028 COMMENT_OUTDATED = u'comment_outdated'
3028 COMMENT_OUTDATED = u'comment_outdated'
3029 COMMENT_TYPE_NOTE = u'note'
3029 COMMENT_TYPE_NOTE = u'note'
3030 COMMENT_TYPE_TODO = u'todo'
3030 COMMENT_TYPE_TODO = u'todo'
3031 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3031 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3032
3032
3033 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3033 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3034 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3034 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3035 revision = Column('revision', String(40), nullable=True)
3035 revision = Column('revision', String(40), nullable=True)
3036 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3036 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3037 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3037 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3038 line_no = Column('line_no', Unicode(10), nullable=True)
3038 line_no = Column('line_no', Unicode(10), nullable=True)
3039 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3039 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3040 f_path = Column('f_path', Unicode(1000), nullable=True)
3040 f_path = Column('f_path', Unicode(1000), nullable=True)
3041 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3041 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3042 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3042 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3043 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3043 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3044 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3044 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3045 renderer = Column('renderer', Unicode(64), nullable=True)
3045 renderer = Column('renderer', Unicode(64), nullable=True)
3046 display_state = Column('display_state', Unicode(128), nullable=True)
3046 display_state = Column('display_state', Unicode(128), nullable=True)
3047
3047
3048 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3048 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3049 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3049 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3050 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3050 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3051 author = relationship('User', lazy='joined')
3051 author = relationship('User', lazy='joined')
3052 repo = relationship('Repository')
3052 repo = relationship('Repository')
3053 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3053 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3054 pull_request = relationship('PullRequest', lazy='joined')
3054 pull_request = relationship('PullRequest', lazy='joined')
3055 pull_request_version = relationship('PullRequestVersion')
3055 pull_request_version = relationship('PullRequestVersion')
3056
3056
3057 @classmethod
3057 @classmethod
3058 def get_users(cls, revision=None, pull_request_id=None):
3058 def get_users(cls, revision=None, pull_request_id=None):
3059 """
3059 """
3060 Returns user associated with this ChangesetComment. ie those
3060 Returns user associated with this ChangesetComment. ie those
3061 who actually commented
3061 who actually commented
3062
3062
3063 :param cls:
3063 :param cls:
3064 :param revision:
3064 :param revision:
3065 """
3065 """
3066 q = Session().query(User)\
3066 q = Session().query(User)\
3067 .join(ChangesetComment.author)
3067 .join(ChangesetComment.author)
3068 if revision:
3068 if revision:
3069 q = q.filter(cls.revision == revision)
3069 q = q.filter(cls.revision == revision)
3070 elif pull_request_id:
3070 elif pull_request_id:
3071 q = q.filter(cls.pull_request_id == pull_request_id)
3071 q = q.filter(cls.pull_request_id == pull_request_id)
3072 return q.all()
3072 return q.all()
3073
3073
3074 @classmethod
3074 @classmethod
3075 def get_index_from_version(cls, pr_version, versions):
3075 def get_index_from_version(cls, pr_version, versions):
3076 num_versions = [x.pull_request_version_id for x in versions]
3076 num_versions = [x.pull_request_version_id for x in versions]
3077 try:
3077 try:
3078 return num_versions.index(pr_version) +1
3078 return num_versions.index(pr_version) +1
3079 except (IndexError, ValueError):
3079 except (IndexError, ValueError):
3080 return
3080 return
3081
3081
3082 @property
3082 @property
3083 def outdated(self):
3083 def outdated(self):
3084 return self.display_state == self.COMMENT_OUTDATED
3084 return self.display_state == self.COMMENT_OUTDATED
3085
3085
3086 def outdated_at_version(self, version):
3086 def outdated_at_version(self, version):
3087 """
3087 """
3088 Checks if comment is outdated for given pull request version
3088 Checks if comment is outdated for given pull request version
3089 """
3089 """
3090 return self.outdated and self.pull_request_version_id != version
3090 return self.outdated and self.pull_request_version_id != version
3091
3091
3092 def older_than_version(self, version):
3092 def older_than_version(self, version):
3093 """
3093 """
3094 Checks if comment is made from previous version than given
3094 Checks if comment is made from previous version than given
3095 """
3095 """
3096 if version is None:
3096 if version is None:
3097 return self.pull_request_version_id is not None
3097 return self.pull_request_version_id is not None
3098
3098
3099 return self.pull_request_version_id < version
3099 return self.pull_request_version_id < version
3100
3100
3101 @property
3101 @property
3102 def resolved(self):
3102 def resolved(self):
3103 return self.resolved_by[0] if self.resolved_by else None
3103 return self.resolved_by[0] if self.resolved_by else None
3104
3104
3105 @property
3105 @property
3106 def is_todo(self):
3106 def is_todo(self):
3107 return self.comment_type == self.COMMENT_TYPE_TODO
3107 return self.comment_type == self.COMMENT_TYPE_TODO
3108
3108
3109 def get_index_version(self, versions):
3109 def get_index_version(self, versions):
3110 return self.get_index_from_version(
3110 return self.get_index_from_version(
3111 self.pull_request_version_id, versions)
3111 self.pull_request_version_id, versions)
3112
3112
3113 def __repr__(self):
3113 def __repr__(self):
3114 if self.comment_id:
3114 if self.comment_id:
3115 return '<DB:Comment #%s>' % self.comment_id
3115 return '<DB:Comment #%s>' % self.comment_id
3116 else:
3116 else:
3117 return '<DB:Comment at %#x>' % id(self)
3117 return '<DB:Comment at %#x>' % id(self)
3118
3118
3119
3119
3120 class ChangesetStatus(Base, BaseModel):
3120 class ChangesetStatus(Base, BaseModel):
3121 __tablename__ = 'changeset_statuses'
3121 __tablename__ = 'changeset_statuses'
3122 __table_args__ = (
3122 __table_args__ = (
3123 Index('cs_revision_idx', 'revision'),
3123 Index('cs_revision_idx', 'revision'),
3124 Index('cs_version_idx', 'version'),
3124 Index('cs_version_idx', 'version'),
3125 UniqueConstraint('repo_id', 'revision', 'version'),
3125 UniqueConstraint('repo_id', 'revision', 'version'),
3126 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3126 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3127 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3127 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3128 )
3128 )
3129 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3129 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3130 STATUS_APPROVED = 'approved'
3130 STATUS_APPROVED = 'approved'
3131 STATUS_REJECTED = 'rejected'
3131 STATUS_REJECTED = 'rejected'
3132 STATUS_UNDER_REVIEW = 'under_review'
3132 STATUS_UNDER_REVIEW = 'under_review'
3133
3133
3134 STATUSES = [
3134 STATUSES = [
3135 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3135 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3136 (STATUS_APPROVED, _("Approved")),
3136 (STATUS_APPROVED, _("Approved")),
3137 (STATUS_REJECTED, _("Rejected")),
3137 (STATUS_REJECTED, _("Rejected")),
3138 (STATUS_UNDER_REVIEW, _("Under Review")),
3138 (STATUS_UNDER_REVIEW, _("Under Review")),
3139 ]
3139 ]
3140
3140
3141 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3141 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3142 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3142 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3143 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3143 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3144 revision = Column('revision', String(40), nullable=False)
3144 revision = Column('revision', String(40), nullable=False)
3145 status = Column('status', String(128), nullable=False, default=DEFAULT)
3145 status = Column('status', String(128), nullable=False, default=DEFAULT)
3146 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3146 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3147 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3147 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3148 version = Column('version', Integer(), nullable=False, default=0)
3148 version = Column('version', Integer(), nullable=False, default=0)
3149 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3149 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3150
3150
3151 author = relationship('User', lazy='joined')
3151 author = relationship('User', lazy='joined')
3152 repo = relationship('Repository')
3152 repo = relationship('Repository')
3153 comment = relationship('ChangesetComment', lazy='joined')
3153 comment = relationship('ChangesetComment', lazy='joined')
3154 pull_request = relationship('PullRequest', lazy='joined')
3154 pull_request = relationship('PullRequest', lazy='joined')
3155
3155
3156 def __unicode__(self):
3156 def __unicode__(self):
3157 return u"<%s('%s[v%s]:%s')>" % (
3157 return u"<%s('%s[v%s]:%s')>" % (
3158 self.__class__.__name__,
3158 self.__class__.__name__,
3159 self.status, self.version, self.author
3159 self.status, self.version, self.author
3160 )
3160 )
3161
3161
3162 @classmethod
3162 @classmethod
3163 def get_status_lbl(cls, value):
3163 def get_status_lbl(cls, value):
3164 return dict(cls.STATUSES).get(value)
3164 return dict(cls.STATUSES).get(value)
3165
3165
3166 @property
3166 @property
3167 def status_lbl(self):
3167 def status_lbl(self):
3168 return ChangesetStatus.get_status_lbl(self.status)
3168 return ChangesetStatus.get_status_lbl(self.status)
3169
3169
3170
3170
3171 class _PullRequestBase(BaseModel):
3171 class _PullRequestBase(BaseModel):
3172 """
3172 """
3173 Common attributes of pull request and version entries.
3173 Common attributes of pull request and version entries.
3174 """
3174 """
3175
3175
3176 # .status values
3176 # .status values
3177 STATUS_NEW = u'new'
3177 STATUS_NEW = u'new'
3178 STATUS_OPEN = u'open'
3178 STATUS_OPEN = u'open'
3179 STATUS_CLOSED = u'closed'
3179 STATUS_CLOSED = u'closed'
3180
3180
3181 title = Column('title', Unicode(255), nullable=True)
3181 title = Column('title', Unicode(255), nullable=True)
3182 description = Column(
3182 description = Column(
3183 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3183 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3184 nullable=True)
3184 nullable=True)
3185 # new/open/closed status of pull request (not approve/reject/etc)
3185 # new/open/closed status of pull request (not approve/reject/etc)
3186 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3186 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3187 created_on = Column(
3187 created_on = Column(
3188 'created_on', DateTime(timezone=False), nullable=False,
3188 'created_on', DateTime(timezone=False), nullable=False,
3189 default=datetime.datetime.now)
3189 default=datetime.datetime.now)
3190 updated_on = Column(
3190 updated_on = Column(
3191 'updated_on', DateTime(timezone=False), nullable=False,
3191 'updated_on', DateTime(timezone=False), nullable=False,
3192 default=datetime.datetime.now)
3192 default=datetime.datetime.now)
3193
3193
3194 @declared_attr
3194 @declared_attr
3195 def user_id(cls):
3195 def user_id(cls):
3196 return Column(
3196 return Column(
3197 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3197 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3198 unique=None)
3198 unique=None)
3199
3199
3200 # 500 revisions max
3200 # 500 revisions max
3201 _revisions = Column(
3201 _revisions = Column(
3202 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3202 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3203
3203
3204 @declared_attr
3204 @declared_attr
3205 def source_repo_id(cls):
3205 def source_repo_id(cls):
3206 # TODO: dan: rename column to source_repo_id
3206 # TODO: dan: rename column to source_repo_id
3207 return Column(
3207 return Column(
3208 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3208 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3209 nullable=False)
3209 nullable=False)
3210
3210
3211 source_ref = Column('org_ref', Unicode(255), nullable=False)
3211 source_ref = Column('org_ref', Unicode(255), nullable=False)
3212
3212
3213 @declared_attr
3213 @declared_attr
3214 def target_repo_id(cls):
3214 def target_repo_id(cls):
3215 # TODO: dan: rename column to target_repo_id
3215 # TODO: dan: rename column to target_repo_id
3216 return Column(
3216 return Column(
3217 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3217 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3218 nullable=False)
3218 nullable=False)
3219
3219
3220 target_ref = Column('other_ref', Unicode(255), nullable=False)
3220 target_ref = Column('other_ref', Unicode(255), nullable=False)
3221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3222
3222
3223 # TODO: dan: rename column to last_merge_source_rev
3223 # TODO: dan: rename column to last_merge_source_rev
3224 _last_merge_source_rev = Column(
3224 _last_merge_source_rev = Column(
3225 'last_merge_org_rev', String(40), nullable=True)
3225 'last_merge_org_rev', String(40), nullable=True)
3226 # TODO: dan: rename column to last_merge_target_rev
3226 # TODO: dan: rename column to last_merge_target_rev
3227 _last_merge_target_rev = Column(
3227 _last_merge_target_rev = Column(
3228 'last_merge_other_rev', String(40), nullable=True)
3228 'last_merge_other_rev', String(40), nullable=True)
3229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3230 merge_rev = Column('merge_rev', String(40), nullable=True)
3230 merge_rev = Column('merge_rev', String(40), nullable=True)
3231
3231
3232 reviewer_data = Column(
3233 'reviewer_data_json', MutationObj.as_mutable(
3234 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3235
3236 @property
3237 def reviewer_data_json(self):
3238 return json.dumps(self.reviewer_data)
3239
3232 @hybrid_property
3240 @hybrid_property
3233 def revisions(self):
3241 def revisions(self):
3234 return self._revisions.split(':') if self._revisions else []
3242 return self._revisions.split(':') if self._revisions else []
3235
3243
3236 @revisions.setter
3244 @revisions.setter
3237 def revisions(self, val):
3245 def revisions(self, val):
3238 self._revisions = ':'.join(val)
3246 self._revisions = ':'.join(val)
3239
3247
3240 @declared_attr
3248 @declared_attr
3241 def author(cls):
3249 def author(cls):
3242 return relationship('User', lazy='joined')
3250 return relationship('User', lazy='joined')
3243
3251
3244 @declared_attr
3252 @declared_attr
3245 def source_repo(cls):
3253 def source_repo(cls):
3246 return relationship(
3254 return relationship(
3247 'Repository',
3255 'Repository',
3248 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3256 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3249
3257
3250 @property
3258 @property
3251 def source_ref_parts(self):
3259 def source_ref_parts(self):
3252 return self.unicode_to_reference(self.source_ref)
3260 return self.unicode_to_reference(self.source_ref)
3253
3261
3254 @declared_attr
3262 @declared_attr
3255 def target_repo(cls):
3263 def target_repo(cls):
3256 return relationship(
3264 return relationship(
3257 'Repository',
3265 'Repository',
3258 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3266 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3259
3267
3260 @property
3268 @property
3261 def target_ref_parts(self):
3269 def target_ref_parts(self):
3262 return self.unicode_to_reference(self.target_ref)
3270 return self.unicode_to_reference(self.target_ref)
3263
3271
3264 @property
3272 @property
3265 def shadow_merge_ref(self):
3273 def shadow_merge_ref(self):
3266 return self.unicode_to_reference(self._shadow_merge_ref)
3274 return self.unicode_to_reference(self._shadow_merge_ref)
3267
3275
3268 @shadow_merge_ref.setter
3276 @shadow_merge_ref.setter
3269 def shadow_merge_ref(self, ref):
3277 def shadow_merge_ref(self, ref):
3270 self._shadow_merge_ref = self.reference_to_unicode(ref)
3278 self._shadow_merge_ref = self.reference_to_unicode(ref)
3271
3279
3272 def unicode_to_reference(self, raw):
3280 def unicode_to_reference(self, raw):
3273 """
3281 """
3274 Convert a unicode (or string) to a reference object.
3282 Convert a unicode (or string) to a reference object.
3275 If unicode evaluates to False it returns None.
3283 If unicode evaluates to False it returns None.
3276 """
3284 """
3277 if raw:
3285 if raw:
3278 refs = raw.split(':')
3286 refs = raw.split(':')
3279 return Reference(*refs)
3287 return Reference(*refs)
3280 else:
3288 else:
3281 return None
3289 return None
3282
3290
3283 def reference_to_unicode(self, ref):
3291 def reference_to_unicode(self, ref):
3284 """
3292 """
3285 Convert a reference object to unicode.
3293 Convert a reference object to unicode.
3286 If reference is None it returns None.
3294 If reference is None it returns None.
3287 """
3295 """
3288 if ref:
3296 if ref:
3289 return u':'.join(ref)
3297 return u':'.join(ref)
3290 else:
3298 else:
3291 return None
3299 return None
3292
3300
3293 def get_api_data(self):
3301 def get_api_data(self):
3294 from rhodecode.model.pull_request import PullRequestModel
3302 from rhodecode.model.pull_request import PullRequestModel
3295 pull_request = self
3303 pull_request = self
3296 merge_status = PullRequestModel().merge_status(pull_request)
3304 merge_status = PullRequestModel().merge_status(pull_request)
3297
3305
3298 pull_request_url = url(
3306 pull_request_url = url(
3299 'pullrequest_show', repo_name=self.target_repo.repo_name,
3307 'pullrequest_show', repo_name=self.target_repo.repo_name,
3300 pull_request_id=self.pull_request_id, qualified=True)
3308 pull_request_id=self.pull_request_id, qualified=True)
3301
3309
3302 merge_data = {
3310 merge_data = {
3303 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3311 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3304 'reference': (
3312 'reference': (
3305 pull_request.shadow_merge_ref._asdict()
3313 pull_request.shadow_merge_ref._asdict()
3306 if pull_request.shadow_merge_ref else None),
3314 if pull_request.shadow_merge_ref else None),
3307 }
3315 }
3308
3316
3309 data = {
3317 data = {
3310 'pull_request_id': pull_request.pull_request_id,
3318 'pull_request_id': pull_request.pull_request_id,
3311 'url': pull_request_url,
3319 'url': pull_request_url,
3312 'title': pull_request.title,
3320 'title': pull_request.title,
3313 'description': pull_request.description,
3321 'description': pull_request.description,
3314 'status': pull_request.status,
3322 'status': pull_request.status,
3315 'created_on': pull_request.created_on,
3323 'created_on': pull_request.created_on,
3316 'updated_on': pull_request.updated_on,
3324 'updated_on': pull_request.updated_on,
3317 'commit_ids': pull_request.revisions,
3325 'commit_ids': pull_request.revisions,
3318 'review_status': pull_request.calculated_review_status(),
3326 'review_status': pull_request.calculated_review_status(),
3319 'mergeable': {
3327 'mergeable': {
3320 'status': merge_status[0],
3328 'status': merge_status[0],
3321 'message': unicode(merge_status[1]),
3329 'message': unicode(merge_status[1]),
3322 },
3330 },
3323 'source': {
3331 'source': {
3324 'clone_url': pull_request.source_repo.clone_url(),
3332 'clone_url': pull_request.source_repo.clone_url(),
3325 'repository': pull_request.source_repo.repo_name,
3333 'repository': pull_request.source_repo.repo_name,
3326 'reference': {
3334 'reference': {
3327 'name': pull_request.source_ref_parts.name,
3335 'name': pull_request.source_ref_parts.name,
3328 'type': pull_request.source_ref_parts.type,
3336 'type': pull_request.source_ref_parts.type,
3329 'commit_id': pull_request.source_ref_parts.commit_id,
3337 'commit_id': pull_request.source_ref_parts.commit_id,
3330 },
3338 },
3331 },
3339 },
3332 'target': {
3340 'target': {
3333 'clone_url': pull_request.target_repo.clone_url(),
3341 'clone_url': pull_request.target_repo.clone_url(),
3334 'repository': pull_request.target_repo.repo_name,
3342 'repository': pull_request.target_repo.repo_name,
3335 'reference': {
3343 'reference': {
3336 'name': pull_request.target_ref_parts.name,
3344 'name': pull_request.target_ref_parts.name,
3337 'type': pull_request.target_ref_parts.type,
3345 'type': pull_request.target_ref_parts.type,
3338 'commit_id': pull_request.target_ref_parts.commit_id,
3346 'commit_id': pull_request.target_ref_parts.commit_id,
3339 },
3347 },
3340 },
3348 },
3341 'merge': merge_data,
3349 'merge': merge_data,
3342 'author': pull_request.author.get_api_data(include_secrets=False,
3350 'author': pull_request.author.get_api_data(include_secrets=False,
3343 details='basic'),
3351 details='basic'),
3344 'reviewers': [
3352 'reviewers': [
3345 {
3353 {
3346 'user': reviewer.get_api_data(include_secrets=False,
3354 'user': reviewer.get_api_data(include_secrets=False,
3347 details='basic'),
3355 details='basic'),
3348 'reasons': reasons,
3356 'reasons': reasons,
3349 'review_status': st[0][1].status if st else 'not_reviewed',
3357 'review_status': st[0][1].status if st else 'not_reviewed',
3350 }
3358 }
3351 for reviewer, reasons, st in pull_request.reviewers_statuses()
3359 for reviewer, reasons, mandatory, st in
3360 pull_request.reviewers_statuses()
3352 ]
3361 ]
3353 }
3362 }
3354
3363
3355 return data
3364 return data
3356
3365
3357
3366
3358 class PullRequest(Base, _PullRequestBase):
3367 class PullRequest(Base, _PullRequestBase):
3359 __tablename__ = 'pull_requests'
3368 __tablename__ = 'pull_requests'
3360 __table_args__ = (
3369 __table_args__ = (
3361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3362 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3363 )
3372 )
3364
3373
3365 pull_request_id = Column(
3374 pull_request_id = Column(
3366 'pull_request_id', Integer(), nullable=False, primary_key=True)
3375 'pull_request_id', Integer(), nullable=False, primary_key=True)
3367
3376
3368 def __repr__(self):
3377 def __repr__(self):
3369 if self.pull_request_id:
3378 if self.pull_request_id:
3370 return '<DB:PullRequest #%s>' % self.pull_request_id
3379 return '<DB:PullRequest #%s>' % self.pull_request_id
3371 else:
3380 else:
3372 return '<DB:PullRequest at %#x>' % id(self)
3381 return '<DB:PullRequest at %#x>' % id(self)
3373
3382
3374 reviewers = relationship('PullRequestReviewers',
3383 reviewers = relationship('PullRequestReviewers',
3375 cascade="all, delete, delete-orphan")
3384 cascade="all, delete, delete-orphan")
3376 statuses = relationship('ChangesetStatus')
3385 statuses = relationship('ChangesetStatus')
3377 comments = relationship('ChangesetComment',
3386 comments = relationship('ChangesetComment',
3378 cascade="all, delete, delete-orphan")
3387 cascade="all, delete, delete-orphan")
3379 versions = relationship('PullRequestVersion',
3388 versions = relationship('PullRequestVersion',
3380 cascade="all, delete, delete-orphan",
3389 cascade="all, delete, delete-orphan",
3381 lazy='dynamic')
3390 lazy='dynamic')
3382
3391
3383 @classmethod
3392 @classmethod
3384 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3393 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3385 internal_methods=None):
3394 internal_methods=None):
3386
3395
3387 class PullRequestDisplay(object):
3396 class PullRequestDisplay(object):
3388 """
3397 """
3389 Special object wrapper for showing PullRequest data via Versions
3398 Special object wrapper for showing PullRequest data via Versions
3390 It mimics PR object as close as possible. This is read only object
3399 It mimics PR object as close as possible. This is read only object
3391 just for display
3400 just for display
3392 """
3401 """
3393
3402
3394 def __init__(self, attrs, internal=None):
3403 def __init__(self, attrs, internal=None):
3395 self.attrs = attrs
3404 self.attrs = attrs
3396 # internal have priority over the given ones via attrs
3405 # internal have priority over the given ones via attrs
3397 self.internal = internal or ['versions']
3406 self.internal = internal or ['versions']
3398
3407
3399 def __getattr__(self, item):
3408 def __getattr__(self, item):
3400 if item in self.internal:
3409 if item in self.internal:
3401 return getattr(self, item)
3410 return getattr(self, item)
3402 try:
3411 try:
3403 return self.attrs[item]
3412 return self.attrs[item]
3404 except KeyError:
3413 except KeyError:
3405 raise AttributeError(
3414 raise AttributeError(
3406 '%s object has no attribute %s' % (self, item))
3415 '%s object has no attribute %s' % (self, item))
3407
3416
3408 def __repr__(self):
3417 def __repr__(self):
3409 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3418 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3410
3419
3411 def versions(self):
3420 def versions(self):
3412 return pull_request_obj.versions.order_by(
3421 return pull_request_obj.versions.order_by(
3413 PullRequestVersion.pull_request_version_id).all()
3422 PullRequestVersion.pull_request_version_id).all()
3414
3423
3415 def is_closed(self):
3424 def is_closed(self):
3416 return pull_request_obj.is_closed()
3425 return pull_request_obj.is_closed()
3417
3426
3418 @property
3427 @property
3419 def pull_request_version_id(self):
3428 def pull_request_version_id(self):
3420 return getattr(pull_request_obj, 'pull_request_version_id', None)
3429 return getattr(pull_request_obj, 'pull_request_version_id', None)
3421
3430
3422 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3431 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3423
3432
3424 attrs.author = StrictAttributeDict(
3433 attrs.author = StrictAttributeDict(
3425 pull_request_obj.author.get_api_data())
3434 pull_request_obj.author.get_api_data())
3426 if pull_request_obj.target_repo:
3435 if pull_request_obj.target_repo:
3427 attrs.target_repo = StrictAttributeDict(
3436 attrs.target_repo = StrictAttributeDict(
3428 pull_request_obj.target_repo.get_api_data())
3437 pull_request_obj.target_repo.get_api_data())
3429 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3438 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3430
3439
3431 if pull_request_obj.source_repo:
3440 if pull_request_obj.source_repo:
3432 attrs.source_repo = StrictAttributeDict(
3441 attrs.source_repo = StrictAttributeDict(
3433 pull_request_obj.source_repo.get_api_data())
3442 pull_request_obj.source_repo.get_api_data())
3434 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3443 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3435
3444
3436 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3445 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3437 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3446 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3438 attrs.revisions = pull_request_obj.revisions
3447 attrs.revisions = pull_request_obj.revisions
3439
3448
3440 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3449 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3450 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3451 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3441
3452
3442 return PullRequestDisplay(attrs, internal=internal_methods)
3453 return PullRequestDisplay(attrs, internal=internal_methods)
3443
3454
3444 def is_closed(self):
3455 def is_closed(self):
3445 return self.status == self.STATUS_CLOSED
3456 return self.status == self.STATUS_CLOSED
3446
3457
3447 def __json__(self):
3458 def __json__(self):
3448 return {
3459 return {
3449 'revisions': self.revisions,
3460 'revisions': self.revisions,
3450 }
3461 }
3451
3462
3452 def calculated_review_status(self):
3463 def calculated_review_status(self):
3453 from rhodecode.model.changeset_status import ChangesetStatusModel
3464 from rhodecode.model.changeset_status import ChangesetStatusModel
3454 return ChangesetStatusModel().calculated_review_status(self)
3465 return ChangesetStatusModel().calculated_review_status(self)
3455
3466
3456 def reviewers_statuses(self):
3467 def reviewers_statuses(self):
3457 from rhodecode.model.changeset_status import ChangesetStatusModel
3468 from rhodecode.model.changeset_status import ChangesetStatusModel
3458 return ChangesetStatusModel().reviewers_statuses(self)
3469 return ChangesetStatusModel().reviewers_statuses(self)
3459
3470
3460 @property
3471 @property
3461 def workspace_id(self):
3472 def workspace_id(self):
3462 from rhodecode.model.pull_request import PullRequestModel
3473 from rhodecode.model.pull_request import PullRequestModel
3463 return PullRequestModel()._workspace_id(self)
3474 return PullRequestModel()._workspace_id(self)
3464
3475
3465 def get_shadow_repo(self):
3476 def get_shadow_repo(self):
3466 workspace_id = self.workspace_id
3477 workspace_id = self.workspace_id
3467 vcs_obj = self.target_repo.scm_instance()
3478 vcs_obj = self.target_repo.scm_instance()
3468 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3479 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3469 workspace_id)
3480 workspace_id)
3470 return vcs_obj._get_shadow_instance(shadow_repository_path)
3481 return vcs_obj._get_shadow_instance(shadow_repository_path)
3471
3482
3472
3483
3473 class PullRequestVersion(Base, _PullRequestBase):
3484 class PullRequestVersion(Base, _PullRequestBase):
3474 __tablename__ = 'pull_request_versions'
3485 __tablename__ = 'pull_request_versions'
3475 __table_args__ = (
3486 __table_args__ = (
3476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3487 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3488 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3478 )
3489 )
3479
3490
3480 pull_request_version_id = Column(
3491 pull_request_version_id = Column(
3481 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3492 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3482 pull_request_id = Column(
3493 pull_request_id = Column(
3483 'pull_request_id', Integer(),
3494 'pull_request_id', Integer(),
3484 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3495 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3485 pull_request = relationship('PullRequest')
3496 pull_request = relationship('PullRequest')
3486
3497
3487 def __repr__(self):
3498 def __repr__(self):
3488 if self.pull_request_version_id:
3499 if self.pull_request_version_id:
3489 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3500 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3490 else:
3501 else:
3491 return '<DB:PullRequestVersion at %#x>' % id(self)
3502 return '<DB:PullRequestVersion at %#x>' % id(self)
3492
3503
3493 @property
3504 @property
3494 def reviewers(self):
3505 def reviewers(self):
3495 return self.pull_request.reviewers
3506 return self.pull_request.reviewers
3496
3507
3497 @property
3508 @property
3498 def versions(self):
3509 def versions(self):
3499 return self.pull_request.versions
3510 return self.pull_request.versions
3500
3511
3501 def is_closed(self):
3512 def is_closed(self):
3502 # calculate from original
3513 # calculate from original
3503 return self.pull_request.status == self.STATUS_CLOSED
3514 return self.pull_request.status == self.STATUS_CLOSED
3504
3515
3505 def calculated_review_status(self):
3516 def calculated_review_status(self):
3506 return self.pull_request.calculated_review_status()
3517 return self.pull_request.calculated_review_status()
3507
3518
3508 def reviewers_statuses(self):
3519 def reviewers_statuses(self):
3509 return self.pull_request.reviewers_statuses()
3520 return self.pull_request.reviewers_statuses()
3510
3521
3511
3522
3512 class PullRequestReviewers(Base, BaseModel):
3523 class PullRequestReviewers(Base, BaseModel):
3513 __tablename__ = 'pull_request_reviewers'
3524 __tablename__ = 'pull_request_reviewers'
3514 __table_args__ = (
3525 __table_args__ = (
3515 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3516 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3517 )
3528 )
3518
3529
3519 def __init__(self, user=None, pull_request=None, reasons=None):
3520 self.user = user
3521 self.pull_request = pull_request
3522 self.reasons = reasons or []
3523
3524 @hybrid_property
3530 @hybrid_property
3525 def reasons(self):
3531 def reasons(self):
3526 if not self._reasons:
3532 if not self._reasons:
3527 return []
3533 return []
3528 return self._reasons
3534 return self._reasons
3529
3535
3530 @reasons.setter
3536 @reasons.setter
3531 def reasons(self, val):
3537 def reasons(self, val):
3532 val = val or []
3538 val = val or []
3533 if any(not isinstance(x, basestring) for x in val):
3539 if any(not isinstance(x, basestring) for x in val):
3534 raise Exception('invalid reasons type, must be list of strings')
3540 raise Exception('invalid reasons type, must be list of strings')
3535 self._reasons = val
3541 self._reasons = val
3536
3542
3537 pull_requests_reviewers_id = Column(
3543 pull_requests_reviewers_id = Column(
3538 'pull_requests_reviewers_id', Integer(), nullable=False,
3544 'pull_requests_reviewers_id', Integer(), nullable=False,
3539 primary_key=True)
3545 primary_key=True)
3540 pull_request_id = Column(
3546 pull_request_id = Column(
3541 "pull_request_id", Integer(),
3547 "pull_request_id", Integer(),
3542 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3548 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3543 user_id = Column(
3549 user_id = Column(
3544 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3550 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3545 _reasons = Column(
3551 _reasons = Column(
3546 'reason', MutationList.as_mutable(
3552 'reason', MutationList.as_mutable(
3547 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3553 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3548
3554 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3549 user = relationship('User')
3555 user = relationship('User')
3550 pull_request = relationship('PullRequest')
3556 pull_request = relationship('PullRequest')
3551
3557
3552
3558
3553 class Notification(Base, BaseModel):
3559 class Notification(Base, BaseModel):
3554 __tablename__ = 'notifications'
3560 __tablename__ = 'notifications'
3555 __table_args__ = (
3561 __table_args__ = (
3556 Index('notification_type_idx', 'type'),
3562 Index('notification_type_idx', 'type'),
3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3563 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3564 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3559 )
3565 )
3560
3566
3561 TYPE_CHANGESET_COMMENT = u'cs_comment'
3567 TYPE_CHANGESET_COMMENT = u'cs_comment'
3562 TYPE_MESSAGE = u'message'
3568 TYPE_MESSAGE = u'message'
3563 TYPE_MENTION = u'mention'
3569 TYPE_MENTION = u'mention'
3564 TYPE_REGISTRATION = u'registration'
3570 TYPE_REGISTRATION = u'registration'
3565 TYPE_PULL_REQUEST = u'pull_request'
3571 TYPE_PULL_REQUEST = u'pull_request'
3566 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3572 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3567
3573
3568 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3574 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3569 subject = Column('subject', Unicode(512), nullable=True)
3575 subject = Column('subject', Unicode(512), nullable=True)
3570 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3576 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3571 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3577 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3572 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3578 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3573 type_ = Column('type', Unicode(255))
3579 type_ = Column('type', Unicode(255))
3574
3580
3575 created_by_user = relationship('User')
3581 created_by_user = relationship('User')
3576 notifications_to_users = relationship('UserNotification', lazy='joined',
3582 notifications_to_users = relationship('UserNotification', lazy='joined',
3577 cascade="all, delete, delete-orphan")
3583 cascade="all, delete, delete-orphan")
3578
3584
3579 @property
3585 @property
3580 def recipients(self):
3586 def recipients(self):
3581 return [x.user for x in UserNotification.query()\
3587 return [x.user for x in UserNotification.query()\
3582 .filter(UserNotification.notification == self)\
3588 .filter(UserNotification.notification == self)\
3583 .order_by(UserNotification.user_id.asc()).all()]
3589 .order_by(UserNotification.user_id.asc()).all()]
3584
3590
3585 @classmethod
3591 @classmethod
3586 def create(cls, created_by, subject, body, recipients, type_=None):
3592 def create(cls, created_by, subject, body, recipients, type_=None):
3587 if type_ is None:
3593 if type_ is None:
3588 type_ = Notification.TYPE_MESSAGE
3594 type_ = Notification.TYPE_MESSAGE
3589
3595
3590 notification = cls()
3596 notification = cls()
3591 notification.created_by_user = created_by
3597 notification.created_by_user = created_by
3592 notification.subject = subject
3598 notification.subject = subject
3593 notification.body = body
3599 notification.body = body
3594 notification.type_ = type_
3600 notification.type_ = type_
3595 notification.created_on = datetime.datetime.now()
3601 notification.created_on = datetime.datetime.now()
3596
3602
3597 for u in recipients:
3603 for u in recipients:
3598 assoc = UserNotification()
3604 assoc = UserNotification()
3599 assoc.notification = notification
3605 assoc.notification = notification
3600
3606
3601 # if created_by is inside recipients mark his notification
3607 # if created_by is inside recipients mark his notification
3602 # as read
3608 # as read
3603 if u.user_id == created_by.user_id:
3609 if u.user_id == created_by.user_id:
3604 assoc.read = True
3610 assoc.read = True
3605
3611
3606 u.notifications.append(assoc)
3612 u.notifications.append(assoc)
3607 Session().add(notification)
3613 Session().add(notification)
3608
3614
3609 return notification
3615 return notification
3610
3616
3611 @property
3617 @property
3612 def description(self):
3618 def description(self):
3613 from rhodecode.model.notification import NotificationModel
3619 from rhodecode.model.notification import NotificationModel
3614 return NotificationModel().make_description(self)
3620 return NotificationModel().make_description(self)
3615
3621
3616
3622
3617 class UserNotification(Base, BaseModel):
3623 class UserNotification(Base, BaseModel):
3618 __tablename__ = 'user_to_notification'
3624 __tablename__ = 'user_to_notification'
3619 __table_args__ = (
3625 __table_args__ = (
3620 UniqueConstraint('user_id', 'notification_id'),
3626 UniqueConstraint('user_id', 'notification_id'),
3621 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3627 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3622 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3628 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3623 )
3629 )
3624 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3630 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3625 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3631 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3626 read = Column('read', Boolean, default=False)
3632 read = Column('read', Boolean, default=False)
3627 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3633 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3628
3634
3629 user = relationship('User', lazy="joined")
3635 user = relationship('User', lazy="joined")
3630 notification = relationship('Notification', lazy="joined",
3636 notification = relationship('Notification', lazy="joined",
3631 order_by=lambda: Notification.created_on.desc(),)
3637 order_by=lambda: Notification.created_on.desc(),)
3632
3638
3633 def mark_as_read(self):
3639 def mark_as_read(self):
3634 self.read = True
3640 self.read = True
3635 Session().add(self)
3641 Session().add(self)
3636
3642
3637
3643
3638 class Gist(Base, BaseModel):
3644 class Gist(Base, BaseModel):
3639 __tablename__ = 'gists'
3645 __tablename__ = 'gists'
3640 __table_args__ = (
3646 __table_args__ = (
3641 Index('g_gist_access_id_idx', 'gist_access_id'),
3647 Index('g_gist_access_id_idx', 'gist_access_id'),
3642 Index('g_created_on_idx', 'created_on'),
3648 Index('g_created_on_idx', 'created_on'),
3643 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3644 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3645 )
3651 )
3646 GIST_PUBLIC = u'public'
3652 GIST_PUBLIC = u'public'
3647 GIST_PRIVATE = u'private'
3653 GIST_PRIVATE = u'private'
3648 DEFAULT_FILENAME = u'gistfile1.txt'
3654 DEFAULT_FILENAME = u'gistfile1.txt'
3649
3655
3650 ACL_LEVEL_PUBLIC = u'acl_public'
3656 ACL_LEVEL_PUBLIC = u'acl_public'
3651 ACL_LEVEL_PRIVATE = u'acl_private'
3657 ACL_LEVEL_PRIVATE = u'acl_private'
3652
3658
3653 gist_id = Column('gist_id', Integer(), primary_key=True)
3659 gist_id = Column('gist_id', Integer(), primary_key=True)
3654 gist_access_id = Column('gist_access_id', Unicode(250))
3660 gist_access_id = Column('gist_access_id', Unicode(250))
3655 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3661 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3656 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3662 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3657 gist_expires = Column('gist_expires', Float(53), nullable=False)
3663 gist_expires = Column('gist_expires', Float(53), nullable=False)
3658 gist_type = Column('gist_type', Unicode(128), nullable=False)
3664 gist_type = Column('gist_type', Unicode(128), nullable=False)
3659 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3665 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3660 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3666 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3661 acl_level = Column('acl_level', Unicode(128), nullable=True)
3667 acl_level = Column('acl_level', Unicode(128), nullable=True)
3662
3668
3663 owner = relationship('User')
3669 owner = relationship('User')
3664
3670
3665 def __repr__(self):
3671 def __repr__(self):
3666 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3672 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3667
3673
3668 @classmethod
3674 @classmethod
3669 def get_or_404(cls, id_, pyramid_exc=False):
3675 def get_or_404(cls, id_, pyramid_exc=False):
3670
3676
3671 if pyramid_exc:
3677 if pyramid_exc:
3672 from pyramid.httpexceptions import HTTPNotFound
3678 from pyramid.httpexceptions import HTTPNotFound
3673 else:
3679 else:
3674 from webob.exc import HTTPNotFound
3680 from webob.exc import HTTPNotFound
3675
3681
3676 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3682 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3677 if not res:
3683 if not res:
3678 raise HTTPNotFound
3684 raise HTTPNotFound
3679 return res
3685 return res
3680
3686
3681 @classmethod
3687 @classmethod
3682 def get_by_access_id(cls, gist_access_id):
3688 def get_by_access_id(cls, gist_access_id):
3683 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3689 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3684
3690
3685 def gist_url(self):
3691 def gist_url(self):
3686 import rhodecode
3692 import rhodecode
3687 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3693 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3688 if alias_url:
3694 if alias_url:
3689 return alias_url.replace('{gistid}', self.gist_access_id)
3695 return alias_url.replace('{gistid}', self.gist_access_id)
3690
3696
3691 return url('gist', gist_id=self.gist_access_id, qualified=True)
3697 return url('gist', gist_id=self.gist_access_id, qualified=True)
3692
3698
3693 @classmethod
3699 @classmethod
3694 def base_path(cls):
3700 def base_path(cls):
3695 """
3701 """
3696 Returns base path when all gists are stored
3702 Returns base path when all gists are stored
3697
3703
3698 :param cls:
3704 :param cls:
3699 """
3705 """
3700 from rhodecode.model.gist import GIST_STORE_LOC
3706 from rhodecode.model.gist import GIST_STORE_LOC
3701 q = Session().query(RhodeCodeUi)\
3707 q = Session().query(RhodeCodeUi)\
3702 .filter(RhodeCodeUi.ui_key == URL_SEP)
3708 .filter(RhodeCodeUi.ui_key == URL_SEP)
3703 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3709 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3704 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3710 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3705
3711
3706 def get_api_data(self):
3712 def get_api_data(self):
3707 """
3713 """
3708 Common function for generating gist related data for API
3714 Common function for generating gist related data for API
3709 """
3715 """
3710 gist = self
3716 gist = self
3711 data = {
3717 data = {
3712 'gist_id': gist.gist_id,
3718 'gist_id': gist.gist_id,
3713 'type': gist.gist_type,
3719 'type': gist.gist_type,
3714 'access_id': gist.gist_access_id,
3720 'access_id': gist.gist_access_id,
3715 'description': gist.gist_description,
3721 'description': gist.gist_description,
3716 'url': gist.gist_url(),
3722 'url': gist.gist_url(),
3717 'expires': gist.gist_expires,
3723 'expires': gist.gist_expires,
3718 'created_on': gist.created_on,
3724 'created_on': gist.created_on,
3719 'modified_at': gist.modified_at,
3725 'modified_at': gist.modified_at,
3720 'content': None,
3726 'content': None,
3721 'acl_level': gist.acl_level,
3727 'acl_level': gist.acl_level,
3722 }
3728 }
3723 return data
3729 return data
3724
3730
3725 def __json__(self):
3731 def __json__(self):
3726 data = dict(
3732 data = dict(
3727 )
3733 )
3728 data.update(self.get_api_data())
3734 data.update(self.get_api_data())
3729 return data
3735 return data
3730 # SCM functions
3736 # SCM functions
3731
3737
3732 def scm_instance(self, **kwargs):
3738 def scm_instance(self, **kwargs):
3733 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3739 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3734 return get_vcs_instance(
3740 return get_vcs_instance(
3735 repo_path=safe_str(full_repo_path), create=False)
3741 repo_path=safe_str(full_repo_path), create=False)
3736
3742
3737
3743
3738 class ExternalIdentity(Base, BaseModel):
3744 class ExternalIdentity(Base, BaseModel):
3739 __tablename__ = 'external_identities'
3745 __tablename__ = 'external_identities'
3740 __table_args__ = (
3746 __table_args__ = (
3741 Index('local_user_id_idx', 'local_user_id'),
3747 Index('local_user_id_idx', 'local_user_id'),
3742 Index('external_id_idx', 'external_id'),
3748 Index('external_id_idx', 'external_id'),
3743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3744 'mysql_charset': 'utf8'})
3750 'mysql_charset': 'utf8'})
3745
3751
3746 external_id = Column('external_id', Unicode(255), default=u'',
3752 external_id = Column('external_id', Unicode(255), default=u'',
3747 primary_key=True)
3753 primary_key=True)
3748 external_username = Column('external_username', Unicode(1024), default=u'')
3754 external_username = Column('external_username', Unicode(1024), default=u'')
3749 local_user_id = Column('local_user_id', Integer(),
3755 local_user_id = Column('local_user_id', Integer(),
3750 ForeignKey('users.user_id'), primary_key=True)
3756 ForeignKey('users.user_id'), primary_key=True)
3751 provider_name = Column('provider_name', Unicode(255), default=u'',
3757 provider_name = Column('provider_name', Unicode(255), default=u'',
3752 primary_key=True)
3758 primary_key=True)
3753 access_token = Column('access_token', String(1024), default=u'')
3759 access_token = Column('access_token', String(1024), default=u'')
3754 alt_token = Column('alt_token', String(1024), default=u'')
3760 alt_token = Column('alt_token', String(1024), default=u'')
3755 token_secret = Column('token_secret', String(1024), default=u'')
3761 token_secret = Column('token_secret', String(1024), default=u'')
3756
3762
3757 @classmethod
3763 @classmethod
3758 def by_external_id_and_provider(cls, external_id, provider_name,
3764 def by_external_id_and_provider(cls, external_id, provider_name,
3759 local_user_id=None):
3765 local_user_id=None):
3760 """
3766 """
3761 Returns ExternalIdentity instance based on search params
3767 Returns ExternalIdentity instance based on search params
3762
3768
3763 :param external_id:
3769 :param external_id:
3764 :param provider_name:
3770 :param provider_name:
3765 :return: ExternalIdentity
3771 :return: ExternalIdentity
3766 """
3772 """
3767 query = cls.query()
3773 query = cls.query()
3768 query = query.filter(cls.external_id == external_id)
3774 query = query.filter(cls.external_id == external_id)
3769 query = query.filter(cls.provider_name == provider_name)
3775 query = query.filter(cls.provider_name == provider_name)
3770 if local_user_id:
3776 if local_user_id:
3771 query = query.filter(cls.local_user_id == local_user_id)
3777 query = query.filter(cls.local_user_id == local_user_id)
3772 return query.first()
3778 return query.first()
3773
3779
3774 @classmethod
3780 @classmethod
3775 def user_by_external_id_and_provider(cls, external_id, provider_name):
3781 def user_by_external_id_and_provider(cls, external_id, provider_name):
3776 """
3782 """
3777 Returns User instance based on search params
3783 Returns User instance based on search params
3778
3784
3779 :param external_id:
3785 :param external_id:
3780 :param provider_name:
3786 :param provider_name:
3781 :return: User
3787 :return: User
3782 """
3788 """
3783 query = User.query()
3789 query = User.query()
3784 query = query.filter(cls.external_id == external_id)
3790 query = query.filter(cls.external_id == external_id)
3785 query = query.filter(cls.provider_name == provider_name)
3791 query = query.filter(cls.provider_name == provider_name)
3786 query = query.filter(User.user_id == cls.local_user_id)
3792 query = query.filter(User.user_id == cls.local_user_id)
3787 return query.first()
3793 return query.first()
3788
3794
3789 @classmethod
3795 @classmethod
3790 def by_local_user_id(cls, local_user_id):
3796 def by_local_user_id(cls, local_user_id):
3791 """
3797 """
3792 Returns all tokens for user
3798 Returns all tokens for user
3793
3799
3794 :param local_user_id:
3800 :param local_user_id:
3795 :return: ExternalIdentity
3801 :return: ExternalIdentity
3796 """
3802 """
3797 query = cls.query()
3803 query = cls.query()
3798 query = query.filter(cls.local_user_id == local_user_id)
3804 query = query.filter(cls.local_user_id == local_user_id)
3799 return query
3805 return query
3800
3806
3801
3807
3802 class Integration(Base, BaseModel):
3808 class Integration(Base, BaseModel):
3803 __tablename__ = 'integrations'
3809 __tablename__ = 'integrations'
3804 __table_args__ = (
3810 __table_args__ = (
3805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3812 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3807 )
3813 )
3808
3814
3809 integration_id = Column('integration_id', Integer(), primary_key=True)
3815 integration_id = Column('integration_id', Integer(), primary_key=True)
3810 integration_type = Column('integration_type', String(255))
3816 integration_type = Column('integration_type', String(255))
3811 enabled = Column('enabled', Boolean(), nullable=False)
3817 enabled = Column('enabled', Boolean(), nullable=False)
3812 name = Column('name', String(255), nullable=False)
3818 name = Column('name', String(255), nullable=False)
3813 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3819 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3814 default=False)
3820 default=False)
3815
3821
3816 settings = Column(
3822 settings = Column(
3817 'settings_json', MutationObj.as_mutable(
3823 'settings_json', MutationObj.as_mutable(
3818 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3824 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3819 repo_id = Column(
3825 repo_id = Column(
3820 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3826 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3821 nullable=True, unique=None, default=None)
3827 nullable=True, unique=None, default=None)
3822 repo = relationship('Repository', lazy='joined')
3828 repo = relationship('Repository', lazy='joined')
3823
3829
3824 repo_group_id = Column(
3830 repo_group_id = Column(
3825 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3831 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3826 nullable=True, unique=None, default=None)
3832 nullable=True, unique=None, default=None)
3827 repo_group = relationship('RepoGroup', lazy='joined')
3833 repo_group = relationship('RepoGroup', lazy='joined')
3828
3834
3829 @property
3835 @property
3830 def scope(self):
3836 def scope(self):
3831 if self.repo:
3837 if self.repo:
3832 return repr(self.repo)
3838 return repr(self.repo)
3833 if self.repo_group:
3839 if self.repo_group:
3834 if self.child_repos_only:
3840 if self.child_repos_only:
3835 return repr(self.repo_group) + ' (child repos only)'
3841 return repr(self.repo_group) + ' (child repos only)'
3836 else:
3842 else:
3837 return repr(self.repo_group) + ' (recursive)'
3843 return repr(self.repo_group) + ' (recursive)'
3838 if self.child_repos_only:
3844 if self.child_repos_only:
3839 return 'root_repos'
3845 return 'root_repos'
3840 return 'global'
3846 return 'global'
3841
3847
3842 def __repr__(self):
3848 def __repr__(self):
3843 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3849 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3844
3850
3845
3851
3846 class RepoReviewRuleUser(Base, BaseModel):
3852 class RepoReviewRuleUser(Base, BaseModel):
3847 __tablename__ = 'repo_review_rules_users'
3853 __tablename__ = 'repo_review_rules_users'
3848 __table_args__ = (
3854 __table_args__ = (
3849 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3850 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3856 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3851 )
3857 )
3852 repo_review_rule_user_id = Column(
3858 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3853 'repo_review_rule_user_id', Integer(), primary_key=True)
3859 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3854 repo_review_rule_id = Column("repo_review_rule_id",
3860 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3855 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3861 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3857 nullable=False)
3858 user = relationship('User')
3862 user = relationship('User')
3859
3863
3864 def rule_data(self):
3865 return {
3866 'mandatory': self.mandatory
3867 }
3868
3860
3869
3861 class RepoReviewRuleUserGroup(Base, BaseModel):
3870 class RepoReviewRuleUserGroup(Base, BaseModel):
3862 __tablename__ = 'repo_review_rules_users_groups'
3871 __tablename__ = 'repo_review_rules_users_groups'
3863 __table_args__ = (
3872 __table_args__ = (
3864 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3873 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3865 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3874 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3866 )
3875 )
3867 repo_review_rule_users_group_id = Column(
3876 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3868 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3877 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3869 repo_review_rule_id = Column("repo_review_rule_id",
3878 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3870 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3879 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3871 users_group_id = Column("users_group_id", Integer(),
3872 ForeignKey('users_groups.users_group_id'), nullable=False)
3873 users_group = relationship('UserGroup')
3880 users_group = relationship('UserGroup')
3874
3881
3882 def rule_data(self):
3883 return {
3884 'mandatory': self.mandatory
3885 }
3886
3875
3887
3876 class RepoReviewRule(Base, BaseModel):
3888 class RepoReviewRule(Base, BaseModel):
3877 __tablename__ = 'repo_review_rules'
3889 __tablename__ = 'repo_review_rules'
3878 __table_args__ = (
3890 __table_args__ = (
3879 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3891 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3880 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3892 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3881 )
3893 )
3882
3894
3883 repo_review_rule_id = Column(
3895 repo_review_rule_id = Column(
3884 'repo_review_rule_id', Integer(), primary_key=True)
3896 'repo_review_rule_id', Integer(), primary_key=True)
3885 repo_id = Column(
3897 repo_id = Column(
3886 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3898 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3887 repo = relationship('Repository', backref='review_rules')
3899 repo = relationship('Repository', backref='review_rules')
3888
3900
3889 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3901 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3890 default=u'*') # glob
3902 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3891 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3903
3892 default=u'*') # glob
3904 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3893
3905 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3894 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3906 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3895 nullable=False, default=False)
3907
3896 rule_users = relationship('RepoReviewRuleUser')
3908 rule_users = relationship('RepoReviewRuleUser')
3897 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3909 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3898
3910
3899 @hybrid_property
3911 @hybrid_property
3900 def branch_pattern(self):
3912 def branch_pattern(self):
3901 return self._branch_pattern or '*'
3913 return self._branch_pattern or '*'
3902
3914
3903 def _validate_glob(self, value):
3915 def _validate_glob(self, value):
3904 re.compile('^' + glob2re(value) + '$')
3916 re.compile('^' + glob2re(value) + '$')
3905
3917
3906 @branch_pattern.setter
3918 @branch_pattern.setter
3907 def branch_pattern(self, value):
3919 def branch_pattern(self, value):
3908 self._validate_glob(value)
3920 self._validate_glob(value)
3909 self._branch_pattern = value or '*'
3921 self._branch_pattern = value or '*'
3910
3922
3911 @hybrid_property
3923 @hybrid_property
3912 def file_pattern(self):
3924 def file_pattern(self):
3913 return self._file_pattern or '*'
3925 return self._file_pattern or '*'
3914
3926
3915 @file_pattern.setter
3927 @file_pattern.setter
3916 def file_pattern(self, value):
3928 def file_pattern(self, value):
3917 self._validate_glob(value)
3929 self._validate_glob(value)
3918 self._file_pattern = value or '*'
3930 self._file_pattern = value or '*'
3919
3931
3920 def matches(self, branch, files_changed):
3932 def matches(self, branch, files_changed):
3921 """
3933 """
3922 Check if this review rule matches a branch/files in a pull request
3934 Check if this review rule matches a branch/files in a pull request
3923
3935
3924 :param branch: branch name for the commit
3936 :param branch: branch name for the commit
3925 :param files_changed: list of file paths changed in the pull request
3937 :param files_changed: list of file paths changed in the pull request
3926 """
3938 """
3927
3939
3928 branch = branch or ''
3940 branch = branch or ''
3929 files_changed = files_changed or []
3941 files_changed = files_changed or []
3930
3942
3931 branch_matches = True
3943 branch_matches = True
3932 if branch:
3944 if branch:
3933 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3945 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3934 branch_matches = bool(branch_regex.search(branch))
3946 branch_matches = bool(branch_regex.search(branch))
3935
3947
3936 files_matches = True
3948 files_matches = True
3937 if self.file_pattern != '*':
3949 if self.file_pattern != '*':
3938 files_matches = False
3950 files_matches = False
3939 file_regex = re.compile(glob2re(self.file_pattern))
3951 file_regex = re.compile(glob2re(self.file_pattern))
3940 for filename in files_changed:
3952 for filename in files_changed:
3941 if file_regex.search(filename):
3953 if file_regex.search(filename):
3942 files_matches = True
3954 files_matches = True
3943 break
3955 break
3944
3956
3945 return branch_matches and files_matches
3957 return branch_matches and files_matches
3946
3958
3947 @property
3959 @property
3948 def review_users(self):
3960 def review_users(self):
3949 """ Returns the users which this rule applies to """
3961 """ Returns the users which this rule applies to """
3950
3962
3951 users = set()
3963 users = collections.OrderedDict()
3952 users |= set([
3964
3953 rule_user.user for rule_user in self.rule_users
3965 for rule_user in self.rule_users:
3954 if rule_user.user.active])
3966 if rule_user.user.active:
3955 users |= set(
3967 if rule_user.user not in users:
3956 member.user
3968 users[rule_user.user.username] = {
3957 for rule_user_group in self.rule_user_groups
3969 'user': rule_user.user,
3958 for member in rule_user_group.users_group.members
3970 'source': 'user',
3959 if member.user.active
3971 'data': rule_user.rule_data()
3960 )
3972 }
3973
3974 for rule_user_group in self.rule_user_groups:
3975 for member in rule_user_group.users_group.members:
3976 if member.user.active:
3977 users[member.user.username] = {
3978 'user': member.user,
3979 'source': 'user_group',
3980 'data': rule_user_group.rule_data()
3981 }
3982
3961 return users
3983 return users
3962
3984
3963 def __repr__(self):
3985 def __repr__(self):
3964 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3986 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3965 self.repo_review_rule_id, self.repo)
3987 self.repo_review_rule_id, self.repo)
3966
3988
3967
3989
3968 class DbMigrateVersion(Base, BaseModel):
3990 class DbMigrateVersion(Base, BaseModel):
3969 __tablename__ = 'db_migrate_version'
3991 __tablename__ = 'db_migrate_version'
3970 __table_args__ = (
3992 __table_args__ = (
3971 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3993 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3972 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3994 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3973 )
3995 )
3974 repository_id = Column('repository_id', String(250), primary_key=True)
3996 repository_id = Column('repository_id', String(250), primary_key=True)
3975 repository_path = Column('repository_path', Text)
3997 repository_path = Column('repository_path', Text)
3976 version = Column('version', Integer)
3998 version = Column('version', Integer)
3977
3999
3978
4000
3979 class DbSession(Base, BaseModel):
4001 class DbSession(Base, BaseModel):
3980 __tablename__ = 'db_session'
4002 __tablename__ = 'db_session'
3981 __table_args__ = (
4003 __table_args__ = (
3982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4004 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4005 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3984 )
4006 )
3985
4007
3986 def __repr__(self):
4008 def __repr__(self):
3987 return '<DB:DbSession({})>'.format(self.id)
4009 return '<DB:DbSession({})>'.format(self.id)
3988
4010
3989 id = Column('id', Integer())
4011 id = Column('id', Integer())
3990 namespace = Column('namespace', String(255), primary_key=True)
4012 namespace = Column('namespace', String(255), primary_key=True)
3991 accessed = Column('accessed', DateTime, nullable=False)
4013 accessed = Column('accessed', DateTime, nullable=False)
3992 created = Column('created', DateTime, nullable=False)
4014 created = Column('created', DateTime, nullable=False)
3993 data = Column('data', PickleType, nullable=False)
4015 data = Column('data', PickleType, nullable=False)
@@ -1,562 +1,563 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pylons.i18n.translation import _
51 from pylons.i18n.translation import _
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 return self.load(template_name)(**kw)
69 return self.load(template_name)(**kw)
70
70
71
71
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 deform.Form.set_default_renderer(form_renderer)
73 deform.Form.set_default_renderer(form_renderer)
74
74
75
75
76 def LoginForm():
76 def LoginForm():
77 class _LoginForm(formencode.Schema):
77 class _LoginForm(formencode.Schema):
78 allow_extra_fields = True
78 allow_extra_fields = True
79 filter_extra_fields = True
79 filter_extra_fields = True
80 username = v.UnicodeString(
80 username = v.UnicodeString(
81 strip=True,
81 strip=True,
82 min=1,
82 min=1,
83 not_empty=True,
83 not_empty=True,
84 messages={
84 messages={
85 'empty': _(u'Please enter a login'),
85 'empty': _(u'Please enter a login'),
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 }
87 }
88 )
88 )
89
89
90 password = v.UnicodeString(
90 password = v.UnicodeString(
91 strip=False,
91 strip=False,
92 min=3,
92 min=3,
93 not_empty=True,
93 not_empty=True,
94 messages={
94 messages={
95 'empty': _(u'Please enter a password'),
95 'empty': _(u'Please enter a password'),
96 'tooShort': _(u'Enter %(min)i characters or more')}
96 'tooShort': _(u'Enter %(min)i characters or more')}
97 )
97 )
98
98
99 remember = v.StringBoolean(if_missing=False)
99 remember = v.StringBoolean(if_missing=False)
100
100
101 chained_validators = [v.ValidAuth()]
101 chained_validators = [v.ValidAuth()]
102 return _LoginForm
102 return _LoginForm
103
103
104
104
105 def UserForm(edit=False, available_languages=[], old_data={}):
105 def UserForm(edit=False, available_languages=[], old_data={}):
106 class _UserForm(formencode.Schema):
106 class _UserForm(formencode.Schema):
107 allow_extra_fields = True
107 allow_extra_fields = True
108 filter_extra_fields = True
108 filter_extra_fields = True
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 v.ValidUsername(edit, old_data))
110 v.ValidUsername(edit, old_data))
111 if edit:
111 if edit:
112 new_password = All(
112 new_password = All(
113 v.ValidPassword(),
113 v.ValidPassword(),
114 v.UnicodeString(strip=False, min=6, not_empty=False)
114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 )
115 )
116 password_confirmation = All(
116 password_confirmation = All(
117 v.ValidPassword(),
117 v.ValidPassword(),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 )
119 )
120 admin = v.StringBoolean(if_missing=False)
120 admin = v.StringBoolean(if_missing=False)
121 else:
121 else:
122 password = All(
122 password = All(
123 v.ValidPassword(),
123 v.ValidPassword(),
124 v.UnicodeString(strip=False, min=6, not_empty=True)
124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 )
125 )
126 password_confirmation = All(
126 password_confirmation = All(
127 v.ValidPassword(),
127 v.ValidPassword(),
128 v.UnicodeString(strip=False, min=6, not_empty=False)
128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 )
129 )
130
130
131 password_change = v.StringBoolean(if_missing=False)
131 password_change = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
133
133
134 active = v.StringBoolean(if_missing=False)
134 active = v.StringBoolean(if_missing=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 extern_name = v.UnicodeString(strip=True)
138 extern_name = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
140 language = v.OneOf(available_languages, hideList=False,
140 language = v.OneOf(available_languages, hideList=False,
141 testValueList=True, if_missing=None)
141 testValueList=True, if_missing=None)
142 chained_validators = [v.ValidPasswordsMatch()]
142 chained_validators = [v.ValidPasswordsMatch()]
143 return _UserForm
143 return _UserForm
144
144
145
145
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
147 old_data = old_data or {}
147 old_data = old_data or {}
148
148
149 class _UserGroupForm(formencode.Schema):
149 class _UserGroupForm(formencode.Schema):
150 allow_extra_fields = True
150 allow_extra_fields = True
151 filter_extra_fields = True
151 filter_extra_fields = True
152
152
153 users_group_name = All(
153 users_group_name = All(
154 v.UnicodeString(strip=True, min=1, not_empty=True),
154 v.UnicodeString(strip=True, min=1, not_empty=True),
155 v.ValidUserGroup(edit, old_data)
155 v.ValidUserGroup(edit, old_data)
156 )
156 )
157 user_group_description = v.UnicodeString(strip=True, min=1,
157 user_group_description = v.UnicodeString(strip=True, min=1,
158 not_empty=False)
158 not_empty=False)
159
159
160 users_group_active = v.StringBoolean(if_missing=False)
160 users_group_active = v.StringBoolean(if_missing=False)
161
161
162 if edit:
162 if edit:
163 # this is user group owner
163 # this is user group owner
164 user = All(
164 user = All(
165 v.UnicodeString(not_empty=True),
165 v.UnicodeString(not_empty=True),
166 v.ValidRepoUser(allow_disabled))
166 v.ValidRepoUser(allow_disabled))
167 return _UserGroupForm
167 return _UserGroupForm
168
168
169
169
170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
171 can_create_in_root=False, allow_disabled=False):
171 can_create_in_root=False, allow_disabled=False):
172 old_data = old_data or {}
172 old_data = old_data or {}
173 available_groups = available_groups or []
173 available_groups = available_groups or []
174
174
175 class _RepoGroupForm(formencode.Schema):
175 class _RepoGroupForm(formencode.Schema):
176 allow_extra_fields = True
176 allow_extra_fields = True
177 filter_extra_fields = False
177 filter_extra_fields = False
178
178
179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
180 v.SlugifyName(),)
180 v.SlugifyName(),)
181 group_description = v.UnicodeString(strip=True, min=1,
181 group_description = v.UnicodeString(strip=True, min=1,
182 not_empty=False)
182 not_empty=False)
183 group_copy_permissions = v.StringBoolean(if_missing=False)
183 group_copy_permissions = v.StringBoolean(if_missing=False)
184
184
185 group_parent_id = v.OneOf(available_groups, hideList=False,
185 group_parent_id = v.OneOf(available_groups, hideList=False,
186 testValueList=True, not_empty=True)
186 testValueList=True, not_empty=True)
187 enable_locking = v.StringBoolean(if_missing=False)
187 enable_locking = v.StringBoolean(if_missing=False)
188 chained_validators = [
188 chained_validators = [
189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
190
190
191 if edit:
191 if edit:
192 # this is repo group owner
192 # this is repo group owner
193 user = All(
193 user = All(
194 v.UnicodeString(not_empty=True),
194 v.UnicodeString(not_empty=True),
195 v.ValidRepoUser(allow_disabled))
195 v.ValidRepoUser(allow_disabled))
196
196
197 return _RepoGroupForm
197 return _RepoGroupForm
198
198
199
199
200 def RegisterForm(edit=False, old_data={}):
200 def RegisterForm(edit=False, old_data={}):
201 class _RegisterForm(formencode.Schema):
201 class _RegisterForm(formencode.Schema):
202 allow_extra_fields = True
202 allow_extra_fields = True
203 filter_extra_fields = True
203 filter_extra_fields = True
204 username = All(
204 username = All(
205 v.ValidUsername(edit, old_data),
205 v.ValidUsername(edit, old_data),
206 v.UnicodeString(strip=True, min=1, not_empty=True)
206 v.UnicodeString(strip=True, min=1, not_empty=True)
207 )
207 )
208 password = All(
208 password = All(
209 v.ValidPassword(),
209 v.ValidPassword(),
210 v.UnicodeString(strip=False, min=6, not_empty=True)
210 v.UnicodeString(strip=False, min=6, not_empty=True)
211 )
211 )
212 password_confirmation = All(
212 password_confirmation = All(
213 v.ValidPassword(),
213 v.ValidPassword(),
214 v.UnicodeString(strip=False, min=6, not_empty=True)
214 v.UnicodeString(strip=False, min=6, not_empty=True)
215 )
215 )
216 active = v.StringBoolean(if_missing=False)
216 active = v.StringBoolean(if_missing=False)
217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
220
220
221 chained_validators = [v.ValidPasswordsMatch()]
221 chained_validators = [v.ValidPasswordsMatch()]
222
222
223 return _RegisterForm
223 return _RegisterForm
224
224
225
225
226 def PasswordResetForm():
226 def PasswordResetForm():
227 class _PasswordResetForm(formencode.Schema):
227 class _PasswordResetForm(formencode.Schema):
228 allow_extra_fields = True
228 allow_extra_fields = True
229 filter_extra_fields = True
229 filter_extra_fields = True
230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
231 return _PasswordResetForm
231 return _PasswordResetForm
232
232
233
233
234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
235 allow_disabled=False):
235 allow_disabled=False):
236 old_data = old_data or {}
236 old_data = old_data or {}
237 repo_groups = repo_groups or []
237 repo_groups = repo_groups or []
238 landing_revs = landing_revs or []
238 landing_revs = landing_revs or []
239 supported_backends = BACKENDS.keys()
239 supported_backends = BACKENDS.keys()
240
240
241 class _RepoForm(formencode.Schema):
241 class _RepoForm(formencode.Schema):
242 allow_extra_fields = True
242 allow_extra_fields = True
243 filter_extra_fields = False
243 filter_extra_fields = False
244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
245 v.SlugifyName(), v.CannotHaveGitSuffix())
245 v.SlugifyName(), v.CannotHaveGitSuffix())
246 repo_group = All(v.CanWriteGroup(old_data),
246 repo_group = All(v.CanWriteGroup(old_data),
247 v.OneOf(repo_groups, hideList=True))
247 v.OneOf(repo_groups, hideList=True))
248 repo_type = v.OneOf(supported_backends, required=False,
248 repo_type = v.OneOf(supported_backends, required=False,
249 if_missing=old_data.get('repo_type'))
249 if_missing=old_data.get('repo_type'))
250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
251 repo_private = v.StringBoolean(if_missing=False)
251 repo_private = v.StringBoolean(if_missing=False)
252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
253 repo_copy_permissions = v.StringBoolean(if_missing=False)
253 repo_copy_permissions = v.StringBoolean(if_missing=False)
254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
255
255
256 repo_enable_statistics = v.StringBoolean(if_missing=False)
256 repo_enable_statistics = v.StringBoolean(if_missing=False)
257 repo_enable_downloads = v.StringBoolean(if_missing=False)
257 repo_enable_downloads = v.StringBoolean(if_missing=False)
258 repo_enable_locking = v.StringBoolean(if_missing=False)
258 repo_enable_locking = v.StringBoolean(if_missing=False)
259
259
260 if edit:
260 if edit:
261 # this is repo owner
261 # this is repo owner
262 user = All(
262 user = All(
263 v.UnicodeString(not_empty=True),
263 v.UnicodeString(not_empty=True),
264 v.ValidRepoUser(allow_disabled))
264 v.ValidRepoUser(allow_disabled))
265 clone_uri_change = v.UnicodeString(
265 clone_uri_change = v.UnicodeString(
266 not_empty=False, if_missing=v.Missing)
266 not_empty=False, if_missing=v.Missing)
267
267
268 chained_validators = [v.ValidCloneUri(),
268 chained_validators = [v.ValidCloneUri(),
269 v.ValidRepoName(edit, old_data)]
269 v.ValidRepoName(edit, old_data)]
270 return _RepoForm
270 return _RepoForm
271
271
272
272
273 def RepoPermsForm():
273 def RepoPermsForm():
274 class _RepoPermsForm(formencode.Schema):
274 class _RepoPermsForm(formencode.Schema):
275 allow_extra_fields = True
275 allow_extra_fields = True
276 filter_extra_fields = False
276 filter_extra_fields = False
277 chained_validators = [v.ValidPerms(type_='repo')]
277 chained_validators = [v.ValidPerms(type_='repo')]
278 return _RepoPermsForm
278 return _RepoPermsForm
279
279
280
280
281 def RepoGroupPermsForm(valid_recursive_choices):
281 def RepoGroupPermsForm(valid_recursive_choices):
282 class _RepoGroupPermsForm(formencode.Schema):
282 class _RepoGroupPermsForm(formencode.Schema):
283 allow_extra_fields = True
283 allow_extra_fields = True
284 filter_extra_fields = False
284 filter_extra_fields = False
285 recursive = v.OneOf(valid_recursive_choices)
285 recursive = v.OneOf(valid_recursive_choices)
286 chained_validators = [v.ValidPerms(type_='repo_group')]
286 chained_validators = [v.ValidPerms(type_='repo_group')]
287 return _RepoGroupPermsForm
287 return _RepoGroupPermsForm
288
288
289
289
290 def UserGroupPermsForm():
290 def UserGroupPermsForm():
291 class _UserPermsForm(formencode.Schema):
291 class _UserPermsForm(formencode.Schema):
292 allow_extra_fields = True
292 allow_extra_fields = True
293 filter_extra_fields = False
293 filter_extra_fields = False
294 chained_validators = [v.ValidPerms(type_='user_group')]
294 chained_validators = [v.ValidPerms(type_='user_group')]
295 return _UserPermsForm
295 return _UserPermsForm
296
296
297
297
298 def RepoFieldForm():
298 def RepoFieldForm():
299 class _RepoFieldForm(formencode.Schema):
299 class _RepoFieldForm(formencode.Schema):
300 filter_extra_fields = True
300 filter_extra_fields = True
301 allow_extra_fields = True
301 allow_extra_fields = True
302
302
303 new_field_key = All(v.FieldKey(),
303 new_field_key = All(v.FieldKey(),
304 v.UnicodeString(strip=True, min=3, not_empty=True))
304 v.UnicodeString(strip=True, min=3, not_empty=True))
305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
307 if_missing='str')
307 if_missing='str')
308 new_field_label = v.UnicodeString(not_empty=False)
308 new_field_label = v.UnicodeString(not_empty=False)
309 new_field_desc = v.UnicodeString(not_empty=False)
309 new_field_desc = v.UnicodeString(not_empty=False)
310
310
311 return _RepoFieldForm
311 return _RepoFieldForm
312
312
313
313
314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
315 repo_groups=[], landing_revs=[]):
315 repo_groups=[], landing_revs=[]):
316 class _RepoForkForm(formencode.Schema):
316 class _RepoForkForm(formencode.Schema):
317 allow_extra_fields = True
317 allow_extra_fields = True
318 filter_extra_fields = False
318 filter_extra_fields = False
319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
320 v.SlugifyName())
320 v.SlugifyName())
321 repo_group = All(v.CanWriteGroup(),
321 repo_group = All(v.CanWriteGroup(),
322 v.OneOf(repo_groups, hideList=True))
322 v.OneOf(repo_groups, hideList=True))
323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
325 private = v.StringBoolean(if_missing=False)
325 private = v.StringBoolean(if_missing=False)
326 copy_permissions = v.StringBoolean(if_missing=False)
326 copy_permissions = v.StringBoolean(if_missing=False)
327 fork_parent_id = v.UnicodeString()
327 fork_parent_id = v.UnicodeString()
328 chained_validators = [v.ValidForkName(edit, old_data)]
328 chained_validators = [v.ValidForkName(edit, old_data)]
329 landing_rev = v.OneOf(landing_revs, hideList=True)
329 landing_rev = v.OneOf(landing_revs, hideList=True)
330
330
331 return _RepoForkForm
331 return _RepoForkForm
332
332
333
333
334 def ApplicationSettingsForm():
334 def ApplicationSettingsForm():
335 class _ApplicationSettingsForm(formencode.Schema):
335 class _ApplicationSettingsForm(formencode.Schema):
336 allow_extra_fields = True
336 allow_extra_fields = True
337 filter_extra_fields = False
337 filter_extra_fields = False
338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
346
346
347 return _ApplicationSettingsForm
347 return _ApplicationSettingsForm
348
348
349
349
350 def ApplicationVisualisationForm():
350 def ApplicationVisualisationForm():
351 class _ApplicationVisualisationForm(formencode.Schema):
351 class _ApplicationVisualisationForm(formencode.Schema):
352 allow_extra_fields = True
352 allow_extra_fields = True
353 filter_extra_fields = False
353 filter_extra_fields = False
354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
357
357
358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
362 rhodecode_show_version = v.StringBoolean(if_missing=False)
362 rhodecode_show_version = v.StringBoolean(if_missing=False)
363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
365 rhodecode_gravatar_url = v.UnicodeString(min=3)
365 rhodecode_gravatar_url = v.UnicodeString(min=3)
366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
367 rhodecode_support_url = v.UnicodeString()
367 rhodecode_support_url = v.UnicodeString()
368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
370
370
371 return _ApplicationVisualisationForm
371 return _ApplicationVisualisationForm
372
372
373
373
374 class _BaseVcsSettingsForm(formencode.Schema):
374 class _BaseVcsSettingsForm(formencode.Schema):
375 allow_extra_fields = True
375 allow_extra_fields = True
376 filter_extra_fields = False
376 filter_extra_fields = False
377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
380
380
381 # PR/Code-review
381 # PR/Code-review
382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
384
384
385 # hg
385 # hg
386 extensions_largefiles = v.StringBoolean(if_missing=False)
386 extensions_largefiles = v.StringBoolean(if_missing=False)
387 extensions_evolve = v.StringBoolean(if_missing=False)
387 extensions_evolve = v.StringBoolean(if_missing=False)
388 phases_publish = v.StringBoolean(if_missing=False)
388 phases_publish = v.StringBoolean(if_missing=False)
389 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
389 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
390
390
391 # git
391 # git
392 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
392 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
393
393
394 # svn
394 # svn
395 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
395 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
396 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
396 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
397
397
398
398
399 def ApplicationUiSettingsForm():
399 def ApplicationUiSettingsForm():
400 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
400 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
401 web_push_ssl = v.StringBoolean(if_missing=False)
401 web_push_ssl = v.StringBoolean(if_missing=False)
402 paths_root_path = All(
402 paths_root_path = All(
403 v.ValidPath(),
403 v.ValidPath(),
404 v.UnicodeString(strip=True, min=1, not_empty=True)
404 v.UnicodeString(strip=True, min=1, not_empty=True)
405 )
405 )
406 largefiles_usercache = All(
406 largefiles_usercache = All(
407 v.ValidPath(),
407 v.ValidPath(),
408 v.UnicodeString(strip=True, min=2, not_empty=True))
408 v.UnicodeString(strip=True, min=2, not_empty=True))
409 vcs_git_lfs_store_location = All(
409 vcs_git_lfs_store_location = All(
410 v.ValidPath(),
410 v.ValidPath(),
411 v.UnicodeString(strip=True, min=2, not_empty=True))
411 v.UnicodeString(strip=True, min=2, not_empty=True))
412 extensions_hgsubversion = v.StringBoolean(if_missing=False)
412 extensions_hgsubversion = v.StringBoolean(if_missing=False)
413 extensions_hggit = v.StringBoolean(if_missing=False)
413 extensions_hggit = v.StringBoolean(if_missing=False)
414 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
414 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
415 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
415 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
416
416
417 return _ApplicationUiSettingsForm
417 return _ApplicationUiSettingsForm
418
418
419
419
420 def RepoVcsSettingsForm(repo_name):
420 def RepoVcsSettingsForm(repo_name):
421 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
421 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
422 inherit_global_settings = v.StringBoolean(if_missing=False)
422 inherit_global_settings = v.StringBoolean(if_missing=False)
423 new_svn_branch = v.ValidSvnPattern(
423 new_svn_branch = v.ValidSvnPattern(
424 section='vcs_svn_branch', repo_name=repo_name)
424 section='vcs_svn_branch', repo_name=repo_name)
425 new_svn_tag = v.ValidSvnPattern(
425 new_svn_tag = v.ValidSvnPattern(
426 section='vcs_svn_tag', repo_name=repo_name)
426 section='vcs_svn_tag', repo_name=repo_name)
427
427
428 return _RepoVcsSettingsForm
428 return _RepoVcsSettingsForm
429
429
430
430
431 def LabsSettingsForm():
431 def LabsSettingsForm():
432 class _LabSettingsForm(formencode.Schema):
432 class _LabSettingsForm(formencode.Schema):
433 allow_extra_fields = True
433 allow_extra_fields = True
434 filter_extra_fields = False
434 filter_extra_fields = False
435
435
436 return _LabSettingsForm
436 return _LabSettingsForm
437
437
438
438
439 def ApplicationPermissionsForm(
439 def ApplicationPermissionsForm(
440 register_choices, password_reset_choices, extern_activate_choices):
440 register_choices, password_reset_choices, extern_activate_choices):
441 class _DefaultPermissionsForm(formencode.Schema):
441 class _DefaultPermissionsForm(formencode.Schema):
442 allow_extra_fields = True
442 allow_extra_fields = True
443 filter_extra_fields = True
443 filter_extra_fields = True
444
444
445 anonymous = v.StringBoolean(if_missing=False)
445 anonymous = v.StringBoolean(if_missing=False)
446 default_register = v.OneOf(register_choices)
446 default_register = v.OneOf(register_choices)
447 default_register_message = v.UnicodeString()
447 default_register_message = v.UnicodeString()
448 default_password_reset = v.OneOf(password_reset_choices)
448 default_password_reset = v.OneOf(password_reset_choices)
449 default_extern_activate = v.OneOf(extern_activate_choices)
449 default_extern_activate = v.OneOf(extern_activate_choices)
450
450
451 return _DefaultPermissionsForm
451 return _DefaultPermissionsForm
452
452
453
453
454 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
454 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
455 user_group_perms_choices):
455 user_group_perms_choices):
456 class _ObjectPermissionsForm(formencode.Schema):
456 class _ObjectPermissionsForm(formencode.Schema):
457 allow_extra_fields = True
457 allow_extra_fields = True
458 filter_extra_fields = True
458 filter_extra_fields = True
459 overwrite_default_repo = v.StringBoolean(if_missing=False)
459 overwrite_default_repo = v.StringBoolean(if_missing=False)
460 overwrite_default_group = v.StringBoolean(if_missing=False)
460 overwrite_default_group = v.StringBoolean(if_missing=False)
461 overwrite_default_user_group = v.StringBoolean(if_missing=False)
461 overwrite_default_user_group = v.StringBoolean(if_missing=False)
462 default_repo_perm = v.OneOf(repo_perms_choices)
462 default_repo_perm = v.OneOf(repo_perms_choices)
463 default_group_perm = v.OneOf(group_perms_choices)
463 default_group_perm = v.OneOf(group_perms_choices)
464 default_user_group_perm = v.OneOf(user_group_perms_choices)
464 default_user_group_perm = v.OneOf(user_group_perms_choices)
465
465
466 return _ObjectPermissionsForm
466 return _ObjectPermissionsForm
467
467
468
468
469 def UserPermissionsForm(create_choices, create_on_write_choices,
469 def UserPermissionsForm(create_choices, create_on_write_choices,
470 repo_group_create_choices, user_group_create_choices,
470 repo_group_create_choices, user_group_create_choices,
471 fork_choices, inherit_default_permissions_choices):
471 fork_choices, inherit_default_permissions_choices):
472 class _DefaultPermissionsForm(formencode.Schema):
472 class _DefaultPermissionsForm(formencode.Schema):
473 allow_extra_fields = True
473 allow_extra_fields = True
474 filter_extra_fields = True
474 filter_extra_fields = True
475
475
476 anonymous = v.StringBoolean(if_missing=False)
476 anonymous = v.StringBoolean(if_missing=False)
477
477
478 default_repo_create = v.OneOf(create_choices)
478 default_repo_create = v.OneOf(create_choices)
479 default_repo_create_on_write = v.OneOf(create_on_write_choices)
479 default_repo_create_on_write = v.OneOf(create_on_write_choices)
480 default_user_group_create = v.OneOf(user_group_create_choices)
480 default_user_group_create = v.OneOf(user_group_create_choices)
481 default_repo_group_create = v.OneOf(repo_group_create_choices)
481 default_repo_group_create = v.OneOf(repo_group_create_choices)
482 default_fork_create = v.OneOf(fork_choices)
482 default_fork_create = v.OneOf(fork_choices)
483 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
483 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
484
484
485 return _DefaultPermissionsForm
485 return _DefaultPermissionsForm
486
486
487
487
488 def UserIndividualPermissionsForm():
488 def UserIndividualPermissionsForm():
489 class _DefaultPermissionsForm(formencode.Schema):
489 class _DefaultPermissionsForm(formencode.Schema):
490 allow_extra_fields = True
490 allow_extra_fields = True
491 filter_extra_fields = True
491 filter_extra_fields = True
492
492
493 inherit_default_permissions = v.StringBoolean(if_missing=False)
493 inherit_default_permissions = v.StringBoolean(if_missing=False)
494
494
495 return _DefaultPermissionsForm
495 return _DefaultPermissionsForm
496
496
497
497
498 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
498 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
499 class _DefaultsForm(formencode.Schema):
499 class _DefaultsForm(formencode.Schema):
500 allow_extra_fields = True
500 allow_extra_fields = True
501 filter_extra_fields = True
501 filter_extra_fields = True
502 default_repo_type = v.OneOf(supported_backends)
502 default_repo_type = v.OneOf(supported_backends)
503 default_repo_private = v.StringBoolean(if_missing=False)
503 default_repo_private = v.StringBoolean(if_missing=False)
504 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
504 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
505 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
505 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
506 default_repo_enable_locking = v.StringBoolean(if_missing=False)
506 default_repo_enable_locking = v.StringBoolean(if_missing=False)
507
507
508 return _DefaultsForm
508 return _DefaultsForm
509
509
510
510
511 def AuthSettingsForm():
511 def AuthSettingsForm():
512 class _AuthSettingsForm(formencode.Schema):
512 class _AuthSettingsForm(formencode.Schema):
513 allow_extra_fields = True
513 allow_extra_fields = True
514 filter_extra_fields = True
514 filter_extra_fields = True
515 auth_plugins = All(v.ValidAuthPlugins(),
515 auth_plugins = All(v.ValidAuthPlugins(),
516 v.UniqueListFromString()(not_empty=True))
516 v.UniqueListFromString()(not_empty=True))
517
517
518 return _AuthSettingsForm
518 return _AuthSettingsForm
519
519
520
520
521 def UserExtraEmailForm():
521 def UserExtraEmailForm():
522 class _UserExtraEmailForm(formencode.Schema):
522 class _UserExtraEmailForm(formencode.Schema):
523 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
523 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
524 return _UserExtraEmailForm
524 return _UserExtraEmailForm
525
525
526
526
527 def UserExtraIpForm():
527 def UserExtraIpForm():
528 class _UserExtraIpForm(formencode.Schema):
528 class _UserExtraIpForm(formencode.Schema):
529 ip = v.ValidIp()(not_empty=True)
529 ip = v.ValidIp()(not_empty=True)
530 return _UserExtraIpForm
530 return _UserExtraIpForm
531
531
532
532
533
533
534 def PullRequestForm(repo_id):
534 def PullRequestForm(repo_id):
535 class ReviewerForm(formencode.Schema):
535 class ReviewerForm(formencode.Schema):
536 user_id = v.Int(not_empty=True)
536 user_id = v.Int(not_empty=True)
537 reasons = All()
537 reasons = All()
538 mandatory = v.StringBoolean()
538
539
539 class _PullRequestForm(formencode.Schema):
540 class _PullRequestForm(formencode.Schema):
540 allow_extra_fields = True
541 allow_extra_fields = True
541 filter_extra_fields = True
542 filter_extra_fields = True
542
543
543 user = v.UnicodeString(strip=True, required=True)
544 common_ancestor = v.UnicodeString(strip=True, required=True)
544 source_repo = v.UnicodeString(strip=True, required=True)
545 source_repo = v.UnicodeString(strip=True, required=True)
545 source_ref = v.UnicodeString(strip=True, required=True)
546 source_ref = v.UnicodeString(strip=True, required=True)
546 target_repo = v.UnicodeString(strip=True, required=True)
547 target_repo = v.UnicodeString(strip=True, required=True)
547 target_ref = v.UnicodeString(strip=True, required=True)
548 target_ref = v.UnicodeString(strip=True, required=True)
548 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 v.UniqueList()(not_empty=True))
550 v.UniqueList()(not_empty=True))
550 review_members = formencode.ForEach(ReviewerForm())
551 review_members = formencode.ForEach(ReviewerForm())
551 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553
554
554 return _PullRequestForm
555 return _PullRequestForm
555
556
556
557
557 def IssueTrackerPatternsForm():
558 def IssueTrackerPatternsForm():
558 class _IssueTrackerPatternsForm(formencode.Schema):
559 class _IssueTrackerPatternsForm(formencode.Schema):
559 allow_extra_fields = True
560 allow_extra_fields = True
560 filter_extra_fields = False
561 filter_extra_fields = False
561 chained_validators = [v.ValidPattern()]
562 chained_validators = [v.ValidPattern()]
562 return _IssueTrackerPatternsForm
563 return _IssueTrackerPatternsForm
@@ -1,1471 +1,1499 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment, Repository)
53 PullRequestVersion, ChangesetComment, Repository)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple('UpdateResponse', [
66 UpdateResponse = namedtuple('UpdateResponse', [
67 'executed', 'reason', 'new', 'old', 'changes',
67 'executed', 'reason', 'new', 'old', 'changes',
68 'source_changed', 'target_changed'])
68 'source_changed', 'target_changed'])
69
69
70
70
71 class PullRequestModel(BaseModel):
71 class PullRequestModel(BaseModel):
72
72
73 cls = PullRequest
73 cls = PullRequest
74
74
75 DIFF_CONTEXT = 3
75 DIFF_CONTEXT = 3
76
76
77 MERGE_STATUS_MESSAGES = {
77 MERGE_STATUS_MESSAGES = {
78 MergeFailureReason.NONE: lazy_ugettext(
78 MergeFailureReason.NONE: lazy_ugettext(
79 'This pull request can be automatically merged.'),
79 'This pull request can be automatically merged.'),
80 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 'This pull request cannot be merged because of an unhandled'
81 'This pull request cannot be merged because of an unhandled'
82 ' exception.'),
82 ' exception.'),
83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 'This pull request cannot be merged because of merge conflicts.'),
84 'This pull request cannot be merged because of merge conflicts.'),
85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 'This pull request could not be merged because push to target'
86 'This pull request could not be merged because push to target'
87 ' failed.'),
87 ' failed.'),
88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 'This pull request cannot be merged because the target is not a'
89 'This pull request cannot be merged because the target is not a'
90 ' head.'),
90 ' head.'),
91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 'This pull request cannot be merged because the source contains'
92 'This pull request cannot be merged because the source contains'
93 ' more branches than the target.'),
93 ' more branches than the target.'),
94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 'This pull request cannot be merged because the target has'
95 'This pull request cannot be merged because the target has'
96 ' multiple heads.'),
96 ' multiple heads.'),
97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 'This pull request cannot be merged because the target repository'
98 'This pull request cannot be merged because the target repository'
99 ' is locked.'),
99 ' is locked.'),
100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 'This pull request cannot be merged because the target or the '
101 'This pull request cannot be merged because the target or the '
102 'source reference is missing.'),
102 'source reference is missing.'),
103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 'This pull request cannot be merged because the target '
104 'This pull request cannot be merged because the target '
105 'reference is missing.'),
105 'reference is missing.'),
106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 'This pull request cannot be merged because the source '
107 'This pull request cannot be merged because the source '
108 'reference is missing.'),
108 'reference is missing.'),
109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 'This pull request cannot be merged because of conflicts related '
110 'This pull request cannot be merged because of conflicts related '
111 'to sub repositories.'),
111 'to sub repositories.'),
112 }
112 }
113
113
114 UPDATE_STATUS_MESSAGES = {
114 UPDATE_STATUS_MESSAGES = {
115 UpdateFailureReason.NONE: lazy_ugettext(
115 UpdateFailureReason.NONE: lazy_ugettext(
116 'Pull request update successful.'),
116 'Pull request update successful.'),
117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 'Pull request update failed because of an unknown error.'),
118 'Pull request update failed because of an unknown error.'),
119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 'No update needed because the source and target have not changed.'),
120 'No update needed because the source and target have not changed.'),
121 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
123 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin',)
158 _perms = ('repository.admin',)
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None,
419 reviewer_data=None):
420
419 created_by_user = self._get_user(created_by)
421 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
422 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
423 target_repo = self._get_repo(target_repo)
422
424
423 pull_request = PullRequest()
425 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
426 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
427 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
428 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
429 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
430 pull_request.revisions = revisions
429 pull_request.title = title
431 pull_request.title = title
430 pull_request.description = description
432 pull_request.description = description
431 pull_request.author = created_by_user
433 pull_request.author = created_by_user
434 pull_request.reviewer_data = reviewer_data
432
435
433 Session().add(pull_request)
436 Session().add(pull_request)
434 Session().flush()
437 Session().flush()
435
438
436 reviewer_ids = set()
439 reviewer_ids = set()
437 # members / reviewers
440 # members / reviewers
438 for reviewer_object in reviewers:
441 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
442 user_id, reasons, mandatory = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers()
448 reviewer.user = user
449 reviewer.pull_request = pull_request
450 reviewer.reasons = reasons
451 reviewer.mandatory = mandatory
448 Session().add(reviewer)
452 Session().add(reviewer)
449
453
450 # Set approval status to "Under Review" for all commits which are
454 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
455 # part of this pull request.
452 ChangesetStatusModel().set_status(
456 ChangesetStatusModel().set_status(
453 repo=target_repo,
457 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
458 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
459 user=created_by_user,
456 pull_request=pull_request
460 pull_request=pull_request
457 )
461 )
458
462
459 self.notify_reviewers(pull_request, reviewer_ids)
463 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
464 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
465 pull_request, created_by_user, 'create')
462
466
463 return pull_request
467 return pull_request
464
468
465 def _trigger_pull_request_hook(self, pull_request, user, action):
469 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
470 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
471 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
472 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
474 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
476 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
478 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
479 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
480 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
481 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
482 else:
479 return
483 return
480
484
481 trigger_hook(
485 trigger_hook(
482 username=user.username,
486 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
487 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
488 repo_alias=target_scm.alias,
485 pull_request=pull_request)
489 pull_request=pull_request)
486
490
487 def _get_commit_ids(self, pull_request):
491 def _get_commit_ids(self, pull_request):
488 """
492 """
489 Return the commit ids of the merged pull request.
493 Return the commit ids of the merged pull request.
490
494
491 This method is not dealing correctly yet with the lack of autoupdates
495 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
496 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
497 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
498 will be reported anyways.
495 """
499 """
496 merge_rev = pull_request.merge_rev
500 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
501 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
502 raise ValueError('This pull request was not merged yet')
499
503
500 commit_ids = list(pull_request.revisions)
504 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
505 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
506 commit_ids.append(merge_rev)
503
507
504 return commit_ids
508 return commit_ids
505
509
506 def merge(self, pull_request, user, extras):
510 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
511 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
512 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
513 if merge_state.executed:
510 log.debug(
514 log.debug(
511 "Merge was successful, updating the pull request comments.")
515 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
516 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
517 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
518 else:
515 log.warn("Merge failed, not updating the pull request.")
519 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
520 return merge_state
517
521
518 def _merge_pull_request(self, pull_request, user, extras):
522 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
523 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
524 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
525 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
526 pull_request.target_ref_parts, target_vcs)
523
527
524 message = _(
528 message = _(
525 'Merge pull request #%(pr_id)s from '
529 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
530 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
531 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
532 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
533 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
534 'pr_title': pull_request.title
531 }
535 }
532
536
533 workspace_id = self._workspace_id(pull_request)
537 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
538 use_rebase = self._use_rebase_for_merging(pull_request)
535
539
536 callback_daemon, extras = prepare_callback_daemon(
540 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
541 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
542 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
543
540 with callback_daemon:
544 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
545 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
546 # for a single call.
543 target_vcs.config.set(
547 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
548 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
549 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
550 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
551 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
552 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
553 return merge_state
550
554
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
555 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
556 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
557 pull_request.updated_on = datetime.datetime.now()
554
558
555 CommentsModel().create(
559 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
560 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
561 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
562 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
563 pull_request=pull_request.pull_request_id,
560 f_path=None,
564 f_path=None,
561 line_no=None,
565 line_no=None,
562 closing_pr=True
566 closing_pr=True
563 )
567 )
564
568
565 Session().add(pull_request)
569 Session().add(pull_request)
566 Session().flush()
570 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
571 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
572 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
573 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
574 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
575
572 def has_valid_update_type(self, pull_request):
576 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
577 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
578 return source_ref_type in ['book', 'branch', 'tag']
575
579
576 def update_commits(self, pull_request):
580 def update_commits(self, pull_request):
577 """
581 """
578 Get the updated list of commits for the pull request
582 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
583 and return the new pull request version and the list
580 of commits processed by this update action
584 of commits processed by this update action
581 """
585 """
582 pull_request = self.__get_pull_request(pull_request)
586 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
587 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
588 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
589 source_ref_id = pull_request.source_ref_parts.commit_id
586
590
587 target_ref_type = pull_request.target_ref_parts.type
591 target_ref_type = pull_request.target_ref_parts.type
588 target_ref_name = pull_request.target_ref_parts.name
592 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
593 target_ref_id = pull_request.target_ref_parts.commit_id
590
594
591 if not self.has_valid_update_type(pull_request):
595 if not self.has_valid_update_type(pull_request):
592 log.debug(
596 log.debug(
593 "Skipping update of pull request %s due to ref type: %s",
597 "Skipping update of pull request %s due to ref type: %s",
594 pull_request, source_ref_type)
598 pull_request, source_ref_type)
595 return UpdateResponse(
599 return UpdateResponse(
596 executed=False,
600 executed=False,
597 reason=UpdateFailureReason.WRONG_REF_TYPE,
601 reason=UpdateFailureReason.WRONG_REF_TYPE,
598 old=pull_request, new=None, changes=None,
602 old=pull_request, new=None, changes=None,
599 source_changed=False, target_changed=False)
603 source_changed=False, target_changed=False)
600
604
601 # source repo
605 # source repo
602 source_repo = pull_request.source_repo.scm_instance()
606 source_repo = pull_request.source_repo.scm_instance()
603 try:
607 try:
604 source_commit = source_repo.get_commit(commit_id=source_ref_name)
608 source_commit = source_repo.get_commit(commit_id=source_ref_name)
605 except CommitDoesNotExistError:
609 except CommitDoesNotExistError:
606 return UpdateResponse(
610 return UpdateResponse(
607 executed=False,
611 executed=False,
608 reason=UpdateFailureReason.MISSING_SOURCE_REF,
612 reason=UpdateFailureReason.MISSING_SOURCE_REF,
609 old=pull_request, new=None, changes=None,
613 old=pull_request, new=None, changes=None,
610 source_changed=False, target_changed=False)
614 source_changed=False, target_changed=False)
611
615
612 source_changed = source_ref_id != source_commit.raw_id
616 source_changed = source_ref_id != source_commit.raw_id
613
617
614 # target repo
618 # target repo
615 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
616 try:
620 try:
617 target_commit = target_repo.get_commit(commit_id=target_ref_name)
621 target_commit = target_repo.get_commit(commit_id=target_ref_name)
618 except CommitDoesNotExistError:
622 except CommitDoesNotExistError:
619 return UpdateResponse(
623 return UpdateResponse(
620 executed=False,
624 executed=False,
621 reason=UpdateFailureReason.MISSING_TARGET_REF,
625 reason=UpdateFailureReason.MISSING_TARGET_REF,
622 old=pull_request, new=None, changes=None,
626 old=pull_request, new=None, changes=None,
623 source_changed=False, target_changed=False)
627 source_changed=False, target_changed=False)
624 target_changed = target_ref_id != target_commit.raw_id
628 target_changed = target_ref_id != target_commit.raw_id
625
629
626 if not (source_changed or target_changed):
630 if not (source_changed or target_changed):
627 log.debug("Nothing changed in pull request %s", pull_request)
631 log.debug("Nothing changed in pull request %s", pull_request)
628 return UpdateResponse(
632 return UpdateResponse(
629 executed=False,
633 executed=False,
630 reason=UpdateFailureReason.NO_CHANGE,
634 reason=UpdateFailureReason.NO_CHANGE,
631 old=pull_request, new=None, changes=None,
635 old=pull_request, new=None, changes=None,
632 source_changed=target_changed, target_changed=source_changed)
636 source_changed=target_changed, target_changed=source_changed)
633
637
634 change_in_found = 'target repo' if target_changed else 'source repo'
638 change_in_found = 'target repo' if target_changed else 'source repo'
635 log.debug('Updating pull request because of change in %s detected',
639 log.debug('Updating pull request because of change in %s detected',
636 change_in_found)
640 change_in_found)
637
641
638 # Finally there is a need for an update, in case of source change
642 # Finally there is a need for an update, in case of source change
639 # we create a new version, else just an update
643 # we create a new version, else just an update
640 if source_changed:
644 if source_changed:
641 pull_request_version = self._create_version_from_snapshot(pull_request)
645 pull_request_version = self._create_version_from_snapshot(pull_request)
642 self._link_comments_to_version(pull_request_version)
646 self._link_comments_to_version(pull_request_version)
643 else:
647 else:
644 try:
648 try:
645 ver = pull_request.versions[-1]
649 ver = pull_request.versions[-1]
646 except IndexError:
650 except IndexError:
647 ver = None
651 ver = None
648
652
649 pull_request.pull_request_version_id = \
653 pull_request.pull_request_version_id = \
650 ver.pull_request_version_id if ver else None
654 ver.pull_request_version_id if ver else None
651 pull_request_version = pull_request
655 pull_request_version = pull_request
652
656
653 try:
657 try:
654 if target_ref_type in ('tag', 'branch', 'book'):
658 if target_ref_type in ('tag', 'branch', 'book'):
655 target_commit = target_repo.get_commit(target_ref_name)
659 target_commit = target_repo.get_commit(target_ref_name)
656 else:
660 else:
657 target_commit = target_repo.get_commit(target_ref_id)
661 target_commit = target_repo.get_commit(target_ref_id)
658 except CommitDoesNotExistError:
662 except CommitDoesNotExistError:
659 return UpdateResponse(
663 return UpdateResponse(
660 executed=False,
664 executed=False,
661 reason=UpdateFailureReason.MISSING_TARGET_REF,
665 reason=UpdateFailureReason.MISSING_TARGET_REF,
662 old=pull_request, new=None, changes=None,
666 old=pull_request, new=None, changes=None,
663 source_changed=source_changed, target_changed=target_changed)
667 source_changed=source_changed, target_changed=target_changed)
664
668
665 # re-compute commit ids
669 # re-compute commit ids
666 old_commit_ids = pull_request.revisions
670 old_commit_ids = pull_request.revisions
667 pre_load = ["author", "branch", "date", "message"]
671 pre_load = ["author", "branch", "date", "message"]
668 commit_ranges = target_repo.compare(
672 commit_ranges = target_repo.compare(
669 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
673 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
670 pre_load=pre_load)
674 pre_load=pre_load)
671
675
672 ancestor = target_repo.get_common_ancestor(
676 ancestor = target_repo.get_common_ancestor(
673 target_commit.raw_id, source_commit.raw_id, source_repo)
677 target_commit.raw_id, source_commit.raw_id, source_repo)
674
678
675 pull_request.source_ref = '%s:%s:%s' % (
679 pull_request.source_ref = '%s:%s:%s' % (
676 source_ref_type, source_ref_name, source_commit.raw_id)
680 source_ref_type, source_ref_name, source_commit.raw_id)
677 pull_request.target_ref = '%s:%s:%s' % (
681 pull_request.target_ref = '%s:%s:%s' % (
678 target_ref_type, target_ref_name, ancestor)
682 target_ref_type, target_ref_name, ancestor)
679
683
680 pull_request.revisions = [
684 pull_request.revisions = [
681 commit.raw_id for commit in reversed(commit_ranges)]
685 commit.raw_id for commit in reversed(commit_ranges)]
682 pull_request.updated_on = datetime.datetime.now()
686 pull_request.updated_on = datetime.datetime.now()
683 Session().add(pull_request)
687 Session().add(pull_request)
684 new_commit_ids = pull_request.revisions
688 new_commit_ids = pull_request.revisions
685
689
686 old_diff_data, new_diff_data = self._generate_update_diffs(
690 old_diff_data, new_diff_data = self._generate_update_diffs(
687 pull_request, pull_request_version)
691 pull_request, pull_request_version)
688
692
689 # calculate commit and file changes
693 # calculate commit and file changes
690 changes = self._calculate_commit_id_changes(
694 changes = self._calculate_commit_id_changes(
691 old_commit_ids, new_commit_ids)
695 old_commit_ids, new_commit_ids)
692 file_changes = self._calculate_file_changes(
696 file_changes = self._calculate_file_changes(
693 old_diff_data, new_diff_data)
697 old_diff_data, new_diff_data)
694
698
695 # set comments as outdated if DIFFS changed
699 # set comments as outdated if DIFFS changed
696 CommentsModel().outdate_comments(
700 CommentsModel().outdate_comments(
697 pull_request, old_diff_data=old_diff_data,
701 pull_request, old_diff_data=old_diff_data,
698 new_diff_data=new_diff_data)
702 new_diff_data=new_diff_data)
699
703
700 commit_changes = (changes.added or changes.removed)
704 commit_changes = (changes.added or changes.removed)
701 file_node_changes = (
705 file_node_changes = (
702 file_changes.added or file_changes.modified or file_changes.removed)
706 file_changes.added or file_changes.modified or file_changes.removed)
703 pr_has_changes = commit_changes or file_node_changes
707 pr_has_changes = commit_changes or file_node_changes
704
708
705 # Add an automatic comment to the pull request, in case
709 # Add an automatic comment to the pull request, in case
706 # anything has changed
710 # anything has changed
707 if pr_has_changes:
711 if pr_has_changes:
708 update_comment = CommentsModel().create(
712 update_comment = CommentsModel().create(
709 text=self._render_update_message(changes, file_changes),
713 text=self._render_update_message(changes, file_changes),
710 repo=pull_request.target_repo,
714 repo=pull_request.target_repo,
711 user=pull_request.author,
715 user=pull_request.author,
712 pull_request=pull_request,
716 pull_request=pull_request,
713 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
717 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
714
718
715 # Update status to "Under Review" for added commits
719 # Update status to "Under Review" for added commits
716 for commit_id in changes.added:
720 for commit_id in changes.added:
717 ChangesetStatusModel().set_status(
721 ChangesetStatusModel().set_status(
718 repo=pull_request.source_repo,
722 repo=pull_request.source_repo,
719 status=ChangesetStatus.STATUS_UNDER_REVIEW,
723 status=ChangesetStatus.STATUS_UNDER_REVIEW,
720 comment=update_comment,
724 comment=update_comment,
721 user=pull_request.author,
725 user=pull_request.author,
722 pull_request=pull_request,
726 pull_request=pull_request,
723 revision=commit_id)
727 revision=commit_id)
724
728
725 log.debug(
729 log.debug(
726 'Updated pull request %s, added_ids: %s, common_ids: %s, '
730 'Updated pull request %s, added_ids: %s, common_ids: %s, '
727 'removed_ids: %s', pull_request.pull_request_id,
731 'removed_ids: %s', pull_request.pull_request_id,
728 changes.added, changes.common, changes.removed)
732 changes.added, changes.common, changes.removed)
729 log.debug(
733 log.debug(
730 'Updated pull request with the following file changes: %s',
734 'Updated pull request with the following file changes: %s',
731 file_changes)
735 file_changes)
732
736
733 log.info(
737 log.info(
734 "Updated pull request %s from commit %s to commit %s, "
738 "Updated pull request %s from commit %s to commit %s, "
735 "stored new version %s of this pull request.",
739 "stored new version %s of this pull request.",
736 pull_request.pull_request_id, source_ref_id,
740 pull_request.pull_request_id, source_ref_id,
737 pull_request.source_ref_parts.commit_id,
741 pull_request.source_ref_parts.commit_id,
738 pull_request_version.pull_request_version_id)
742 pull_request_version.pull_request_version_id)
739 Session().commit()
743 Session().commit()
740 self._trigger_pull_request_hook(
744 self._trigger_pull_request_hook(
741 pull_request, pull_request.author, 'update')
745 pull_request, pull_request.author, 'update')
742
746
743 return UpdateResponse(
747 return UpdateResponse(
744 executed=True, reason=UpdateFailureReason.NONE,
748 executed=True, reason=UpdateFailureReason.NONE,
745 old=pull_request, new=pull_request_version, changes=changes,
749 old=pull_request, new=pull_request_version, changes=changes,
746 source_changed=source_changed, target_changed=target_changed)
750 source_changed=source_changed, target_changed=target_changed)
747
751
748 def _create_version_from_snapshot(self, pull_request):
752 def _create_version_from_snapshot(self, pull_request):
749 version = PullRequestVersion()
753 version = PullRequestVersion()
750 version.title = pull_request.title
754 version.title = pull_request.title
751 version.description = pull_request.description
755 version.description = pull_request.description
752 version.status = pull_request.status
756 version.status = pull_request.status
753 version.created_on = datetime.datetime.now()
757 version.created_on = datetime.datetime.now()
754 version.updated_on = pull_request.updated_on
758 version.updated_on = pull_request.updated_on
755 version.user_id = pull_request.user_id
759 version.user_id = pull_request.user_id
756 version.source_repo = pull_request.source_repo
760 version.source_repo = pull_request.source_repo
757 version.source_ref = pull_request.source_ref
761 version.source_ref = pull_request.source_ref
758 version.target_repo = pull_request.target_repo
762 version.target_repo = pull_request.target_repo
759 version.target_ref = pull_request.target_ref
763 version.target_ref = pull_request.target_ref
760
764
761 version._last_merge_source_rev = pull_request._last_merge_source_rev
765 version._last_merge_source_rev = pull_request._last_merge_source_rev
762 version._last_merge_target_rev = pull_request._last_merge_target_rev
766 version._last_merge_target_rev = pull_request._last_merge_target_rev
763 version._last_merge_status = pull_request._last_merge_status
767 version._last_merge_status = pull_request._last_merge_status
764 version.shadow_merge_ref = pull_request.shadow_merge_ref
768 version.shadow_merge_ref = pull_request.shadow_merge_ref
765 version.merge_rev = pull_request.merge_rev
769 version.merge_rev = pull_request.merge_rev
770 version.reviewer_data = pull_request.reviewer_data
766
771
767 version.revisions = pull_request.revisions
772 version.revisions = pull_request.revisions
768 version.pull_request = pull_request
773 version.pull_request = pull_request
769 Session().add(version)
774 Session().add(version)
770 Session().flush()
775 Session().flush()
771
776
772 return version
777 return version
773
778
774 def _generate_update_diffs(self, pull_request, pull_request_version):
779 def _generate_update_diffs(self, pull_request, pull_request_version):
775
780
776 diff_context = (
781 diff_context = (
777 self.DIFF_CONTEXT +
782 self.DIFF_CONTEXT +
778 CommentsModel.needed_extra_diff_context())
783 CommentsModel.needed_extra_diff_context())
779
784
780 source_repo = pull_request_version.source_repo
785 source_repo = pull_request_version.source_repo
781 source_ref_id = pull_request_version.source_ref_parts.commit_id
786 source_ref_id = pull_request_version.source_ref_parts.commit_id
782 target_ref_id = pull_request_version.target_ref_parts.commit_id
787 target_ref_id = pull_request_version.target_ref_parts.commit_id
783 old_diff = self._get_diff_from_pr_or_version(
788 old_diff = self._get_diff_from_pr_or_version(
784 source_repo, source_ref_id, target_ref_id, context=diff_context)
789 source_repo, source_ref_id, target_ref_id, context=diff_context)
785
790
786 source_repo = pull_request.source_repo
791 source_repo = pull_request.source_repo
787 source_ref_id = pull_request.source_ref_parts.commit_id
792 source_ref_id = pull_request.source_ref_parts.commit_id
788 target_ref_id = pull_request.target_ref_parts.commit_id
793 target_ref_id = pull_request.target_ref_parts.commit_id
789
794
790 new_diff = self._get_diff_from_pr_or_version(
795 new_diff = self._get_diff_from_pr_or_version(
791 source_repo, source_ref_id, target_ref_id, context=diff_context)
796 source_repo, source_ref_id, target_ref_id, context=diff_context)
792
797
793 old_diff_data = diffs.DiffProcessor(old_diff)
798 old_diff_data = diffs.DiffProcessor(old_diff)
794 old_diff_data.prepare()
799 old_diff_data.prepare()
795 new_diff_data = diffs.DiffProcessor(new_diff)
800 new_diff_data = diffs.DiffProcessor(new_diff)
796 new_diff_data.prepare()
801 new_diff_data.prepare()
797
802
798 return old_diff_data, new_diff_data
803 return old_diff_data, new_diff_data
799
804
800 def _link_comments_to_version(self, pull_request_version):
805 def _link_comments_to_version(self, pull_request_version):
801 """
806 """
802 Link all unlinked comments of this pull request to the given version.
807 Link all unlinked comments of this pull request to the given version.
803
808
804 :param pull_request_version: The `PullRequestVersion` to which
809 :param pull_request_version: The `PullRequestVersion` to which
805 the comments shall be linked.
810 the comments shall be linked.
806
811
807 """
812 """
808 pull_request = pull_request_version.pull_request
813 pull_request = pull_request_version.pull_request
809 comments = ChangesetComment.query()\
814 comments = ChangesetComment.query()\
810 .filter(
815 .filter(
811 # TODO: johbo: Should we query for the repo at all here?
816 # TODO: johbo: Should we query for the repo at all here?
812 # Pending decision on how comments of PRs are to be related
817 # Pending decision on how comments of PRs are to be related
813 # to either the source repo, the target repo or no repo at all.
818 # to either the source repo, the target repo or no repo at all.
814 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
819 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
815 ChangesetComment.pull_request == pull_request,
820 ChangesetComment.pull_request == pull_request,
816 ChangesetComment.pull_request_version == None)\
821 ChangesetComment.pull_request_version == None)\
817 .order_by(ChangesetComment.comment_id.asc())
822 .order_by(ChangesetComment.comment_id.asc())
818
823
819 # TODO: johbo: Find out why this breaks if it is done in a bulk
824 # TODO: johbo: Find out why this breaks if it is done in a bulk
820 # operation.
825 # operation.
821 for comment in comments:
826 for comment in comments:
822 comment.pull_request_version_id = (
827 comment.pull_request_version_id = (
823 pull_request_version.pull_request_version_id)
828 pull_request_version.pull_request_version_id)
824 Session().add(comment)
829 Session().add(comment)
825
830
826 def _calculate_commit_id_changes(self, old_ids, new_ids):
831 def _calculate_commit_id_changes(self, old_ids, new_ids):
827 added = [x for x in new_ids if x not in old_ids]
832 added = [x for x in new_ids if x not in old_ids]
828 common = [x for x in new_ids if x in old_ids]
833 common = [x for x in new_ids if x in old_ids]
829 removed = [x for x in old_ids if x not in new_ids]
834 removed = [x for x in old_ids if x not in new_ids]
830 total = new_ids
835 total = new_ids
831 return ChangeTuple(added, common, removed, total)
836 return ChangeTuple(added, common, removed, total)
832
837
833 def _calculate_file_changes(self, old_diff_data, new_diff_data):
838 def _calculate_file_changes(self, old_diff_data, new_diff_data):
834
839
835 old_files = OrderedDict()
840 old_files = OrderedDict()
836 for diff_data in old_diff_data.parsed_diff:
841 for diff_data in old_diff_data.parsed_diff:
837 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
842 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
838
843
839 added_files = []
844 added_files = []
840 modified_files = []
845 modified_files = []
841 removed_files = []
846 removed_files = []
842 for diff_data in new_diff_data.parsed_diff:
847 for diff_data in new_diff_data.parsed_diff:
843 new_filename = diff_data['filename']
848 new_filename = diff_data['filename']
844 new_hash = md5_safe(diff_data['raw_diff'])
849 new_hash = md5_safe(diff_data['raw_diff'])
845
850
846 old_hash = old_files.get(new_filename)
851 old_hash = old_files.get(new_filename)
847 if not old_hash:
852 if not old_hash:
848 # file is not present in old diff, means it's added
853 # file is not present in old diff, means it's added
849 added_files.append(new_filename)
854 added_files.append(new_filename)
850 else:
855 else:
851 if new_hash != old_hash:
856 if new_hash != old_hash:
852 modified_files.append(new_filename)
857 modified_files.append(new_filename)
853 # now remove a file from old, since we have seen it already
858 # now remove a file from old, since we have seen it already
854 del old_files[new_filename]
859 del old_files[new_filename]
855
860
856 # removed files is when there are present in old, but not in NEW,
861 # removed files is when there are present in old, but not in NEW,
857 # since we remove old files that are present in new diff, left-overs
862 # since we remove old files that are present in new diff, left-overs
858 # if any should be the removed files
863 # if any should be the removed files
859 removed_files.extend(old_files.keys())
864 removed_files.extend(old_files.keys())
860
865
861 return FileChangeTuple(added_files, modified_files, removed_files)
866 return FileChangeTuple(added_files, modified_files, removed_files)
862
867
863 def _render_update_message(self, changes, file_changes):
868 def _render_update_message(self, changes, file_changes):
864 """
869 """
865 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
870 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
866 so it's always looking the same disregarding on which default
871 so it's always looking the same disregarding on which default
867 renderer system is using.
872 renderer system is using.
868
873
869 :param changes: changes named tuple
874 :param changes: changes named tuple
870 :param file_changes: file changes named tuple
875 :param file_changes: file changes named tuple
871
876
872 """
877 """
873 new_status = ChangesetStatus.get_status_lbl(
878 new_status = ChangesetStatus.get_status_lbl(
874 ChangesetStatus.STATUS_UNDER_REVIEW)
879 ChangesetStatus.STATUS_UNDER_REVIEW)
875
880
876 changed_files = (
881 changed_files = (
877 file_changes.added + file_changes.modified + file_changes.removed)
882 file_changes.added + file_changes.modified + file_changes.removed)
878
883
879 params = {
884 params = {
880 'under_review_label': new_status,
885 'under_review_label': new_status,
881 'added_commits': changes.added,
886 'added_commits': changes.added,
882 'removed_commits': changes.removed,
887 'removed_commits': changes.removed,
883 'changed_files': changed_files,
888 'changed_files': changed_files,
884 'added_files': file_changes.added,
889 'added_files': file_changes.added,
885 'modified_files': file_changes.modified,
890 'modified_files': file_changes.modified,
886 'removed_files': file_changes.removed,
891 'removed_files': file_changes.removed,
887 }
892 }
888 renderer = RstTemplateRenderer()
893 renderer = RstTemplateRenderer()
889 return renderer.render('pull_request_update.mako', **params)
894 return renderer.render('pull_request_update.mako', **params)
890
895
891 def edit(self, pull_request, title, description):
896 def edit(self, pull_request, title, description):
892 pull_request = self.__get_pull_request(pull_request)
897 pull_request = self.__get_pull_request(pull_request)
893 if pull_request.is_closed():
898 if pull_request.is_closed():
894 raise ValueError('This pull request is closed')
899 raise ValueError('This pull request is closed')
895 if title:
900 if title:
896 pull_request.title = title
901 pull_request.title = title
897 pull_request.description = description
902 pull_request.description = description
898 pull_request.updated_on = datetime.datetime.now()
903 pull_request.updated_on = datetime.datetime.now()
899 Session().add(pull_request)
904 Session().add(pull_request)
900
905
901 def update_reviewers(self, pull_request, reviewer_data):
906 def update_reviewers(self, pull_request, reviewer_data):
902 """
907 """
903 Update the reviewers in the pull request
908 Update the reviewers in the pull request
904
909
905 :param pull_request: the pr to update
910 :param pull_request: the pr to update
906 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
911 :param reviewer_data: list of tuples
912 [(user, ['reason1', 'reason2'], mandatory_flag)]
907 """
913 """
908
914
909 reviewers_reasons = {}
915 reviewers = {}
910 for user_id, reasons in reviewer_data:
916 for user_id, reasons, mandatory in reviewer_data:
911 if isinstance(user_id, (int, basestring)):
917 if isinstance(user_id, (int, basestring)):
912 user_id = self._get_user(user_id).user_id
918 user_id = self._get_user(user_id).user_id
913 reviewers_reasons[user_id] = reasons
919 reviewers[user_id] = {
920 'reasons': reasons, 'mandatory': mandatory}
914
921
915 reviewers_ids = set(reviewers_reasons.keys())
922 reviewers_ids = set(reviewers.keys())
916 pull_request = self.__get_pull_request(pull_request)
923 pull_request = self.__get_pull_request(pull_request)
917 current_reviewers = PullRequestReviewers.query()\
924 current_reviewers = PullRequestReviewers.query()\
918 .filter(PullRequestReviewers.pull_request ==
925 .filter(PullRequestReviewers.pull_request ==
919 pull_request).all()
926 pull_request).all()
920 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
927 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
921
928
922 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
929 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
923 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
930 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
924
931
925 log.debug("Adding %s reviewers", ids_to_add)
932 log.debug("Adding %s reviewers", ids_to_add)
926 log.debug("Removing %s reviewers", ids_to_remove)
933 log.debug("Removing %s reviewers", ids_to_remove)
927 changed = False
934 changed = False
928 for uid in ids_to_add:
935 for uid in ids_to_add:
929 changed = True
936 changed = True
930 _usr = self._get_user(uid)
937 _usr = self._get_user(uid)
931 reasons = reviewers_reasons[uid]
938 reviewer = PullRequestReviewers()
932 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
939 reviewer.user = _usr
940 reviewer.pull_request = pull_request
941 reviewer.reasons = reviewers[uid]['reasons']
942 # NOTE(marcink): mandatory shouldn't be changed now
943 #reviewer.mandatory = reviewers[uid]['reasons']
933 Session().add(reviewer)
944 Session().add(reviewer)
934
945
935 for uid in ids_to_remove:
946 for uid in ids_to_remove:
936 changed = True
947 changed = True
937 reviewers = PullRequestReviewers.query()\
948 reviewers = PullRequestReviewers.query()\
938 .filter(PullRequestReviewers.user_id == uid,
949 .filter(PullRequestReviewers.user_id == uid,
939 PullRequestReviewers.pull_request == pull_request)\
950 PullRequestReviewers.pull_request == pull_request)\
940 .all()
951 .all()
941 # use .all() in case we accidentally added the same person twice
952 # use .all() in case we accidentally added the same person twice
942 # this CAN happen due to the lack of DB checks
953 # this CAN happen due to the lack of DB checks
943 for obj in reviewers:
954 for obj in reviewers:
944 Session().delete(obj)
955 Session().delete(obj)
945
956
946 if changed:
957 if changed:
947 pull_request.updated_on = datetime.datetime.now()
958 pull_request.updated_on = datetime.datetime.now()
948 Session().add(pull_request)
959 Session().add(pull_request)
949
960
950 self.notify_reviewers(pull_request, ids_to_add)
961 self.notify_reviewers(pull_request, ids_to_add)
951 return ids_to_add, ids_to_remove
962 return ids_to_add, ids_to_remove
952
963
953 def get_url(self, pull_request):
964 def get_url(self, pull_request):
954 return h.url('pullrequest_show',
965 return h.url('pullrequest_show',
955 repo_name=safe_str(pull_request.target_repo.repo_name),
966 repo_name=safe_str(pull_request.target_repo.repo_name),
956 pull_request_id=pull_request.pull_request_id,
967 pull_request_id=pull_request.pull_request_id,
957 qualified=True)
968 qualified=True)
958
969
959 def get_shadow_clone_url(self, pull_request):
970 def get_shadow_clone_url(self, pull_request):
960 """
971 """
961 Returns qualified url pointing to the shadow repository. If this pull
972 Returns qualified url pointing to the shadow repository. If this pull
962 request is closed there is no shadow repository and ``None`` will be
973 request is closed there is no shadow repository and ``None`` will be
963 returned.
974 returned.
964 """
975 """
965 if pull_request.is_closed():
976 if pull_request.is_closed():
966 return None
977 return None
967 else:
978 else:
968 pr_url = urllib.unquote(self.get_url(pull_request))
979 pr_url = urllib.unquote(self.get_url(pull_request))
969 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
980 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
970
981
971 def notify_reviewers(self, pull_request, reviewers_ids):
982 def notify_reviewers(self, pull_request, reviewers_ids):
972 # notification to reviewers
983 # notification to reviewers
973 if not reviewers_ids:
984 if not reviewers_ids:
974 return
985 return
975
986
976 pull_request_obj = pull_request
987 pull_request_obj = pull_request
977 # get the current participants of this pull request
988 # get the current participants of this pull request
978 recipients = reviewers_ids
989 recipients = reviewers_ids
979 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
990 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
980
991
981 pr_source_repo = pull_request_obj.source_repo
992 pr_source_repo = pull_request_obj.source_repo
982 pr_target_repo = pull_request_obj.target_repo
993 pr_target_repo = pull_request_obj.target_repo
983
994
984 pr_url = h.url(
995 pr_url = h.url(
985 'pullrequest_show',
996 'pullrequest_show',
986 repo_name=pr_target_repo.repo_name,
997 repo_name=pr_target_repo.repo_name,
987 pull_request_id=pull_request_obj.pull_request_id,
998 pull_request_id=pull_request_obj.pull_request_id,
988 qualified=True,)
999 qualified=True,)
989
1000
990 # set some variables for email notification
1001 # set some variables for email notification
991 pr_target_repo_url = h.url(
1002 pr_target_repo_url = h.url(
992 'summary_home',
1003 'summary_home',
993 repo_name=pr_target_repo.repo_name,
1004 repo_name=pr_target_repo.repo_name,
994 qualified=True)
1005 qualified=True)
995
1006
996 pr_source_repo_url = h.url(
1007 pr_source_repo_url = h.url(
997 'summary_home',
1008 'summary_home',
998 repo_name=pr_source_repo.repo_name,
1009 repo_name=pr_source_repo.repo_name,
999 qualified=True)
1010 qualified=True)
1000
1011
1001 # pull request specifics
1012 # pull request specifics
1002 pull_request_commits = [
1013 pull_request_commits = [
1003 (x.raw_id, x.message)
1014 (x.raw_id, x.message)
1004 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1015 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1005
1016
1006 kwargs = {
1017 kwargs = {
1007 'user': pull_request.author,
1018 'user': pull_request.author,
1008 'pull_request': pull_request_obj,
1019 'pull_request': pull_request_obj,
1009 'pull_request_commits': pull_request_commits,
1020 'pull_request_commits': pull_request_commits,
1010
1021
1011 'pull_request_target_repo': pr_target_repo,
1022 'pull_request_target_repo': pr_target_repo,
1012 'pull_request_target_repo_url': pr_target_repo_url,
1023 'pull_request_target_repo_url': pr_target_repo_url,
1013
1024
1014 'pull_request_source_repo': pr_source_repo,
1025 'pull_request_source_repo': pr_source_repo,
1015 'pull_request_source_repo_url': pr_source_repo_url,
1026 'pull_request_source_repo_url': pr_source_repo_url,
1016
1027
1017 'pull_request_url': pr_url,
1028 'pull_request_url': pr_url,
1018 }
1029 }
1019
1030
1020 # pre-generate the subject for notification itself
1031 # pre-generate the subject for notification itself
1021 (subject,
1032 (subject,
1022 _h, _e, # we don't care about those
1033 _h, _e, # we don't care about those
1023 body_plaintext) = EmailNotificationModel().render_email(
1034 body_plaintext) = EmailNotificationModel().render_email(
1024 notification_type, **kwargs)
1035 notification_type, **kwargs)
1025
1036
1026 # create notification objects, and emails
1037 # create notification objects, and emails
1027 NotificationModel().create(
1038 NotificationModel().create(
1028 created_by=pull_request.author,
1039 created_by=pull_request.author,
1029 notification_subject=subject,
1040 notification_subject=subject,
1030 notification_body=body_plaintext,
1041 notification_body=body_plaintext,
1031 notification_type=notification_type,
1042 notification_type=notification_type,
1032 recipients=recipients,
1043 recipients=recipients,
1033 email_kwargs=kwargs,
1044 email_kwargs=kwargs,
1034 )
1045 )
1035
1046
1036 def delete(self, pull_request):
1047 def delete(self, pull_request):
1037 pull_request = self.__get_pull_request(pull_request)
1048 pull_request = self.__get_pull_request(pull_request)
1038 self._cleanup_merge_workspace(pull_request)
1049 self._cleanup_merge_workspace(pull_request)
1039 Session().delete(pull_request)
1050 Session().delete(pull_request)
1040
1051
1041 def close_pull_request(self, pull_request, user):
1052 def close_pull_request(self, pull_request, user):
1042 pull_request = self.__get_pull_request(pull_request)
1053 pull_request = self.__get_pull_request(pull_request)
1043 self._cleanup_merge_workspace(pull_request)
1054 self._cleanup_merge_workspace(pull_request)
1044 pull_request.status = PullRequest.STATUS_CLOSED
1055 pull_request.status = PullRequest.STATUS_CLOSED
1045 pull_request.updated_on = datetime.datetime.now()
1056 pull_request.updated_on = datetime.datetime.now()
1046 Session().add(pull_request)
1057 Session().add(pull_request)
1047 self._trigger_pull_request_hook(
1058 self._trigger_pull_request_hook(
1048 pull_request, pull_request.author, 'close')
1059 pull_request, pull_request.author, 'close')
1049 self._log_action('user_closed_pull_request', user, pull_request)
1060 self._log_action('user_closed_pull_request', user, pull_request)
1050
1061
1051 def close_pull_request_with_comment(self, pull_request, user, repo,
1062 def close_pull_request_with_comment(self, pull_request, user, repo,
1052 message=None):
1063 message=None):
1053 status = ChangesetStatus.STATUS_REJECTED
1064 status = ChangesetStatus.STATUS_REJECTED
1054
1065
1055 if not message:
1066 if not message:
1056 message = (
1067 message = (
1057 _('Status change %(transition_icon)s %(status)s') % {
1068 _('Status change %(transition_icon)s %(status)s') % {
1058 'transition_icon': '>',
1069 'transition_icon': '>',
1059 'status': ChangesetStatus.get_status_lbl(status)})
1070 'status': ChangesetStatus.get_status_lbl(status)})
1060
1071
1061 internal_message = _('Closing with') + ' ' + message
1072 internal_message = _('Closing with') + ' ' + message
1062
1073
1063 comm = CommentsModel().create(
1074 comm = CommentsModel().create(
1064 text=internal_message,
1075 text=internal_message,
1065 repo=repo.repo_id,
1076 repo=repo.repo_id,
1066 user=user.user_id,
1077 user=user.user_id,
1067 pull_request=pull_request.pull_request_id,
1078 pull_request=pull_request.pull_request_id,
1068 f_path=None,
1079 f_path=None,
1069 line_no=None,
1080 line_no=None,
1070 status_change=ChangesetStatus.get_status_lbl(status),
1081 status_change=ChangesetStatus.get_status_lbl(status),
1071 status_change_type=status,
1082 status_change_type=status,
1072 closing_pr=True
1083 closing_pr=True
1073 )
1084 )
1074
1085
1075 ChangesetStatusModel().set_status(
1086 ChangesetStatusModel().set_status(
1076 repo.repo_id,
1087 repo.repo_id,
1077 status,
1088 status,
1078 user.user_id,
1089 user.user_id,
1079 comm,
1090 comm,
1080 pull_request=pull_request.pull_request_id
1091 pull_request=pull_request.pull_request_id
1081 )
1092 )
1082 Session().flush()
1093 Session().flush()
1083
1094
1084 PullRequestModel().close_pull_request(
1095 PullRequestModel().close_pull_request(
1085 pull_request.pull_request_id, user)
1096 pull_request.pull_request_id, user)
1086
1097
1087 def merge_status(self, pull_request):
1098 def merge_status(self, pull_request):
1088 if not self._is_merge_enabled(pull_request):
1099 if not self._is_merge_enabled(pull_request):
1089 return False, _('Server-side pull request merging is disabled.')
1100 return False, _('Server-side pull request merging is disabled.')
1090 if pull_request.is_closed():
1101 if pull_request.is_closed():
1091 return False, _('This pull request is closed.')
1102 return False, _('This pull request is closed.')
1092 merge_possible, msg = self._check_repo_requirements(
1103 merge_possible, msg = self._check_repo_requirements(
1093 target=pull_request.target_repo, source=pull_request.source_repo)
1104 target=pull_request.target_repo, source=pull_request.source_repo)
1094 if not merge_possible:
1105 if not merge_possible:
1095 return merge_possible, msg
1106 return merge_possible, msg
1096
1107
1097 try:
1108 try:
1098 resp = self._try_merge(pull_request)
1109 resp = self._try_merge(pull_request)
1099 log.debug("Merge response: %s", resp)
1110 log.debug("Merge response: %s", resp)
1100 status = resp.possible, self.merge_status_message(
1111 status = resp.possible, self.merge_status_message(
1101 resp.failure_reason)
1112 resp.failure_reason)
1102 except NotImplementedError:
1113 except NotImplementedError:
1103 status = False, _('Pull request merging is not supported.')
1114 status = False, _('Pull request merging is not supported.')
1104
1115
1105 return status
1116 return status
1106
1117
1107 def _check_repo_requirements(self, target, source):
1118 def _check_repo_requirements(self, target, source):
1108 """
1119 """
1109 Check if `target` and `source` have compatible requirements.
1120 Check if `target` and `source` have compatible requirements.
1110
1121
1111 Currently this is just checking for largefiles.
1122 Currently this is just checking for largefiles.
1112 """
1123 """
1113 target_has_largefiles = self._has_largefiles(target)
1124 target_has_largefiles = self._has_largefiles(target)
1114 source_has_largefiles = self._has_largefiles(source)
1125 source_has_largefiles = self._has_largefiles(source)
1115 merge_possible = True
1126 merge_possible = True
1116 message = u''
1127 message = u''
1117
1128
1118 if target_has_largefiles != source_has_largefiles:
1129 if target_has_largefiles != source_has_largefiles:
1119 merge_possible = False
1130 merge_possible = False
1120 if source_has_largefiles:
1131 if source_has_largefiles:
1121 message = _(
1132 message = _(
1122 'Target repository large files support is disabled.')
1133 'Target repository large files support is disabled.')
1123 else:
1134 else:
1124 message = _(
1135 message = _(
1125 'Source repository large files support is disabled.')
1136 'Source repository large files support is disabled.')
1126
1137
1127 return merge_possible, message
1138 return merge_possible, message
1128
1139
1129 def _has_largefiles(self, repo):
1140 def _has_largefiles(self, repo):
1130 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1141 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1131 'extensions', 'largefiles')
1142 'extensions', 'largefiles')
1132 return largefiles_ui and largefiles_ui[0].active
1143 return largefiles_ui and largefiles_ui[0].active
1133
1144
1134 def _try_merge(self, pull_request):
1145 def _try_merge(self, pull_request):
1135 """
1146 """
1136 Try to merge the pull request and return the merge status.
1147 Try to merge the pull request and return the merge status.
1137 """
1148 """
1138 log.debug(
1149 log.debug(
1139 "Trying out if the pull request %s can be merged.",
1150 "Trying out if the pull request %s can be merged.",
1140 pull_request.pull_request_id)
1151 pull_request.pull_request_id)
1141 target_vcs = pull_request.target_repo.scm_instance()
1152 target_vcs = pull_request.target_repo.scm_instance()
1142
1153
1143 # Refresh the target reference.
1154 # Refresh the target reference.
1144 try:
1155 try:
1145 target_ref = self._refresh_reference(
1156 target_ref = self._refresh_reference(
1146 pull_request.target_ref_parts, target_vcs)
1157 pull_request.target_ref_parts, target_vcs)
1147 except CommitDoesNotExistError:
1158 except CommitDoesNotExistError:
1148 merge_state = MergeResponse(
1159 merge_state = MergeResponse(
1149 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1160 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1150 return merge_state
1161 return merge_state
1151
1162
1152 target_locked = pull_request.target_repo.locked
1163 target_locked = pull_request.target_repo.locked
1153 if target_locked and target_locked[0]:
1164 if target_locked and target_locked[0]:
1154 log.debug("The target repository is locked.")
1165 log.debug("The target repository is locked.")
1155 merge_state = MergeResponse(
1166 merge_state = MergeResponse(
1156 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1167 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1157 elif self._needs_merge_state_refresh(pull_request, target_ref):
1168 elif self._needs_merge_state_refresh(pull_request, target_ref):
1158 log.debug("Refreshing the merge status of the repository.")
1169 log.debug("Refreshing the merge status of the repository.")
1159 merge_state = self._refresh_merge_state(
1170 merge_state = self._refresh_merge_state(
1160 pull_request, target_vcs, target_ref)
1171 pull_request, target_vcs, target_ref)
1161 else:
1172 else:
1162 possible = pull_request.\
1173 possible = pull_request.\
1163 _last_merge_status == MergeFailureReason.NONE
1174 _last_merge_status == MergeFailureReason.NONE
1164 merge_state = MergeResponse(
1175 merge_state = MergeResponse(
1165 possible, False, None, pull_request._last_merge_status)
1176 possible, False, None, pull_request._last_merge_status)
1166
1177
1167 return merge_state
1178 return merge_state
1168
1179
1169 def _refresh_reference(self, reference, vcs_repository):
1180 def _refresh_reference(self, reference, vcs_repository):
1170 if reference.type in ('branch', 'book'):
1181 if reference.type in ('branch', 'book'):
1171 name_or_id = reference.name
1182 name_or_id = reference.name
1172 else:
1183 else:
1173 name_or_id = reference.commit_id
1184 name_or_id = reference.commit_id
1174 refreshed_commit = vcs_repository.get_commit(name_or_id)
1185 refreshed_commit = vcs_repository.get_commit(name_or_id)
1175 refreshed_reference = Reference(
1186 refreshed_reference = Reference(
1176 reference.type, reference.name, refreshed_commit.raw_id)
1187 reference.type, reference.name, refreshed_commit.raw_id)
1177 return refreshed_reference
1188 return refreshed_reference
1178
1189
1179 def _needs_merge_state_refresh(self, pull_request, target_reference):
1190 def _needs_merge_state_refresh(self, pull_request, target_reference):
1180 return not(
1191 return not(
1181 pull_request.revisions and
1192 pull_request.revisions and
1182 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1193 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1183 target_reference.commit_id == pull_request._last_merge_target_rev)
1194 target_reference.commit_id == pull_request._last_merge_target_rev)
1184
1195
1185 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1196 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1186 workspace_id = self._workspace_id(pull_request)
1197 workspace_id = self._workspace_id(pull_request)
1187 source_vcs = pull_request.source_repo.scm_instance()
1198 source_vcs = pull_request.source_repo.scm_instance()
1188 use_rebase = self._use_rebase_for_merging(pull_request)
1199 use_rebase = self._use_rebase_for_merging(pull_request)
1189 merge_state = target_vcs.merge(
1200 merge_state = target_vcs.merge(
1190 target_reference, source_vcs, pull_request.source_ref_parts,
1201 target_reference, source_vcs, pull_request.source_ref_parts,
1191 workspace_id, dry_run=True, use_rebase=use_rebase)
1202 workspace_id, dry_run=True, use_rebase=use_rebase)
1192
1203
1193 # Do not store the response if there was an unknown error.
1204 # Do not store the response if there was an unknown error.
1194 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1205 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1195 pull_request._last_merge_source_rev = \
1206 pull_request._last_merge_source_rev = \
1196 pull_request.source_ref_parts.commit_id
1207 pull_request.source_ref_parts.commit_id
1197 pull_request._last_merge_target_rev = target_reference.commit_id
1208 pull_request._last_merge_target_rev = target_reference.commit_id
1198 pull_request._last_merge_status = merge_state.failure_reason
1209 pull_request._last_merge_status = merge_state.failure_reason
1199 pull_request.shadow_merge_ref = merge_state.merge_ref
1210 pull_request.shadow_merge_ref = merge_state.merge_ref
1200 Session().add(pull_request)
1211 Session().add(pull_request)
1201 Session().commit()
1212 Session().commit()
1202
1213
1203 return merge_state
1214 return merge_state
1204
1215
1205 def _workspace_id(self, pull_request):
1216 def _workspace_id(self, pull_request):
1206 workspace_id = 'pr-%s' % pull_request.pull_request_id
1217 workspace_id = 'pr-%s' % pull_request.pull_request_id
1207 return workspace_id
1218 return workspace_id
1208
1219
1209 def merge_status_message(self, status_code):
1220 def merge_status_message(self, status_code):
1210 """
1221 """
1211 Return a human friendly error message for the given merge status code.
1222 Return a human friendly error message for the given merge status code.
1212 """
1223 """
1213 return self.MERGE_STATUS_MESSAGES[status_code]
1224 return self.MERGE_STATUS_MESSAGES[status_code]
1214
1225
1215 def generate_repo_data(self, repo, commit_id=None, branch=None,
1226 def generate_repo_data(self, repo, commit_id=None, branch=None,
1216 bookmark=None):
1227 bookmark=None):
1217 all_refs, selected_ref = \
1228 all_refs, selected_ref = \
1218 self._get_repo_pullrequest_sources(
1229 self._get_repo_pullrequest_sources(
1219 repo.scm_instance(), commit_id=commit_id,
1230 repo.scm_instance(), commit_id=commit_id,
1220 branch=branch, bookmark=bookmark)
1231 branch=branch, bookmark=bookmark)
1221
1232
1222 refs_select2 = []
1233 refs_select2 = []
1223 for element in all_refs:
1234 for element in all_refs:
1224 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1235 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1225 refs_select2.append({'text': element[1], 'children': children})
1236 refs_select2.append({'text': element[1], 'children': children})
1226
1237
1227 return {
1238 return {
1228 'user': {
1239 'user': {
1229 'user_id': repo.user.user_id,
1240 'user_id': repo.user.user_id,
1230 'username': repo.user.username,
1241 'username': repo.user.username,
1231 'firstname': repo.user.firstname,
1242 'firstname': repo.user.firstname,
1232 'lastname': repo.user.lastname,
1243 'lastname': repo.user.lastname,
1233 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1244 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1234 },
1245 },
1235 'description': h.chop_at_smart(repo.description, '\n'),
1246 'description': h.chop_at_smart(repo.description, '\n'),
1236 'refs': {
1247 'refs': {
1237 'all_refs': all_refs,
1248 'all_refs': all_refs,
1238 'selected_ref': selected_ref,
1249 'selected_ref': selected_ref,
1239 'select2_refs': refs_select2
1250 'select2_refs': refs_select2
1240 }
1251 }
1241 }
1252 }
1242
1253
1243 def generate_pullrequest_title(self, source, source_ref, target):
1254 def generate_pullrequest_title(self, source, source_ref, target):
1244 return u'{source}#{at_ref} to {target}'.format(
1255 return u'{source}#{at_ref} to {target}'.format(
1245 source=source,
1256 source=source,
1246 at_ref=source_ref,
1257 at_ref=source_ref,
1247 target=target,
1258 target=target,
1248 )
1259 )
1249
1260
1250 def _cleanup_merge_workspace(self, pull_request):
1261 def _cleanup_merge_workspace(self, pull_request):
1251 # Merging related cleanup
1262 # Merging related cleanup
1252 target_scm = pull_request.target_repo.scm_instance()
1263 target_scm = pull_request.target_repo.scm_instance()
1253 workspace_id = 'pr-%s' % pull_request.pull_request_id
1264 workspace_id = 'pr-%s' % pull_request.pull_request_id
1254
1265
1255 try:
1266 try:
1256 target_scm.cleanup_merge_workspace(workspace_id)
1267 target_scm.cleanup_merge_workspace(workspace_id)
1257 except NotImplementedError:
1268 except NotImplementedError:
1258 pass
1269 pass
1259
1270
1260 def _get_repo_pullrequest_sources(
1271 def _get_repo_pullrequest_sources(
1261 self, repo, commit_id=None, branch=None, bookmark=None):
1272 self, repo, commit_id=None, branch=None, bookmark=None):
1262 """
1273 """
1263 Return a structure with repo's interesting commits, suitable for
1274 Return a structure with repo's interesting commits, suitable for
1264 the selectors in pullrequest controller
1275 the selectors in pullrequest controller
1265
1276
1266 :param commit_id: a commit that must be in the list somehow
1277 :param commit_id: a commit that must be in the list somehow
1267 and selected by default
1278 and selected by default
1268 :param branch: a branch that must be in the list and selected
1279 :param branch: a branch that must be in the list and selected
1269 by default - even if closed
1280 by default - even if closed
1270 :param bookmark: a bookmark that must be in the list and selected
1281 :param bookmark: a bookmark that must be in the list and selected
1271 """
1282 """
1272
1283
1273 commit_id = safe_str(commit_id) if commit_id else None
1284 commit_id = safe_str(commit_id) if commit_id else None
1274 branch = safe_str(branch) if branch else None
1285 branch = safe_str(branch) if branch else None
1275 bookmark = safe_str(bookmark) if bookmark else None
1286 bookmark = safe_str(bookmark) if bookmark else None
1276
1287
1277 selected = None
1288 selected = None
1278
1289
1279 # order matters: first source that has commit_id in it will be selected
1290 # order matters: first source that has commit_id in it will be selected
1280 sources = []
1291 sources = []
1281 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1292 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1282 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1293 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1283
1294
1284 if commit_id:
1295 if commit_id:
1285 ref_commit = (h.short_id(commit_id), commit_id)
1296 ref_commit = (h.short_id(commit_id), commit_id)
1286 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1297 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1287
1298
1288 sources.append(
1299 sources.append(
1289 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1300 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1290 )
1301 )
1291
1302
1292 groups = []
1303 groups = []
1293 for group_key, ref_list, group_name, match in sources:
1304 for group_key, ref_list, group_name, match in sources:
1294 group_refs = []
1305 group_refs = []
1295 for ref_name, ref_id in ref_list:
1306 for ref_name, ref_id in ref_list:
1296 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1307 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1297 group_refs.append((ref_key, ref_name))
1308 group_refs.append((ref_key, ref_name))
1298
1309
1299 if not selected:
1310 if not selected:
1300 if set([commit_id, match]) & set([ref_id, ref_name]):
1311 if set([commit_id, match]) & set([ref_id, ref_name]):
1301 selected = ref_key
1312 selected = ref_key
1302
1313
1303 if group_refs:
1314 if group_refs:
1304 groups.append((group_refs, group_name))
1315 groups.append((group_refs, group_name))
1305
1316
1306 if not selected:
1317 if not selected:
1307 ref = commit_id or branch or bookmark
1318 ref = commit_id or branch or bookmark
1308 if ref:
1319 if ref:
1309 raise CommitDoesNotExistError(
1320 raise CommitDoesNotExistError(
1310 'No commit refs could be found matching: %s' % ref)
1321 'No commit refs could be found matching: %s' % ref)
1311 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1322 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1312 selected = 'branch:%s:%s' % (
1323 selected = 'branch:%s:%s' % (
1313 repo.DEFAULT_BRANCH_NAME,
1324 repo.DEFAULT_BRANCH_NAME,
1314 repo.branches[repo.DEFAULT_BRANCH_NAME]
1325 repo.branches[repo.DEFAULT_BRANCH_NAME]
1315 )
1326 )
1316 elif repo.commit_ids:
1327 elif repo.commit_ids:
1317 rev = repo.commit_ids[0]
1328 rev = repo.commit_ids[0]
1318 selected = 'rev:%s:%s' % (rev, rev)
1329 selected = 'rev:%s:%s' % (rev, rev)
1319 else:
1330 else:
1320 raise EmptyRepositoryError()
1331 raise EmptyRepositoryError()
1321 return groups, selected
1332 return groups, selected
1322
1333
1323 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1334 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1324 return self._get_diff_from_pr_or_version(
1335 return self._get_diff_from_pr_or_version(
1325 source_repo, source_ref_id, target_ref_id, context=context)
1336 source_repo, source_ref_id, target_ref_id, context=context)
1326
1337
1327 def _get_diff_from_pr_or_version(
1338 def _get_diff_from_pr_or_version(
1328 self, source_repo, source_ref_id, target_ref_id, context):
1339 self, source_repo, source_ref_id, target_ref_id, context):
1329 target_commit = source_repo.get_commit(
1340 target_commit = source_repo.get_commit(
1330 commit_id=safe_str(target_ref_id))
1341 commit_id=safe_str(target_ref_id))
1331 source_commit = source_repo.get_commit(
1342 source_commit = source_repo.get_commit(
1332 commit_id=safe_str(source_ref_id))
1343 commit_id=safe_str(source_ref_id))
1333 if isinstance(source_repo, Repository):
1344 if isinstance(source_repo, Repository):
1334 vcs_repo = source_repo.scm_instance()
1345 vcs_repo = source_repo.scm_instance()
1335 else:
1346 else:
1336 vcs_repo = source_repo
1347 vcs_repo = source_repo
1337
1348
1338 # TODO: johbo: In the context of an update, we cannot reach
1349 # TODO: johbo: In the context of an update, we cannot reach
1339 # the old commit anymore with our normal mechanisms. It needs
1350 # the old commit anymore with our normal mechanisms. It needs
1340 # some sort of special support in the vcs layer to avoid this
1351 # some sort of special support in the vcs layer to avoid this
1341 # workaround.
1352 # workaround.
1342 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1353 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1343 vcs_repo.alias == 'git'):
1354 vcs_repo.alias == 'git'):
1344 source_commit.raw_id = safe_str(source_ref_id)
1355 source_commit.raw_id = safe_str(source_ref_id)
1345
1356
1346 log.debug('calculating diff between '
1357 log.debug('calculating diff between '
1347 'source_ref:%s and target_ref:%s for repo `%s`',
1358 'source_ref:%s and target_ref:%s for repo `%s`',
1348 target_ref_id, source_ref_id,
1359 target_ref_id, source_ref_id,
1349 safe_unicode(vcs_repo.path))
1360 safe_unicode(vcs_repo.path))
1350
1361
1351 vcs_diff = vcs_repo.get_diff(
1362 vcs_diff = vcs_repo.get_diff(
1352 commit1=target_commit, commit2=source_commit, context=context)
1363 commit1=target_commit, commit2=source_commit, context=context)
1353 return vcs_diff
1364 return vcs_diff
1354
1365
1355 def _is_merge_enabled(self, pull_request):
1366 def _is_merge_enabled(self, pull_request):
1356 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1367 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1357 settings = settings_model.get_general_settings()
1368 settings = settings_model.get_general_settings()
1358 return settings.get('rhodecode_pr_merge_enabled', False)
1369 return settings.get('rhodecode_pr_merge_enabled', False)
1359
1370
1360 def _use_rebase_for_merging(self, pull_request):
1371 def _use_rebase_for_merging(self, pull_request):
1361 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1372 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1362 settings = settings_model.get_general_settings()
1373 settings = settings_model.get_general_settings()
1363 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1374 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1364
1375
1365 def _log_action(self, action, user, pull_request):
1376 def _log_action(self, action, user, pull_request):
1366 action_logger(
1377 action_logger(
1367 user,
1378 user,
1368 '{action}:{pr_id}'.format(
1379 '{action}:{pr_id}'.format(
1369 action=action, pr_id=pull_request.pull_request_id),
1380 action=action, pr_id=pull_request.pull_request_id),
1370 pull_request.target_repo)
1381 pull_request.target_repo)
1371
1382
1383 def get_reviewer_functions(self):
1384 """
1385 Fetches functions for validation and fetching default reviewers.
1386 If available we use the EE package, else we fallback to CE
1387 package functions
1388 """
1389 try:
1390 from rc_reviewers.utils import get_default_reviewers_data
1391 from rc_reviewers.utils import validate_default_reviewers
1392 except ImportError:
1393 from rhodecode.apps.repository.utils import \
1394 get_default_reviewers_data
1395 from rhodecode.apps.repository.utils import \
1396 validate_default_reviewers
1397
1398 return get_default_reviewers_data, validate_default_reviewers
1399
1372
1400
1373 class MergeCheck(object):
1401 class MergeCheck(object):
1374 """
1402 """
1375 Perform Merge Checks and returns a check object which stores information
1403 Perform Merge Checks and returns a check object which stores information
1376 about merge errors, and merge conditions
1404 about merge errors, and merge conditions
1377 """
1405 """
1378 TODO_CHECK = 'todo'
1406 TODO_CHECK = 'todo'
1379 PERM_CHECK = 'perm'
1407 PERM_CHECK = 'perm'
1380 REVIEW_CHECK = 'review'
1408 REVIEW_CHECK = 'review'
1381 MERGE_CHECK = 'merge'
1409 MERGE_CHECK = 'merge'
1382
1410
1383 def __init__(self):
1411 def __init__(self):
1384 self.review_status = None
1412 self.review_status = None
1385 self.merge_possible = None
1413 self.merge_possible = None
1386 self.merge_msg = ''
1414 self.merge_msg = ''
1387 self.failed = None
1415 self.failed = None
1388 self.errors = []
1416 self.errors = []
1389 self.error_details = OrderedDict()
1417 self.error_details = OrderedDict()
1390
1418
1391 def push_error(self, error_type, message, error_key, details):
1419 def push_error(self, error_type, message, error_key, details):
1392 self.failed = True
1420 self.failed = True
1393 self.errors.append([error_type, message])
1421 self.errors.append([error_type, message])
1394 self.error_details[error_key] = dict(
1422 self.error_details[error_key] = dict(
1395 details=details,
1423 details=details,
1396 error_type=error_type,
1424 error_type=error_type,
1397 message=message
1425 message=message
1398 )
1426 )
1399
1427
1400 @classmethod
1428 @classmethod
1401 def validate(cls, pull_request, user, fail_early=False, translator=None):
1429 def validate(cls, pull_request, user, fail_early=False, translator=None):
1402 # if migrated to pyramid...
1430 # if migrated to pyramid...
1403 # _ = lambda: translator or _ # use passed in translator if any
1431 # _ = lambda: translator or _ # use passed in translator if any
1404
1432
1405 merge_check = cls()
1433 merge_check = cls()
1406
1434
1407 # permissions to merge
1435 # permissions to merge
1408 user_allowed_to_merge = PullRequestModel().check_user_merge(
1436 user_allowed_to_merge = PullRequestModel().check_user_merge(
1409 pull_request, user)
1437 pull_request, user)
1410 if not user_allowed_to_merge:
1438 if not user_allowed_to_merge:
1411 log.debug("MergeCheck: cannot merge, approval is pending.")
1439 log.debug("MergeCheck: cannot merge, approval is pending.")
1412
1440
1413 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1441 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1414 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1442 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1415 if fail_early:
1443 if fail_early:
1416 return merge_check
1444 return merge_check
1417
1445
1418 # review status, must be always present
1446 # review status, must be always present
1419 review_status = pull_request.calculated_review_status()
1447 review_status = pull_request.calculated_review_status()
1420 merge_check.review_status = review_status
1448 merge_check.review_status = review_status
1421
1449
1422 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1450 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1423 if not status_approved:
1451 if not status_approved:
1424 log.debug("MergeCheck: cannot merge, approval is pending.")
1452 log.debug("MergeCheck: cannot merge, approval is pending.")
1425
1453
1426 msg = _('Pull request reviewer approval is pending.')
1454 msg = _('Pull request reviewer approval is pending.')
1427
1455
1428 merge_check.push_error(
1456 merge_check.push_error(
1429 'warning', msg, cls.REVIEW_CHECK, review_status)
1457 'warning', msg, cls.REVIEW_CHECK, review_status)
1430
1458
1431 if fail_early:
1459 if fail_early:
1432 return merge_check
1460 return merge_check
1433
1461
1434 # left over TODOs
1462 # left over TODOs
1435 todos = CommentsModel().get_unresolved_todos(pull_request)
1463 todos = CommentsModel().get_unresolved_todos(pull_request)
1436 if todos:
1464 if todos:
1437 log.debug("MergeCheck: cannot merge, {} "
1465 log.debug("MergeCheck: cannot merge, {} "
1438 "unresolved todos left.".format(len(todos)))
1466 "unresolved todos left.".format(len(todos)))
1439
1467
1440 if len(todos) == 1:
1468 if len(todos) == 1:
1441 msg = _('Cannot merge, {} TODO still not resolved.').format(
1469 msg = _('Cannot merge, {} TODO still not resolved.').format(
1442 len(todos))
1470 len(todos))
1443 else:
1471 else:
1444 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1472 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1445 len(todos))
1473 len(todos))
1446
1474
1447 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1475 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1448
1476
1449 if fail_early:
1477 if fail_early:
1450 return merge_check
1478 return merge_check
1451
1479
1452 # merge possible
1480 # merge possible
1453 merge_status, msg = PullRequestModel().merge_status(pull_request)
1481 merge_status, msg = PullRequestModel().merge_status(pull_request)
1454 merge_check.merge_possible = merge_status
1482 merge_check.merge_possible = merge_status
1455 merge_check.merge_msg = msg
1483 merge_check.merge_msg = msg
1456 if not merge_status:
1484 if not merge_status:
1457 log.debug(
1485 log.debug(
1458 "MergeCheck: cannot merge, pull request merge not possible.")
1486 "MergeCheck: cannot merge, pull request merge not possible.")
1459 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1487 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1460
1488
1461 if fail_early:
1489 if fail_early:
1462 return merge_check
1490 return merge_check
1463
1491
1464 return merge_check
1492 return merge_check
1465
1493
1466
1494
1467 ChangeTuple = namedtuple('ChangeTuple',
1495 ChangeTuple = namedtuple('ChangeTuple',
1468 ['added', 'common', 'removed', 'total'])
1496 ['added', 'common', 'removed', 'total'])
1469
1497
1470 FileChangeTuple = namedtuple('FileChangeTuple',
1498 FileChangeTuple = namedtuple('FileChangeTuple',
1471 ['added', 'modified', 'removed'])
1499 ['added', 'modified', 'removed'])
@@ -1,196 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22
22
23 import colander
23 import colander
24 from rhodecode.model.validation_schema import preparers
24 from rhodecode.model.validation_schema import preparers
25 from rhodecode.model.db import User, UserGroup
25 from rhodecode.model.db import User, UserGroup
26
26
27
27
28 class _RootLocation(object):
28 class _RootLocation(object):
29 pass
29 pass
30
30
31 RootLocation = _RootLocation()
31 RootLocation = _RootLocation()
32
32
33
33
34 def _normalize(seperator, path):
34 def _normalize(seperator, path):
35
35
36 if not path:
36 if not path:
37 return ''
37 return ''
38 elif path is colander.null:
38 elif path is colander.null:
39 return colander.null
39 return colander.null
40
40
41 parts = path.split(seperator)
41 parts = path.split(seperator)
42
42
43 def bad_parts(value):
43 def bad_parts(value):
44 if not value:
44 if not value:
45 return False
45 return False
46 if re.match(r'^[.]+$', value):
46 if re.match(r'^[.]+$', value):
47 return False
47 return False
48
48
49 return True
49 return True
50
50
51 def slugify(value):
51 def slugify(value):
52 value = preparers.slugify_preparer(value)
52 value = preparers.slugify_preparer(value)
53 value = re.sub(r'[.]{2,}', '.', value)
53 value = re.sub(r'[.]{2,}', '.', value)
54 return value
54 return value
55
55
56 clean_parts = [slugify(item) for item in parts if item]
56 clean_parts = [slugify(item) for item in parts if item]
57 path = filter(bad_parts, clean_parts)
57 path = filter(bad_parts, clean_parts)
58 return seperator.join(path)
58 return seperator.join(path)
59
59
60
60
61 class RepoNameType(colander.String):
61 class RepoNameType(colander.String):
62 SEPARATOR = '/'
62 SEPARATOR = '/'
63
63
64 def deserialize(self, node, cstruct):
64 def deserialize(self, node, cstruct):
65 result = super(RepoNameType, self).deserialize(node, cstruct)
65 result = super(RepoNameType, self).deserialize(node, cstruct)
66 if cstruct is colander.null:
66 if cstruct is colander.null:
67 return colander.null
67 return colander.null
68 return self._normalize(result)
68 return self._normalize(result)
69
69
70 def _normalize(self, path):
70 def _normalize(self, path):
71 return _normalize(self.SEPARATOR, path)
71 return _normalize(self.SEPARATOR, path)
72
72
73
73
74 class GroupNameType(colander.String):
74 class GroupNameType(colander.String):
75 SEPARATOR = '/'
75 SEPARATOR = '/'
76
76
77 def deserialize(self, node, cstruct):
77 def deserialize(self, node, cstruct):
78 if cstruct is RootLocation:
78 if cstruct is RootLocation:
79 return cstruct
79 return cstruct
80
80
81 result = super(GroupNameType, self).deserialize(node, cstruct)
81 result = super(GroupNameType, self).deserialize(node, cstruct)
82 if cstruct is colander.null:
82 if cstruct is colander.null:
83 return colander.null
83 return colander.null
84 return self._normalize(result)
84 return self._normalize(result)
85
85
86 def _normalize(self, path):
86 def _normalize(self, path):
87 return _normalize(self.SEPARATOR, path)
87 return _normalize(self.SEPARATOR, path)
88
88
89
89
90 class StringBooleanType(colander.String):
90 class StringBooleanType(colander.String):
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
93
93
94 def serialize(self, node, appstruct):
94 def serialize(self, node, appstruct):
95 if appstruct is colander.null:
95 if appstruct is colander.null:
96 return colander.null
96 return colander.null
97 if not isinstance(appstruct, bool):
97 if not isinstance(appstruct, bool):
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
99
99
100 return appstruct and 'true' or 'false'
100 return appstruct and 'true' or 'false'
101
101
102 def deserialize(self, node, cstruct):
102 def deserialize(self, node, cstruct):
103 if cstruct is colander.null:
103 if cstruct is colander.null:
104 return colander.null
104 return colander.null
105
105
106 if isinstance(cstruct, bool):
106 if isinstance(cstruct, bool):
107 return cstruct
107 return cstruct
108
108
109 if not isinstance(cstruct, basestring):
109 if not isinstance(cstruct, basestring):
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
111
111
112 value = cstruct.lower()
112 value = cstruct.lower()
113 if value in self.true_values:
113 if value in self.true_values:
114 return True
114 return True
115 elif value in self.false_values:
115 elif value in self.false_values:
116 return False
116 return False
117 else:
117 else:
118 raise colander.Invalid(
118 raise colander.Invalid(
119 node, '{} value cannot be translated to bool'.format(value))
119 node, '{} value cannot be translated to bool'.format(value))
120
120
121
121
122 class UserOrUserGroupType(colander.SchemaType):
122 class UserOrUserGroupType(colander.SchemaType):
123 """ colander Schema type for valid rhodecode user and/or usergroup """
123 """ colander Schema type for valid rhodecode user and/or usergroup """
124 scopes = ('user', 'usergroup')
124 scopes = ('user', 'usergroup')
125
125
126 def __init__(self):
126 def __init__(self):
127 self.users = 'user' in self.scopes
127 self.users = 'user' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
129
129
130 def serialize(self, node, appstruct):
130 def serialize(self, node, appstruct):
131 if appstruct is colander.null:
131 if appstruct is colander.null:
132 return colander.null
132 return colander.null
133
133
134 if self.users:
134 if self.users:
135 if isinstance(appstruct, User):
135 if isinstance(appstruct, User):
136 if self.usergroups:
136 if self.usergroups:
137 return 'user:%s' % appstruct.username
137 return 'user:%s' % appstruct.username
138 return appstruct.username
138 return appstruct.username
139
139
140 if self.usergroups:
140 if self.usergroups:
141 if isinstance(appstruct, UserGroup):
141 if isinstance(appstruct, UserGroup):
142 if self.users:
142 if self.users:
143 return 'usergroup:%s' % appstruct.users_group_name
143 return 'usergroup:%s' % appstruct.users_group_name
144 return appstruct.users_group_name
144 return appstruct.users_group_name
145
145
146 raise colander.Invalid(
146 raise colander.Invalid(
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
148
148
149 def deserialize(self, node, cstruct):
149 def deserialize(self, node, cstruct):
150 if cstruct is colander.null:
150 if cstruct is colander.null:
151 return colander.null
151 return colander.null
152
152
153 user, usergroup = None, None
153 user, usergroup = None, None
154 if self.users:
154 if self.users:
155 if cstruct.startswith('user:'):
155 if cstruct.startswith('user:'):
156 user = User.get_by_username(cstruct.split(':')[1])
156 user = User.get_by_username(cstruct.split(':')[1])
157 else:
157 else:
158 user = User.get_by_username(cstruct)
158 user = User.get_by_username(cstruct)
159
159
160 if self.usergroups:
160 if self.usergroups:
161 if cstruct.startswith('usergroup:'):
161 if cstruct.startswith('usergroup:'):
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
163 else:
163 else:
164 usergroup = UserGroup.get_by_group_name(cstruct)
164 usergroup = UserGroup.get_by_group_name(cstruct)
165
165
166 if self.users and self.usergroups:
166 if self.users and self.usergroups:
167 if user and usergroup:
167 if user and usergroup:
168 raise colander.Invalid(node, (
168 raise colander.Invalid(node, (
169 '%s is both a user and usergroup, specify which '
169 '%s is both a user and usergroup, specify which '
170 'one was wanted by prepending user: or usergroup: to the '
170 'one was wanted by prepending user: or usergroup: to the '
171 'name') % cstruct)
171 'name') % cstruct)
172
172
173 if self.users and user:
173 if self.users and user:
174 return user
174 return user
175
175
176 if self.usergroups and usergroup:
176 if self.usergroups and usergroup:
177 return usergroup
177 return usergroup
178
178
179 raise colander.Invalid(
179 raise colander.Invalid(
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
181
181
182
182
183 class UserType(UserOrUserGroupType):
183 class UserType(UserOrUserGroupType):
184 scopes = ('user',)
184 scopes = ('user',)
185
185
186
186
187 class UserGroupType(UserOrUserGroupType):
187 class UserGroupType(UserOrUserGroupType):
188 scopes = ('usergroup',)
188 scopes = ('usergroup',)
189
189
190
190
191 class StrOrIntType(colander.String):
191 class StrOrIntType(colander.String):
192 def deserialize(self, node, cstruct):
192 def deserialize(self, node, cstruct):
193 if isinstance(node, basestring):
193 if isinstance(cstruct, basestring):
194 return super(StrOrIntType, self).deserialize(node, cstruct)
194 return super(StrOrIntType, self).deserialize(node, cstruct)
195 else:
195 else:
196 return colander.Integer().deserialize(node, cstruct)
196 return colander.Integer().deserialize(node, cstruct)
@@ -1,408 +1,418 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey4;
17 color: @grey4;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey4 );
21 .border ( @border-thickness-buttons, @grey4 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29
29
30 a {
30 a {
31 display: block;
31 display: block;
32 margin: 0;
32 margin: 0;
33 padding: 0;
33 padding: 0;
34 color: inherit;
34 color: inherit;
35 text-decoration: none;
35 text-decoration: none;
36
36
37 &:hover {
37 &:hover {
38 text-decoration: none;
38 text-decoration: none;
39 }
39 }
40 }
40 }
41
41
42 &:focus,
42 &:focus,
43 &:active {
43 &:active {
44 outline:none;
44 outline:none;
45 }
45 }
46 &:hover {
46 &:hover {
47 color: white;
47 color: white;
48 background-color: @grey4;
48 background-color: @grey4;
49 }
49 }
50
50
51 .icon-remove-sign {
51 .icon-remove-sign {
52 display: none;
52 display: none;
53 }
53 }
54
54
55 //disabled buttons
55 //disabled buttons
56 //last; overrides any other styles
56 //last; overrides any other styles
57 &:disabled {
57 &:disabled {
58 opacity: .7;
58 opacity: .7;
59 cursor: auto;
59 cursor: auto;
60 background-color: white;
60 background-color: white;
61 color: @grey4;
61 color: @grey4;
62 text-shadow: none;
62 text-shadow: none;
63 }
63 }
64
64
65 &.no-margin {
66 margin: 0 0 0 0;
67 }
68
65 }
69 }
66
70
67
71
68 .btn-default {
72 .btn-default {
69 .border ( @border-thickness-buttons, @rcblue );
73 .border ( @border-thickness-buttons, @rcblue );
70 background-image: none;
74 background-image: none;
71 color: @rcblue;
75 color: @rcblue;
72
76
73 a {
77 a {
74 color: @rcblue;
78 color: @rcblue;
75 }
79 }
76
80
77 &:hover,
81 &:hover,
78 &.active {
82 &.active {
79 color: white;
83 color: white;
80 background-color: @rcdarkblue;
84 background-color: @rcdarkblue;
81 .border ( @border-thickness, @rcdarkblue );
85 .border ( @border-thickness, @rcdarkblue );
82
86
83 a {
87 a {
84 color: white;
88 color: white;
85 }
89 }
86 }
90 }
87 &:disabled {
91 &:disabled {
88 .border ( @border-thickness-buttons, @grey4 );
92 .border ( @border-thickness-buttons, @grey4 );
89 background-color: transparent;
93 background-color: transparent;
90 }
94 }
91 }
95 }
92
96
93 .btn-primary,
97 .btn-primary,
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
98 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
95 .btn-success {
99 .btn-success {
96 .border ( @border-thickness, @rcblue );
100 .border ( @border-thickness, @rcblue );
97 background-color: @rcblue;
101 background-color: @rcblue;
98 color: white;
102 color: white;
99
103
100 a {
104 a {
101 color: white;
105 color: white;
102 }
106 }
103
107
104 &:hover,
108 &:hover,
105 &.active {
109 &.active {
106 .border ( @border-thickness, @rcdarkblue );
110 .border ( @border-thickness, @rcdarkblue );
107 color: white;
111 color: white;
108 background-color: @rcdarkblue;
112 background-color: @rcdarkblue;
109
113
110 a {
114 a {
111 color: white;
115 color: white;
112 }
116 }
113 }
117 }
114 &:disabled {
118 &:disabled {
115 background-color: @rcblue;
119 background-color: @rcblue;
116 }
120 }
117 }
121 }
118
122
119 .btn-secondary {
123 .btn-secondary {
120 &:extend(.btn-default);
124 &:extend(.btn-default);
121
125
122 background-color: white;
126 background-color: white;
123
127
124 &:focus {
128 &:focus {
125 outline: 0;
129 outline: 0;
126 }
130 }
127
131
128 &:hover {
132 &:hover {
129 &:extend(.btn-default:hover);
133 &:extend(.btn-default:hover);
130 }
134 }
131
135
132 &.btn-link {
136 &.btn-link {
133 &:extend(.btn-link);
137 &:extend(.btn-link);
134 color: @rcblue;
138 color: @rcblue;
135 }
139 }
136
140
137 &:disabled {
141 &:disabled {
138 color: @rcblue;
142 color: @rcblue;
139 background-color: white;
143 background-color: white;
140 }
144 }
141 }
145 }
142
146
143 .btn-warning,
147 .btn-warning,
144 .btn-danger,
148 .btn-danger,
145 .revoke_perm,
149 .revoke_perm,
146 .btn-x,
150 .btn-x,
147 .form .action_button.btn-x {
151 .form .action_button.btn-x {
148 .border ( @border-thickness, @alert2 );
152 .border ( @border-thickness, @alert2 );
149 background-color: white;
153 background-color: white;
150 color: @alert2;
154 color: @alert2;
151
155
152 a {
156 a {
153 color: @alert2;
157 color: @alert2;
154 }
158 }
155
159
156 &:hover,
160 &:hover,
157 &.active {
161 &.active {
158 .border ( @border-thickness, @alert2 );
162 .border ( @border-thickness, @alert2 );
159 color: white;
163 color: white;
160 background-color: @alert2;
164 background-color: @alert2;
161
165
162 a {
166 a {
163 color: white;
167 color: white;
164 }
168 }
165 }
169 }
166
170
167 i {
171 i {
168 display:none;
172 display:none;
169 }
173 }
170
174
171 &:disabled {
175 &:disabled {
172 background-color: white;
176 background-color: white;
173 color: @alert2;
177 color: @alert2;
174 }
178 }
175 }
179 }
176
180
177 .btn-approved-status {
181 .btn-approved-status {
178 .border ( @border-thickness, @alert1 );
182 .border ( @border-thickness, @alert1 );
179 background-color: white;
183 background-color: white;
180 color: @alert1;
184 color: @alert1;
181
185
182 }
186 }
183
187
184 .btn-rejected-status {
188 .btn-rejected-status {
185 .border ( @border-thickness, @alert2 );
189 .border ( @border-thickness, @alert2 );
186 background-color: white;
190 background-color: white;
187 color: @alert2;
191 color: @alert2;
188 }
192 }
189
193
190 .btn-sm,
194 .btn-sm,
191 .btn-mini,
195 .btn-mini,
192 .field-sm .btn {
196 .field-sm .btn {
193 padding: @padding/3;
197 padding: @padding/3;
194 }
198 }
195
199
196 .btn-xs {
200 .btn-xs {
197 padding: @padding/4;
201 padding: @padding/4;
198 }
202 }
199
203
200 .btn-lg {
204 .btn-lg {
201 padding: @padding * 1.2;
205 padding: @padding * 1.2;
202 }
206 }
203
207
204 .btn-group {
208 .btn-group {
205 display: inline-block;
209 display: inline-block;
206 .btn {
210 .btn {
207 float: left;
211 float: left;
208 margin: 0 0 0 -1px;
212 margin: 0 0 0 -1px;
209 }
213 }
210 }
214 }
211
215
212 .btn-link {
216 .btn-link {
213 background: transparent;
217 background: transparent;
214 border: none;
218 border: none;
215 padding: 0;
219 padding: 0;
216 color: @rcblue;
220 color: @rcblue;
217
221
218 &:hover {
222 &:hover {
219 background: transparent;
223 background: transparent;
220 border: none;
224 border: none;
221 color: @rcdarkblue;
225 color: @rcdarkblue;
222 }
226 }
223
227
224 //disabled buttons
228 //disabled buttons
225 //last; overrides any other styles
229 //last; overrides any other styles
226 &:disabled {
230 &:disabled {
227 opacity: .7;
231 opacity: .7;
228 cursor: auto;
232 cursor: auto;
229 background-color: white;
233 background-color: white;
230 color: @grey4;
234 color: @grey4;
231 text-shadow: none;
235 text-shadow: none;
232 }
236 }
233
237
234 // TODO: johbo: Check if we can avoid this, indicates that the structure
238 // TODO: johbo: Check if we can avoid this, indicates that the structure
235 // is not yet good.
239 // is not yet good.
236 // lisa: The button CSS reflects the button HTML; both need a cleanup.
240 // lisa: The button CSS reflects the button HTML; both need a cleanup.
237 &.btn-danger {
241 &.btn-danger {
238 color: @alert2;
242 color: @alert2;
239
243
240 &:hover {
244 &:hover {
241 color: darken(@alert2,30%);
245 color: darken(@alert2,30%);
242 }
246 }
243
247
244 &:disabled {
248 &:disabled {
245 color: @alert2;
249 color: @alert2;
246 }
250 }
247 }
251 }
248 }
252 }
249
253
250 .btn-social {
254 .btn-social {
251 &:extend(.btn-default);
255 &:extend(.btn-default);
252 margin: 5px 5px 5px 0px;
256 margin: 5px 5px 5px 0px;
253 min-width: 150px;
257 min-width: 150px;
254 }
258 }
255
259
256 // TODO: johbo: check these exceptions
260 // TODO: johbo: check these exceptions
257
261
258 .links {
262 .links {
259
263
260 .btn + .btn {
264 .btn + .btn {
261 margin-top: @padding;
265 margin-top: @padding;
262 }
266 }
263 }
267 }
264
268
265
269
266 .action_button {
270 .action_button {
267 display:inline;
271 display:inline;
268 margin: 0;
272 margin: 0;
269 padding: 0 1em 0 0;
273 padding: 0 1em 0 0;
270 font-size: inherit;
274 font-size: inherit;
271 color: @rcblue;
275 color: @rcblue;
272 border: none;
276 border: none;
273 .border-radius (0);
277 border-radius: 0;
274 background-color: transparent;
278 background-color: transparent;
275
279
280 &.last-item {
281 border: none;
282 padding: 0 0 0 0;
283 }
284
276 &:last-child {
285 &:last-child {
277 border: none;
286 border: none;
287 padding: 0 0 0 0;
278 }
288 }
279
289
280 &:hover {
290 &:hover {
281 color: @rcdarkblue;
291 color: @rcdarkblue;
282 background-color: transparent;
292 background-color: transparent;
283 border: none;
293 border: none;
284 }
294 }
285 }
295 }
286 .grid_delete {
296 .grid_delete {
287 .action_button {
297 .action_button {
288 border: none;
298 border: none;
289 }
299 }
290 }
300 }
291
301
292
302
293 // TODO: johbo: Form button tweaks, check if we can use the classes instead
303 // TODO: johbo: Form button tweaks, check if we can use the classes instead
294 input[type="submit"] {
304 input[type="submit"] {
295 &:extend(.btn-primary);
305 &:extend(.btn-primary);
296
306
297 &:focus {
307 &:focus {
298 outline: 0;
308 outline: 0;
299 }
309 }
300
310
301 &:hover {
311 &:hover {
302 &:extend(.btn-primary:hover);
312 &:extend(.btn-primary:hover);
303 }
313 }
304
314
305 &.btn-link {
315 &.btn-link {
306 &:extend(.btn-link);
316 &:extend(.btn-link);
307 color: @rcblue;
317 color: @rcblue;
308
318
309 &:disabled {
319 &:disabled {
310 color: @rcblue;
320 color: @rcblue;
311 background-color: transparent;
321 background-color: transparent;
312 }
322 }
313 }
323 }
314
324
315 &:disabled {
325 &:disabled {
316 .border ( @border-thickness-buttons, @rcblue );
326 .border ( @border-thickness-buttons, @rcblue );
317 background-color: @rcblue;
327 background-color: @rcblue;
318 color: white;
328 color: white;
319 }
329 }
320 }
330 }
321
331
322 input[type="reset"] {
332 input[type="reset"] {
323 &:extend(.btn-default);
333 &:extend(.btn-default);
324
334
325 // TODO: johbo: Check if this tweak can be avoided.
335 // TODO: johbo: Check if this tweak can be avoided.
326 background: transparent;
336 background: transparent;
327
337
328 &:focus {
338 &:focus {
329 outline: 0;
339 outline: 0;
330 }
340 }
331
341
332 &:hover {
342 &:hover {
333 &:extend(.btn-default:hover);
343 &:extend(.btn-default:hover);
334 }
344 }
335
345
336 &.btn-link {
346 &.btn-link {
337 &:extend(.btn-link);
347 &:extend(.btn-link);
338 color: @rcblue;
348 color: @rcblue;
339
349
340 &:disabled {
350 &:disabled {
341 border: none;
351 border: none;
342 }
352 }
343 }
353 }
344
354
345 &:disabled {
355 &:disabled {
346 .border ( @border-thickness-buttons, @rcblue );
356 .border ( @border-thickness-buttons, @rcblue );
347 background-color: white;
357 background-color: white;
348 color: @rcblue;
358 color: @rcblue;
349 }
359 }
350 }
360 }
351
361
352 input[type="submit"],
362 input[type="submit"],
353 input[type="reset"] {
363 input[type="reset"] {
354 &.btn-danger {
364 &.btn-danger {
355 &:extend(.btn-danger);
365 &:extend(.btn-danger);
356
366
357 &:focus {
367 &:focus {
358 outline: 0;
368 outline: 0;
359 }
369 }
360
370
361 &:hover {
371 &:hover {
362 &:extend(.btn-danger:hover);
372 &:extend(.btn-danger:hover);
363 }
373 }
364
374
365 &.btn-link {
375 &.btn-link {
366 &:extend(.btn-link);
376 &:extend(.btn-link);
367 color: @alert2;
377 color: @alert2;
368
378
369 &:hover {
379 &:hover {
370 color: darken(@alert2,30%);
380 color: darken(@alert2,30%);
371 }
381 }
372 }
382 }
373
383
374 &:disabled {
384 &:disabled {
375 color: @alert2;
385 color: @alert2;
376 background-color: white;
386 background-color: white;
377 }
387 }
378 }
388 }
379 &.btn-danger-action {
389 &.btn-danger-action {
380 .border ( @border-thickness, @alert2 );
390 .border ( @border-thickness, @alert2 );
381 background-color: @alert2;
391 background-color: @alert2;
382 color: white;
392 color: white;
383
393
384 a {
394 a {
385 color: white;
395 color: white;
386 }
396 }
387
397
388 &:hover {
398 &:hover {
389 background-color: darken(@alert2,20%);
399 background-color: darken(@alert2,20%);
390 }
400 }
391
401
392 &.active {
402 &.active {
393 .border ( @border-thickness, @alert2 );
403 .border ( @border-thickness, @alert2 );
394 color: white;
404 color: white;
395 background-color: @alert2;
405 background-color: @alert2;
396
406
397 a {
407 a {
398 color: white;
408 color: white;
399 }
409 }
400 }
410 }
401
411
402 &:disabled {
412 &:disabled {
403 background-color: white;
413 background-color: white;
404 color: @alert2;
414 color: @alert2;
405 }
415 }
406 }
416 }
407 }
417 }
408
418
@@ -1,139 +1,139 b''
1 .deform {
1 .deform {
2
2
3 * {
3 * {
4 box-sizing: border-box;
4 box-sizing: border-box;
5 }
5 }
6
6
7 .required:after {
7 .required:after {
8 color: #e32;
8 color: #e32;
9 content: '*';
9 content: '*';
10 display:inline;
10 display:inline;
11 }
11 }
12
12
13 .control-label {
13 .control-label {
14 width: 200px;
14 width: 200px;
15 padding: 10px;
15 padding: 10px 0px;
16 float: left;
16 float: left;
17 }
17 }
18 .control-inputs {
18 .control-inputs {
19 width: 400px;
19 width: 400px;
20 float: left;
20 float: left;
21 }
21 }
22 .form-group .radio, .form-group .checkbox {
22 .form-group .radio, .form-group .checkbox {
23 position: relative;
23 position: relative;
24 display: block;
24 display: block;
25 /* margin-bottom: 10px; */
25 /* margin-bottom: 10px; */
26 }
26 }
27
27
28 .form-group {
28 .form-group {
29 clear: left;
29 clear: left;
30 margin-bottom: 20px;
30 margin-bottom: 20px;
31
31
32 &:after { /* clear fix */
32 &:after { /* clear fix */
33 content: " ";
33 content: " ";
34 display: block;
34 display: block;
35 clear: left;
35 clear: left;
36 }
36 }
37 }
37 }
38
38
39 .form-control {
39 .form-control {
40 width: 100%;
40 width: 100%;
41 padding: 0.9em;
41 padding: 0.9em;
42 border: 1px solid #979797;
42 border: 1px solid #979797;
43 border-radius: 2px;
43 border-radius: 2px;
44 }
44 }
45 .form-control.select2-container {
45 .form-control.select2-container {
46 padding: 0; /* padding already applied in .drop-menu a */
46 padding: 0; /* padding already applied in .drop-menu a */
47 }
47 }
48
48
49 .form-control.readonly {
49 .form-control.readonly {
50 background: #eeeeee;
50 background: #eeeeee;
51 cursor: not-allowed;
51 cursor: not-allowed;
52 }
52 }
53
53
54 .error-block {
54 .error-block {
55 color: red;
55 color: red;
56 margin: 0;
56 margin: 0;
57 }
57 }
58
58
59 .help-block {
59 .help-block {
60 margin: 0;
60 margin: 0;
61 }
61 }
62
62
63 .deform-seq-container .control-inputs {
63 .deform-seq-container .control-inputs {
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .deform-seq-container .deform-seq-item-handle {
67 .deform-seq-container .deform-seq-item-handle {
68 width: 8.3%;
68 width: 8.3%;
69 float: left;
69 float: left;
70 }
70 }
71
71
72 .deform-seq-container .deform-seq-item-group {
72 .deform-seq-container .deform-seq-item-group {
73 width: 91.6%;
73 width: 91.6%;
74 float: left;
74 float: left;
75 }
75 }
76
76
77 .form-control {
77 .form-control {
78 input {
78 input {
79 height: 40px;
79 height: 40px;
80 }
80 }
81 input[type=checkbox], input[type=radio] {
81 input[type=checkbox], input[type=radio] {
82 height: auto;
82 height: auto;
83 }
83 }
84 select {
84 select {
85 height: 40px;
85 height: 40px;
86 }
86 }
87 }
87 }
88
88
89 .form-control.select2-container {
89 .form-control.select2-container {
90 height: 40px;
90 height: 40px;
91 }
91 }
92
92
93 .deform-full-field-sequence.control-inputs {
93 .deform-full-field-sequence.control-inputs {
94 width: 100%;
94 width: 100%;
95 }
95 }
96
96
97 .deform-table-sequence {
97 .deform-table-sequence {
98 .deform-seq-container {
98 .deform-seq-container {
99 .deform-seq-item {
99 .deform-seq-item {
100 margin: 0;
100 margin: 0;
101 label {
101 label {
102 display: none;
102 display: none;
103 }
103 }
104 .panel-heading {
104 .panel-heading {
105 display: none;
105 display: none;
106 }
106 }
107 .deform-seq-item-group > .panel {
107 .deform-seq-item-group > .panel {
108 padding: 0;
108 padding: 0;
109 margin: 5px 0;
109 margin: 5px 0;
110 border: none;
110 border: none;
111 &> .panel-body {
111 &> .panel-body {
112 padding: 0;
112 padding: 0;
113 }
113 }
114 }
114 }
115 &:first-child label {
115 &:first-child label {
116 display: block;
116 display: block;
117 }
117 }
118 }
118 }
119 }
119 }
120 }
120 }
121 .deform-table-2-sequence {
121 .deform-table-2-sequence {
122 .deform-seq-container {
122 .deform-seq-container {
123 .deform-seq-item {
123 .deform-seq-item {
124 .form-group {
124 .form-group {
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
126 }
126 }
127 }
127 }
128 }
128 }
129 }
129 }
130 .deform-table-3-sequence {
130 .deform-table-3-sequence {
131 .deform-seq-container {
131 .deform-seq-container {
132 .deform-seq-item {
132 .deform-seq-item {
133 .form-group {
133 .form-group {
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
135 }
135 }
136 }
136 }
137 }
137 }
138 }
138 }
139 }
139 }
@@ -1,2347 +1,2385 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'readme-box';
14 @import 'readme-box';
15 @import 'progress-bar';
15 @import 'progress-bar';
16
16
17 @import 'type';
17 @import 'type';
18 @import 'alerts';
18 @import 'alerts';
19 @import 'buttons';
19 @import 'buttons';
20 @import 'tags';
20 @import 'tags';
21 @import 'code-block';
21 @import 'code-block';
22 @import 'examples';
22 @import 'examples';
23 @import 'login';
23 @import 'login';
24 @import 'main-content';
24 @import 'main-content';
25 @import 'select2';
25 @import 'select2';
26 @import 'comments';
26 @import 'comments';
27 @import 'panels-bootstrap';
27 @import 'panels-bootstrap';
28 @import 'panels';
28 @import 'panels';
29 @import 'deform';
29 @import 'deform';
30
30
31 //--- BASE ------------------//
31 //--- BASE ------------------//
32 .noscript-error {
32 .noscript-error {
33 top: 0;
33 top: 0;
34 left: 0;
34 left: 0;
35 width: 100%;
35 width: 100%;
36 z-index: 101;
36 z-index: 101;
37 text-align: center;
37 text-align: center;
38 font-family: @text-semibold;
38 font-family: @text-semibold;
39 font-size: 120%;
39 font-size: 120%;
40 color: white;
40 color: white;
41 background-color: @alert2;
41 background-color: @alert2;
42 padding: 5px 0 5px 0;
42 padding: 5px 0 5px 0;
43 }
43 }
44
44
45 html {
45 html {
46 display: table;
46 display: table;
47 height: 100%;
47 height: 100%;
48 width: 100%;
48 width: 100%;
49 }
49 }
50
50
51 body {
51 body {
52 display: table-cell;
52 display: table-cell;
53 width: 100%;
53 width: 100%;
54 }
54 }
55
55
56 //--- LAYOUT ------------------//
56 //--- LAYOUT ------------------//
57
57
58 .hidden{
58 .hidden{
59 display: none !important;
59 display: none !important;
60 }
60 }
61
61
62 .box{
62 .box{
63 float: left;
63 float: left;
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .browser-header {
67 .browser-header {
68 clear: both;
68 clear: both;
69 }
69 }
70 .main {
70 .main {
71 clear: both;
71 clear: both;
72 padding:0 0 @pagepadding;
72 padding:0 0 @pagepadding;
73 height: auto;
73 height: auto;
74
74
75 &:after { //clearfix
75 &:after { //clearfix
76 content:"";
76 content:"";
77 clear:both;
77 clear:both;
78 width:100%;
78 width:100%;
79 display:block;
79 display:block;
80 }
80 }
81 }
81 }
82
82
83 .action-link{
83 .action-link{
84 margin-left: @padding;
84 margin-left: @padding;
85 padding-left: @padding;
85 padding-left: @padding;
86 border-left: @border-thickness solid @border-default-color;
86 border-left: @border-thickness solid @border-default-color;
87 }
87 }
88
88
89 input + .action-link, .action-link.first{
89 input + .action-link, .action-link.first{
90 border-left: none;
90 border-left: none;
91 }
91 }
92
92
93 .action-link.last{
93 .action-link.last{
94 margin-right: @padding;
94 margin-right: @padding;
95 padding-right: @padding;
95 padding-right: @padding;
96 }
96 }
97
97
98 .action-link.active,
98 .action-link.active,
99 .action-link.active a{
99 .action-link.active a{
100 color: @grey4;
100 color: @grey4;
101 }
101 }
102
102
103 ul.simple-list{
103 ul.simple-list{
104 list-style: none;
104 list-style: none;
105 margin: 0;
105 margin: 0;
106 padding: 0;
106 padding: 0;
107 }
107 }
108
108
109 .main-content {
109 .main-content {
110 padding-bottom: @pagepadding;
110 padding-bottom: @pagepadding;
111 }
111 }
112
112
113 .wide-mode-wrapper {
113 .wide-mode-wrapper {
114 max-width:4000px !important;
114 max-width:4000px !important;
115 }
115 }
116
116
117 .wrapper {
117 .wrapper {
118 position: relative;
118 position: relative;
119 max-width: @wrapper-maxwidth;
119 max-width: @wrapper-maxwidth;
120 margin: 0 auto;
120 margin: 0 auto;
121 }
121 }
122
122
123 #content {
123 #content {
124 clear: both;
124 clear: both;
125 padding: 0 @contentpadding;
125 padding: 0 @contentpadding;
126 }
126 }
127
127
128 .advanced-settings-fields{
128 .advanced-settings-fields{
129 input{
129 input{
130 margin-left: @textmargin;
130 margin-left: @textmargin;
131 margin-right: @padding/2;
131 margin-right: @padding/2;
132 }
132 }
133 }
133 }
134
134
135 .cs_files_title {
135 .cs_files_title {
136 margin: @pagepadding 0 0;
136 margin: @pagepadding 0 0;
137 }
137 }
138
138
139 input.inline[type="file"] {
139 input.inline[type="file"] {
140 display: inline;
140 display: inline;
141 }
141 }
142
142
143 .error_page {
143 .error_page {
144 margin: 10% auto;
144 margin: 10% auto;
145
145
146 h1 {
146 h1 {
147 color: @grey2;
147 color: @grey2;
148 }
148 }
149
149
150 .alert {
150 .alert {
151 margin: @padding 0;
151 margin: @padding 0;
152 }
152 }
153
153
154 .error-branding {
154 .error-branding {
155 font-family: @text-semibold;
155 font-family: @text-semibold;
156 color: @grey4;
156 color: @grey4;
157 }
157 }
158
158
159 .error_message {
159 .error_message {
160 font-family: @text-regular;
160 font-family: @text-regular;
161 }
161 }
162
162
163 .sidebar {
163 .sidebar {
164 min-height: 275px;
164 min-height: 275px;
165 margin: 0;
165 margin: 0;
166 padding: 0 0 @sidebarpadding @sidebarpadding;
166 padding: 0 0 @sidebarpadding @sidebarpadding;
167 border: none;
167 border: none;
168 }
168 }
169
169
170 .main-content {
170 .main-content {
171 position: relative;
171 position: relative;
172 margin: 0 @sidebarpadding @sidebarpadding;
172 margin: 0 @sidebarpadding @sidebarpadding;
173 padding: 0 0 0 @sidebarpadding;
173 padding: 0 0 0 @sidebarpadding;
174 border-left: @border-thickness solid @grey5;
174 border-left: @border-thickness solid @grey5;
175
175
176 @media (max-width:767px) {
176 @media (max-width:767px) {
177 clear: both;
177 clear: both;
178 width: 100%;
178 width: 100%;
179 margin: 0;
179 margin: 0;
180 border: none;
180 border: none;
181 }
181 }
182 }
182 }
183
183
184 .inner-column {
184 .inner-column {
185 float: left;
185 float: left;
186 width: 29.75%;
186 width: 29.75%;
187 min-height: 150px;
187 min-height: 150px;
188 margin: @sidebarpadding 2% 0 0;
188 margin: @sidebarpadding 2% 0 0;
189 padding: 0 2% 0 0;
189 padding: 0 2% 0 0;
190 border-right: @border-thickness solid @grey5;
190 border-right: @border-thickness solid @grey5;
191
191
192 @media (max-width:767px) {
192 @media (max-width:767px) {
193 clear: both;
193 clear: both;
194 width: 100%;
194 width: 100%;
195 border: none;
195 border: none;
196 }
196 }
197
197
198 ul {
198 ul {
199 padding-left: 1.25em;
199 padding-left: 1.25em;
200 }
200 }
201
201
202 &:last-child {
202 &:last-child {
203 margin: @sidebarpadding 0 0;
203 margin: @sidebarpadding 0 0;
204 border: none;
204 border: none;
205 }
205 }
206
206
207 h4 {
207 h4 {
208 margin: 0 0 @padding;
208 margin: 0 0 @padding;
209 font-family: @text-semibold;
209 font-family: @text-semibold;
210 }
210 }
211 }
211 }
212 }
212 }
213 .error-page-logo {
213 .error-page-logo {
214 width: 130px;
214 width: 130px;
215 height: 160px;
215 height: 160px;
216 }
216 }
217
217
218 // HEADER
218 // HEADER
219 .header {
219 .header {
220
220
221 // TODO: johbo: Fix login pages, so that they work without a min-height
221 // TODO: johbo: Fix login pages, so that they work without a min-height
222 // for the header and then remove the min-height. I chose a smaller value
222 // for the header and then remove the min-height. I chose a smaller value
223 // intentionally here to avoid rendering issues in the main navigation.
223 // intentionally here to avoid rendering issues in the main navigation.
224 min-height: 49px;
224 min-height: 49px;
225
225
226 position: relative;
226 position: relative;
227 vertical-align: bottom;
227 vertical-align: bottom;
228 padding: 0 @header-padding;
228 padding: 0 @header-padding;
229 background-color: @grey2;
229 background-color: @grey2;
230 color: @grey5;
230 color: @grey5;
231
231
232 .title {
232 .title {
233 overflow: visible;
233 overflow: visible;
234 }
234 }
235
235
236 &:before,
236 &:before,
237 &:after {
237 &:after {
238 content: "";
238 content: "";
239 clear: both;
239 clear: both;
240 width: 100%;
240 width: 100%;
241 }
241 }
242
242
243 // TODO: johbo: Avoids breaking "Repositories" chooser
243 // TODO: johbo: Avoids breaking "Repositories" chooser
244 .select2-container .select2-choice .select2-arrow {
244 .select2-container .select2-choice .select2-arrow {
245 display: none;
245 display: none;
246 }
246 }
247 }
247 }
248
248
249 #header-inner {
249 #header-inner {
250 &.title {
250 &.title {
251 margin: 0;
251 margin: 0;
252 }
252 }
253 &:before,
253 &:before,
254 &:after {
254 &:after {
255 content: "";
255 content: "";
256 clear: both;
256 clear: both;
257 }
257 }
258 }
258 }
259
259
260 // Gists
260 // Gists
261 #files_data {
261 #files_data {
262 clear: both; //for firefox
262 clear: both; //for firefox
263 }
263 }
264 #gistid {
264 #gistid {
265 margin-right: @padding;
265 margin-right: @padding;
266 }
266 }
267
267
268 // Global Settings Editor
268 // Global Settings Editor
269 .textarea.editor {
269 .textarea.editor {
270 float: left;
270 float: left;
271 position: relative;
271 position: relative;
272 max-width: @texteditor-width;
272 max-width: @texteditor-width;
273
273
274 select {
274 select {
275 position: absolute;
275 position: absolute;
276 top:10px;
276 top:10px;
277 right:0;
277 right:0;
278 }
278 }
279
279
280 .CodeMirror {
280 .CodeMirror {
281 margin: 0;
281 margin: 0;
282 }
282 }
283
283
284 .help-block {
284 .help-block {
285 margin: 0 0 @padding;
285 margin: 0 0 @padding;
286 padding:.5em;
286 padding:.5em;
287 background-color: @grey6;
287 background-color: @grey6;
288 }
288 }
289 }
289 }
290
290
291 ul.auth_plugins {
291 ul.auth_plugins {
292 margin: @padding 0 @padding @legend-width;
292 margin: @padding 0 @padding @legend-width;
293 padding: 0;
293 padding: 0;
294
294
295 li {
295 li {
296 margin-bottom: @padding;
296 margin-bottom: @padding;
297 line-height: 1em;
297 line-height: 1em;
298 list-style-type: none;
298 list-style-type: none;
299
299
300 .auth_buttons .btn {
300 .auth_buttons .btn {
301 margin-right: @padding;
301 margin-right: @padding;
302 }
302 }
303
303
304 &:before { content: none; }
304 &:before { content: none; }
305 }
305 }
306 }
306 }
307
307
308
308
309 // My Account PR list
309 // My Account PR list
310
310
311 #show_closed {
311 #show_closed {
312 margin: 0 1em 0 0;
312 margin: 0 1em 0 0;
313 }
313 }
314
314
315 .pullrequestlist {
315 .pullrequestlist {
316 .closed {
316 .closed {
317 background-color: @grey6;
317 background-color: @grey6;
318 }
318 }
319 .td-status {
319 .td-status {
320 padding-left: .5em;
320 padding-left: .5em;
321 }
321 }
322 .log-container .truncate {
322 .log-container .truncate {
323 height: 2.75em;
323 height: 2.75em;
324 white-space: pre-line;
324 white-space: pre-line;
325 }
325 }
326 table.rctable .user {
326 table.rctable .user {
327 padding-left: 0;
327 padding-left: 0;
328 }
328 }
329 table.rctable {
329 table.rctable {
330 td.td-description,
330 td.td-description,
331 .rc-user {
331 .rc-user {
332 min-width: auto;
332 min-width: auto;
333 }
333 }
334 }
334 }
335 }
335 }
336
336
337 // Pull Requests
337 // Pull Requests
338
338
339 .pullrequests_section_head {
339 .pullrequests_section_head {
340 display: block;
340 display: block;
341 clear: both;
341 clear: both;
342 margin: @padding 0;
342 margin: @padding 0;
343 font-family: @text-bold;
343 font-family: @text-bold;
344 }
344 }
345
345
346 .pr-origininfo, .pr-targetinfo {
346 .pr-origininfo, .pr-targetinfo {
347 position: relative;
347 position: relative;
348
348
349 .tag {
349 .tag {
350 display: inline-block;
350 display: inline-block;
351 margin: 0 1em .5em 0;
351 margin: 0 1em .5em 0;
352 }
352 }
353
353
354 .clone-url {
354 .clone-url {
355 display: inline-block;
355 display: inline-block;
356 margin: 0 0 .5em 0;
356 margin: 0 0 .5em 0;
357 padding: 0;
357 padding: 0;
358 line-height: 1.2em;
358 line-height: 1.2em;
359 }
359 }
360 }
360 }
361
361
362 .pr-mergeinfo {
363 clear: both;
364 margin: .5em 0;
365
366 input {
367 min-width: 100% !important;
368 padding: 0 !important;
369 border: 0;
370 }
371 }
372
362 .pr-pullinfo {
373 .pr-pullinfo {
363 clear: both;
374 clear: both;
364 margin: .5em 0;
375 margin: .5em 0;
376
377 input {
378 min-width: 100% !important;
379 padding: 0 !important;
380 border: 0;
381 }
365 }
382 }
366
383
367 #pr-title-input {
384 #pr-title-input {
368 width: 72%;
385 width: 72%;
369 font-size: 1em;
386 font-size: 1em;
370 font-family: @text-bold;
387 font-family: @text-bold;
371 margin: 0;
388 margin: 0;
372 padding: 0 0 0 @padding/4;
389 padding: 0 0 0 @padding/4;
373 line-height: 1.7em;
390 line-height: 1.7em;
374 color: @text-color;
391 color: @text-color;
375 letter-spacing: .02em;
392 letter-spacing: .02em;
376 }
393 }
377
394
378 #pullrequest_title {
395 #pullrequest_title {
379 width: 100%;
396 width: 100%;
380 box-sizing: border-box;
397 box-sizing: border-box;
381 }
398 }
382
399
383 #pr_open_message {
400 #pr_open_message {
384 border: @border-thickness solid #fff;
401 border: @border-thickness solid #fff;
385 border-radius: @border-radius;
402 border-radius: @border-radius;
386 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
403 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
387 text-align: left;
404 text-align: left;
388 overflow: hidden;
405 overflow: hidden;
389 }
406 }
390
407
391 .pr-submit-button {
408 .pr-submit-button {
392 float: right;
409 float: right;
393 margin: 0 0 0 5px;
410 margin: 0 0 0 5px;
394 }
411 }
395
412
396 .pr-spacing-container {
413 .pr-spacing-container {
397 padding: 20px;
414 padding: 20px;
398 clear: both
415 clear: both
399 }
416 }
400
417
401 #pr-description-input {
418 #pr-description-input {
402 margin-bottom: 0;
419 margin-bottom: 0;
403 }
420 }
404
421
405 .pr-description-label {
422 .pr-description-label {
406 vertical-align: top;
423 vertical-align: top;
407 }
424 }
408
425
409 .perms_section_head {
426 .perms_section_head {
410 min-width: 625px;
427 min-width: 625px;
411
428
412 h2 {
429 h2 {
413 margin-bottom: 0;
430 margin-bottom: 0;
414 }
431 }
415
432
416 .label-checkbox {
433 .label-checkbox {
417 float: left;
434 float: left;
418 }
435 }
419
436
420 &.field {
437 &.field {
421 margin: @space 0 @padding;
438 margin: @space 0 @padding;
422 }
439 }
423
440
424 &:first-child.field {
441 &:first-child.field {
425 margin-top: 0;
442 margin-top: 0;
426
443
427 .label {
444 .label {
428 margin-top: 0;
445 margin-top: 0;
429 padding-top: 0;
446 padding-top: 0;
430 }
447 }
431
448
432 .radios {
449 .radios {
433 padding-top: 0;
450 padding-top: 0;
434 }
451 }
435 }
452 }
436
453
437 .radios {
454 .radios {
438 float: right;
455 float: right;
439 position: relative;
456 position: relative;
440 width: 405px;
457 width: 405px;
441 }
458 }
442 }
459 }
443
460
444 //--- MODULES ------------------//
461 //--- MODULES ------------------//
445
462
446
463
447 // Server Announcement
464 // Server Announcement
448 #server-announcement {
465 #server-announcement {
449 width: 95%;
466 width: 95%;
450 margin: @padding auto;
467 margin: @padding auto;
451 padding: @padding;
468 padding: @padding;
452 border-width: 2px;
469 border-width: 2px;
453 border-style: solid;
470 border-style: solid;
454 .border-radius(2px);
471 .border-radius(2px);
455 font-family: @text-bold;
472 font-family: @text-bold;
456
473
457 &.info { border-color: @alert4; background-color: @alert4-inner; }
474 &.info { border-color: @alert4; background-color: @alert4-inner; }
458 &.warning { border-color: @alert3; background-color: @alert3-inner; }
475 &.warning { border-color: @alert3; background-color: @alert3-inner; }
459 &.error { border-color: @alert2; background-color: @alert2-inner; }
476 &.error { border-color: @alert2; background-color: @alert2-inner; }
460 &.success { border-color: @alert1; background-color: @alert1-inner; }
477 &.success { border-color: @alert1; background-color: @alert1-inner; }
461 &.neutral { border-color: @grey3; background-color: @grey6; }
478 &.neutral { border-color: @grey3; background-color: @grey6; }
462 }
479 }
463
480
464 // Fixed Sidebar Column
481 // Fixed Sidebar Column
465 .sidebar-col-wrapper {
482 .sidebar-col-wrapper {
466 padding-left: @sidebar-all-width;
483 padding-left: @sidebar-all-width;
467
484
468 .sidebar {
485 .sidebar {
469 width: @sidebar-width;
486 width: @sidebar-width;
470 margin-left: -@sidebar-all-width;
487 margin-left: -@sidebar-all-width;
471 }
488 }
472 }
489 }
473
490
474 .sidebar-col-wrapper.scw-small {
491 .sidebar-col-wrapper.scw-small {
475 padding-left: @sidebar-small-all-width;
492 padding-left: @sidebar-small-all-width;
476
493
477 .sidebar {
494 .sidebar {
478 width: @sidebar-small-width;
495 width: @sidebar-small-width;
479 margin-left: -@sidebar-small-all-width;
496 margin-left: -@sidebar-small-all-width;
480 }
497 }
481 }
498 }
482
499
483
500
484 // FOOTER
501 // FOOTER
485 #footer {
502 #footer {
486 padding: 0;
503 padding: 0;
487 text-align: center;
504 text-align: center;
488 vertical-align: middle;
505 vertical-align: middle;
489 color: @grey2;
506 color: @grey2;
490 background-color: @grey6;
507 background-color: @grey6;
491
508
492 p {
509 p {
493 margin: 0;
510 margin: 0;
494 padding: 1em;
511 padding: 1em;
495 line-height: 1em;
512 line-height: 1em;
496 }
513 }
497
514
498 .server-instance { //server instance
515 .server-instance { //server instance
499 display: none;
516 display: none;
500 }
517 }
501
518
502 .title {
519 .title {
503 float: none;
520 float: none;
504 margin: 0 auto;
521 margin: 0 auto;
505 }
522 }
506 }
523 }
507
524
508 button.close {
525 button.close {
509 padding: 0;
526 padding: 0;
510 cursor: pointer;
527 cursor: pointer;
511 background: transparent;
528 background: transparent;
512 border: 0;
529 border: 0;
513 .box-shadow(none);
530 .box-shadow(none);
514 -webkit-appearance: none;
531 -webkit-appearance: none;
515 }
532 }
516
533
517 .close {
534 .close {
518 float: right;
535 float: right;
519 font-size: 21px;
536 font-size: 21px;
520 font-family: @text-bootstrap;
537 font-family: @text-bootstrap;
521 line-height: 1em;
538 line-height: 1em;
522 font-weight: bold;
539 font-weight: bold;
523 color: @grey2;
540 color: @grey2;
524
541
525 &:hover,
542 &:hover,
526 &:focus {
543 &:focus {
527 color: @grey1;
544 color: @grey1;
528 text-decoration: none;
545 text-decoration: none;
529 cursor: pointer;
546 cursor: pointer;
530 }
547 }
531 }
548 }
532
549
533 // GRID
550 // GRID
534 .sorting,
551 .sorting,
535 .sorting_desc,
552 .sorting_desc,
536 .sorting_asc {
553 .sorting_asc {
537 cursor: pointer;
554 cursor: pointer;
538 }
555 }
539 .sorting_desc:after {
556 .sorting_desc:after {
540 content: "\00A0\25B2";
557 content: "\00A0\25B2";
541 font-size: .75em;
558 font-size: .75em;
542 }
559 }
543 .sorting_asc:after {
560 .sorting_asc:after {
544 content: "\00A0\25BC";
561 content: "\00A0\25BC";
545 font-size: .68em;
562 font-size: .68em;
546 }
563 }
547
564
548
565
549 .user_auth_tokens {
566 .user_auth_tokens {
550
567
551 &.truncate {
568 &.truncate {
552 white-space: nowrap;
569 white-space: nowrap;
553 overflow: hidden;
570 overflow: hidden;
554 text-overflow: ellipsis;
571 text-overflow: ellipsis;
555 }
572 }
556
573
557 .fields .field .input {
574 .fields .field .input {
558 margin: 0;
575 margin: 0;
559 }
576 }
560
577
561 input#description {
578 input#description {
562 width: 100px;
579 width: 100px;
563 margin: 0;
580 margin: 0;
564 }
581 }
565
582
566 .drop-menu {
583 .drop-menu {
567 // TODO: johbo: Remove this, should work out of the box when
584 // TODO: johbo: Remove this, should work out of the box when
568 // having multiple inputs inline
585 // having multiple inputs inline
569 margin: 0 0 0 5px;
586 margin: 0 0 0 5px;
570 }
587 }
571 }
588 }
572 #user_list_table {
589 #user_list_table {
573 .closed {
590 .closed {
574 background-color: @grey6;
591 background-color: @grey6;
575 }
592 }
576 }
593 }
577
594
578
595
579 input {
596 input {
580 &.disabled {
597 &.disabled {
581 opacity: .5;
598 opacity: .5;
582 }
599 }
583 }
600 }
584
601
585 // remove extra padding in firefox
602 // remove extra padding in firefox
586 input::-moz-focus-inner { border:0; padding:0 }
603 input::-moz-focus-inner { border:0; padding:0 }
587
604
588 .adjacent input {
605 .adjacent input {
589 margin-bottom: @padding;
606 margin-bottom: @padding;
590 }
607 }
591
608
592 .permissions_boxes {
609 .permissions_boxes {
593 display: block;
610 display: block;
594 }
611 }
595
612
596 //TODO: lisa: this should be in tables
613 //TODO: lisa: this should be in tables
597 .show_more_col {
614 .show_more_col {
598 width: 20px;
615 width: 20px;
599 }
616 }
600
617
601 //FORMS
618 //FORMS
602
619
603 .medium-inline,
620 .medium-inline,
604 input#description.medium-inline {
621 input#description.medium-inline {
605 display: inline;
622 display: inline;
606 width: @medium-inline-input-width;
623 width: @medium-inline-input-width;
607 min-width: 100px;
624 min-width: 100px;
608 }
625 }
609
626
610 select {
627 select {
611 //reset
628 //reset
612 -webkit-appearance: none;
629 -webkit-appearance: none;
613 -moz-appearance: none;
630 -moz-appearance: none;
614
631
615 display: inline-block;
632 display: inline-block;
616 height: 28px;
633 height: 28px;
617 width: auto;
634 width: auto;
618 margin: 0 @padding @padding 0;
635 margin: 0 @padding @padding 0;
619 padding: 0 18px 0 8px;
636 padding: 0 18px 0 8px;
620 line-height:1em;
637 line-height:1em;
621 font-size: @basefontsize;
638 font-size: @basefontsize;
622 border: @border-thickness solid @rcblue;
639 border: @border-thickness solid @rcblue;
623 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
640 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
624 color: @rcblue;
641 color: @rcblue;
625
642
626 &:after {
643 &:after {
627 content: "\00A0\25BE";
644 content: "\00A0\25BE";
628 }
645 }
629
646
630 &:focus {
647 &:focus {
631 outline: none;
648 outline: none;
632 }
649 }
633 }
650 }
634
651
635 option {
652 option {
636 &:focus {
653 &:focus {
637 outline: none;
654 outline: none;
638 }
655 }
639 }
656 }
640
657
641 input,
658 input,
642 textarea {
659 textarea {
643 padding: @input-padding;
660 padding: @input-padding;
644 border: @input-border-thickness solid @border-highlight-color;
661 border: @input-border-thickness solid @border-highlight-color;
645 .border-radius (@border-radius);
662 .border-radius (@border-radius);
646 font-family: @text-light;
663 font-family: @text-light;
647 font-size: @basefontsize;
664 font-size: @basefontsize;
648
665
649 &.input-sm {
666 &.input-sm {
650 padding: 5px;
667 padding: 5px;
651 }
668 }
652
669
653 &#description {
670 &#description {
654 min-width: @input-description-minwidth;
671 min-width: @input-description-minwidth;
655 min-height: 1em;
672 min-height: 1em;
656 padding: 10px;
673 padding: 10px;
657 }
674 }
658 }
675 }
659
676
660 .field-sm {
677 .field-sm {
661 input,
678 input,
662 textarea {
679 textarea {
663 padding: 5px;
680 padding: 5px;
664 }
681 }
665 }
682 }
666
683
667 textarea {
684 textarea {
668 display: block;
685 display: block;
669 clear: both;
686 clear: both;
670 width: 100%;
687 width: 100%;
671 min-height: 100px;
688 min-height: 100px;
672 margin-bottom: @padding;
689 margin-bottom: @padding;
673 .box-sizing(border-box);
690 .box-sizing(border-box);
674 overflow: auto;
691 overflow: auto;
675 }
692 }
676
693
677 label {
694 label {
678 font-family: @text-light;
695 font-family: @text-light;
679 }
696 }
680
697
681 // GRAVATARS
698 // GRAVATARS
682 // centers gravatar on username to the right
699 // centers gravatar on username to the right
683
700
684 .gravatar {
701 .gravatar {
685 display: inline;
702 display: inline;
686 min-width: 16px;
703 min-width: 16px;
687 min-height: 16px;
704 min-height: 16px;
688 margin: -5px 0;
705 margin: -5px 0;
689 padding: 0;
706 padding: 0;
690 line-height: 1em;
707 line-height: 1em;
691 border: 1px solid @grey4;
708 border: 1px solid @grey4;
692 box-sizing: content-box;
709 box-sizing: content-box;
693
710
694 &.gravatar-large {
711 &.gravatar-large {
695 margin: -0.5em .25em -0.5em 0;
712 margin: -0.5em .25em -0.5em 0;
696 }
713 }
697
714
698 & + .user {
715 & + .user {
699 display: inline;
716 display: inline;
700 margin: 0;
717 margin: 0;
701 padding: 0 0 0 .17em;
718 padding: 0 0 0 .17em;
702 line-height: 1em;
719 line-height: 1em;
703 }
720 }
704 }
721 }
705
722
706 .user-inline-data {
723 .user-inline-data {
707 display: inline-block;
724 display: inline-block;
708 float: left;
725 float: left;
709 padding-left: .5em;
726 padding-left: .5em;
710 line-height: 1.3em;
727 line-height: 1.3em;
711 }
728 }
712
729
713 .rc-user { // gravatar + user wrapper
730 .rc-user { // gravatar + user wrapper
714 float: left;
731 float: left;
715 position: relative;
732 position: relative;
716 min-width: 100px;
733 min-width: 100px;
717 max-width: 200px;
734 max-width: 200px;
718 min-height: (@gravatar-size + @border-thickness * 2); // account for border
735 min-height: (@gravatar-size + @border-thickness * 2); // account for border
719 display: block;
736 display: block;
720 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
737 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
721
738
722
739
723 .gravatar {
740 .gravatar {
724 display: block;
741 display: block;
725 position: absolute;
742 position: absolute;
726 top: 0;
743 top: 0;
727 left: 0;
744 left: 0;
728 min-width: @gravatar-size;
745 min-width: @gravatar-size;
729 min-height: @gravatar-size;
746 min-height: @gravatar-size;
730 margin: 0;
747 margin: 0;
731 }
748 }
732
749
733 .user {
750 .user {
734 display: block;
751 display: block;
735 max-width: 175px;
752 max-width: 175px;
736 padding-top: 2px;
753 padding-top: 2px;
737 overflow: hidden;
754 overflow: hidden;
738 text-overflow: ellipsis;
755 text-overflow: ellipsis;
739 }
756 }
740 }
757 }
741
758
742 .gist-gravatar,
759 .gist-gravatar,
743 .journal_container {
760 .journal_container {
744 .gravatar-large {
761 .gravatar-large {
745 margin: 0 .5em -10px 0;
762 margin: 0 .5em -10px 0;
746 }
763 }
747 }
764 }
748
765
749
766
750 // ADMIN SETTINGS
767 // ADMIN SETTINGS
751
768
752 // Tag Patterns
769 // Tag Patterns
753 .tag_patterns {
770 .tag_patterns {
754 .tag_input {
771 .tag_input {
755 margin-bottom: @padding;
772 margin-bottom: @padding;
756 }
773 }
757 }
774 }
758
775
759 .locked_input {
776 .locked_input {
760 position: relative;
777 position: relative;
761
778
762 input {
779 input {
763 display: inline;
780 display: inline;
764 margin: 3px 5px 0px 0px;
781 margin: 3px 5px 0px 0px;
765 }
782 }
766
783
767 br {
784 br {
768 display: none;
785 display: none;
769 }
786 }
770
787
771 .error-message {
788 .error-message {
772 float: left;
789 float: left;
773 width: 100%;
790 width: 100%;
774 }
791 }
775
792
776 .lock_input_button {
793 .lock_input_button {
777 display: inline;
794 display: inline;
778 }
795 }
779
796
780 .help-block {
797 .help-block {
781 clear: both;
798 clear: both;
782 }
799 }
783 }
800 }
784
801
785 // Notifications
802 // Notifications
786
803
787 .notifications_buttons {
804 .notifications_buttons {
788 margin: 0 0 @space 0;
805 margin: 0 0 @space 0;
789 padding: 0;
806 padding: 0;
790
807
791 .btn {
808 .btn {
792 display: inline-block;
809 display: inline-block;
793 }
810 }
794 }
811 }
795
812
796 .notification-list {
813 .notification-list {
797
814
798 div {
815 div {
799 display: inline-block;
816 display: inline-block;
800 vertical-align: middle;
817 vertical-align: middle;
801 }
818 }
802
819
803 .container {
820 .container {
804 display: block;
821 display: block;
805 margin: 0 0 @padding 0;
822 margin: 0 0 @padding 0;
806 }
823 }
807
824
808 .delete-notifications {
825 .delete-notifications {
809 margin-left: @padding;
826 margin-left: @padding;
810 text-align: right;
827 text-align: right;
811 cursor: pointer;
828 cursor: pointer;
812 }
829 }
813
830
814 .read-notifications {
831 .read-notifications {
815 margin-left: @padding/2;
832 margin-left: @padding/2;
816 text-align: right;
833 text-align: right;
817 width: 35px;
834 width: 35px;
818 cursor: pointer;
835 cursor: pointer;
819 }
836 }
820
837
821 .icon-minus-sign {
838 .icon-minus-sign {
822 color: @alert2;
839 color: @alert2;
823 }
840 }
824
841
825 .icon-ok-sign {
842 .icon-ok-sign {
826 color: @alert1;
843 color: @alert1;
827 }
844 }
828 }
845 }
829
846
830 .user_settings {
847 .user_settings {
831 float: left;
848 float: left;
832 clear: both;
849 clear: both;
833 display: block;
850 display: block;
834 width: 100%;
851 width: 100%;
835
852
836 .gravatar_box {
853 .gravatar_box {
837 margin-bottom: @padding;
854 margin-bottom: @padding;
838
855
839 &:after {
856 &:after {
840 content: " ";
857 content: " ";
841 clear: both;
858 clear: both;
842 width: 100%;
859 width: 100%;
843 }
860 }
844 }
861 }
845
862
846 .fields .field {
863 .fields .field {
847 clear: both;
864 clear: both;
848 }
865 }
849 }
866 }
850
867
851 .advanced_settings {
868 .advanced_settings {
852 margin-bottom: @space;
869 margin-bottom: @space;
853
870
854 .help-block {
871 .help-block {
855 margin-left: 0;
872 margin-left: 0;
856 }
873 }
857
874
858 button + .help-block {
875 button + .help-block {
859 margin-top: @padding;
876 margin-top: @padding;
860 }
877 }
861 }
878 }
862
879
863 // admin settings radio buttons and labels
880 // admin settings radio buttons and labels
864 .label-2 {
881 .label-2 {
865 float: left;
882 float: left;
866 width: @label2-width;
883 width: @label2-width;
867
884
868 label {
885 label {
869 color: @grey1;
886 color: @grey1;
870 }
887 }
871 }
888 }
872 .checkboxes {
889 .checkboxes {
873 float: left;
890 float: left;
874 width: @checkboxes-width;
891 width: @checkboxes-width;
875 margin-bottom: @padding;
892 margin-bottom: @padding;
876
893
877 .checkbox {
894 .checkbox {
878 width: 100%;
895 width: 100%;
879
896
880 label {
897 label {
881 margin: 0;
898 margin: 0;
882 padding: 0;
899 padding: 0;
883 }
900 }
884 }
901 }
885
902
886 .checkbox + .checkbox {
903 .checkbox + .checkbox {
887 display: inline-block;
904 display: inline-block;
888 }
905 }
889
906
890 label {
907 label {
891 margin-right: 1em;
908 margin-right: 1em;
892 }
909 }
893 }
910 }
894
911
895 // CHANGELOG
912 // CHANGELOG
896 .container_header {
913 .container_header {
897 float: left;
914 float: left;
898 display: block;
915 display: block;
899 width: 100%;
916 width: 100%;
900 margin: @padding 0 @padding;
917 margin: @padding 0 @padding;
901
918
902 #filter_changelog {
919 #filter_changelog {
903 float: left;
920 float: left;
904 margin-right: @padding;
921 margin-right: @padding;
905 }
922 }
906
923
907 .breadcrumbs_light {
924 .breadcrumbs_light {
908 display: inline-block;
925 display: inline-block;
909 }
926 }
910 }
927 }
911
928
912 .info_box {
929 .info_box {
913 float: right;
930 float: right;
914 }
931 }
915
932
916
933
917 #graph_nodes {
934 #graph_nodes {
918 padding-top: 43px;
935 padding-top: 43px;
919 }
936 }
920
937
921 #graph_content{
938 #graph_content{
922
939
923 // adjust for table headers so that graph renders properly
940 // adjust for table headers so that graph renders properly
924 // #graph_nodes padding - table cell padding
941 // #graph_nodes padding - table cell padding
925 padding-top: (@space - (@basefontsize * 2.4));
942 padding-top: (@space - (@basefontsize * 2.4));
926
943
927 &.graph_full_width {
944 &.graph_full_width {
928 width: 100%;
945 width: 100%;
929 max-width: 100%;
946 max-width: 100%;
930 }
947 }
931 }
948 }
932
949
933 #graph {
950 #graph {
934 .flag_status {
951 .flag_status {
935 margin: 0;
952 margin: 0;
936 }
953 }
937
954
938 .pagination-left {
955 .pagination-left {
939 float: left;
956 float: left;
940 clear: both;
957 clear: both;
941 }
958 }
942
959
943 .log-container {
960 .log-container {
944 max-width: 345px;
961 max-width: 345px;
945
962
946 .message{
963 .message{
947 max-width: 340px;
964 max-width: 340px;
948 }
965 }
949 }
966 }
950
967
951 .graph-col-wrapper {
968 .graph-col-wrapper {
952 padding-left: 110px;
969 padding-left: 110px;
953
970
954 #graph_nodes {
971 #graph_nodes {
955 width: 100px;
972 width: 100px;
956 margin-left: -110px;
973 margin-left: -110px;
957 float: left;
974 float: left;
958 clear: left;
975 clear: left;
959 }
976 }
960 }
977 }
961
978
962 .load-more-commits {
979 .load-more-commits {
963 text-align: center;
980 text-align: center;
964 }
981 }
965 .load-more-commits:hover {
982 .load-more-commits:hover {
966 background-color: @grey7;
983 background-color: @grey7;
967 }
984 }
968 .load-more-commits {
985 .load-more-commits {
969 a {
986 a {
970 display: block;
987 display: block;
971 }
988 }
972 }
989 }
973 }
990 }
974
991
975 #filter_changelog {
992 #filter_changelog {
976 float: left;
993 float: left;
977 }
994 }
978
995
979
996
980 //--- THEME ------------------//
997 //--- THEME ------------------//
981
998
982 #logo {
999 #logo {
983 float: left;
1000 float: left;
984 margin: 9px 0 0 0;
1001 margin: 9px 0 0 0;
985
1002
986 .header {
1003 .header {
987 background-color: transparent;
1004 background-color: transparent;
988 }
1005 }
989
1006
990 a {
1007 a {
991 display: inline-block;
1008 display: inline-block;
992 }
1009 }
993
1010
994 img {
1011 img {
995 height:30px;
1012 height:30px;
996 }
1013 }
997 }
1014 }
998
1015
999 .logo-wrapper {
1016 .logo-wrapper {
1000 float:left;
1017 float:left;
1001 }
1018 }
1002
1019
1003 .branding{
1020 .branding{
1004 float: left;
1021 float: left;
1005 padding: 9px 2px;
1022 padding: 9px 2px;
1006 line-height: 1em;
1023 line-height: 1em;
1007 font-size: @navigation-fontsize;
1024 font-size: @navigation-fontsize;
1008 }
1025 }
1009
1026
1010 img {
1027 img {
1011 border: none;
1028 border: none;
1012 outline: none;
1029 outline: none;
1013 }
1030 }
1014 user-profile-header
1031 user-profile-header
1015 label {
1032 label {
1016
1033
1017 input[type="checkbox"] {
1034 input[type="checkbox"] {
1018 margin-right: 1em;
1035 margin-right: 1em;
1019 }
1036 }
1020 input[type="radio"] {
1037 input[type="radio"] {
1021 margin-right: 1em;
1038 margin-right: 1em;
1022 }
1039 }
1023 }
1040 }
1024
1041
1025 .flag_status {
1042 .flag_status {
1026 margin: 2px 8px 6px 2px;
1043 margin: 2px 8px 6px 2px;
1027 &.under_review {
1044 &.under_review {
1028 .circle(5px, @alert3);
1045 .circle(5px, @alert3);
1029 }
1046 }
1030 &.approved {
1047 &.approved {
1031 .circle(5px, @alert1);
1048 .circle(5px, @alert1);
1032 }
1049 }
1033 &.rejected,
1050 &.rejected,
1034 &.forced_closed{
1051 &.forced_closed{
1035 .circle(5px, @alert2);
1052 .circle(5px, @alert2);
1036 }
1053 }
1037 &.not_reviewed {
1054 &.not_reviewed {
1038 .circle(5px, @grey5);
1055 .circle(5px, @grey5);
1039 }
1056 }
1040 }
1057 }
1041
1058
1042 .flag_status_comment_box {
1059 .flag_status_comment_box {
1043 margin: 5px 6px 0px 2px;
1060 margin: 5px 6px 0px 2px;
1044 }
1061 }
1045 .test_pattern_preview {
1062 .test_pattern_preview {
1046 margin: @space 0;
1063 margin: @space 0;
1047
1064
1048 p {
1065 p {
1049 margin-bottom: 0;
1066 margin-bottom: 0;
1050 border-bottom: @border-thickness solid @border-default-color;
1067 border-bottom: @border-thickness solid @border-default-color;
1051 color: @grey3;
1068 color: @grey3;
1052 }
1069 }
1053
1070
1054 .btn {
1071 .btn {
1055 margin-bottom: @padding;
1072 margin-bottom: @padding;
1056 }
1073 }
1057 }
1074 }
1058 #test_pattern_result {
1075 #test_pattern_result {
1059 display: none;
1076 display: none;
1060 &:extend(pre);
1077 &:extend(pre);
1061 padding: .9em;
1078 padding: .9em;
1062 color: @grey3;
1079 color: @grey3;
1063 background-color: @grey7;
1080 background-color: @grey7;
1064 border-right: @border-thickness solid @border-default-color;
1081 border-right: @border-thickness solid @border-default-color;
1065 border-bottom: @border-thickness solid @border-default-color;
1082 border-bottom: @border-thickness solid @border-default-color;
1066 border-left: @border-thickness solid @border-default-color;
1083 border-left: @border-thickness solid @border-default-color;
1067 }
1084 }
1068
1085
1069 #repo_vcs_settings {
1086 #repo_vcs_settings {
1070 #inherit_overlay_vcs_default {
1087 #inherit_overlay_vcs_default {
1071 display: none;
1088 display: none;
1072 }
1089 }
1073 #inherit_overlay_vcs_custom {
1090 #inherit_overlay_vcs_custom {
1074 display: custom;
1091 display: custom;
1075 }
1092 }
1076 &.inherited {
1093 &.inherited {
1077 #inherit_overlay_vcs_default {
1094 #inherit_overlay_vcs_default {
1078 display: block;
1095 display: block;
1079 }
1096 }
1080 #inherit_overlay_vcs_custom {
1097 #inherit_overlay_vcs_custom {
1081 display: none;
1098 display: none;
1082 }
1099 }
1083 }
1100 }
1084 }
1101 }
1085
1102
1086 .issue-tracker-link {
1103 .issue-tracker-link {
1087 color: @rcblue;
1104 color: @rcblue;
1088 }
1105 }
1089
1106
1090 // Issue Tracker Table Show/Hide
1107 // Issue Tracker Table Show/Hide
1091 #repo_issue_tracker {
1108 #repo_issue_tracker {
1092 #inherit_overlay {
1109 #inherit_overlay {
1093 display: none;
1110 display: none;
1094 }
1111 }
1095 #custom_overlay {
1112 #custom_overlay {
1096 display: custom;
1113 display: custom;
1097 }
1114 }
1098 &.inherited {
1115 &.inherited {
1099 #inherit_overlay {
1116 #inherit_overlay {
1100 display: block;
1117 display: block;
1101 }
1118 }
1102 #custom_overlay {
1119 #custom_overlay {
1103 display: none;
1120 display: none;
1104 }
1121 }
1105 }
1122 }
1106 }
1123 }
1107 table.issuetracker {
1124 table.issuetracker {
1108 &.readonly {
1125 &.readonly {
1109 tr, td {
1126 tr, td {
1110 color: @grey3;
1127 color: @grey3;
1111 }
1128 }
1112 }
1129 }
1113 .edit {
1130 .edit {
1114 display: none;
1131 display: none;
1115 }
1132 }
1116 .editopen {
1133 .editopen {
1117 .edit {
1134 .edit {
1118 display: inline;
1135 display: inline;
1119 }
1136 }
1120 .entry {
1137 .entry {
1121 display: none;
1138 display: none;
1122 }
1139 }
1123 }
1140 }
1124 tr td.td-action {
1141 tr td.td-action {
1125 min-width: 117px;
1142 min-width: 117px;
1126 }
1143 }
1127 td input {
1144 td input {
1128 max-width: none;
1145 max-width: none;
1129 min-width: 30px;
1146 min-width: 30px;
1130 width: 80%;
1147 width: 80%;
1131 }
1148 }
1132 .issuetracker_pref input {
1149 .issuetracker_pref input {
1133 width: 40%;
1150 width: 40%;
1134 }
1151 }
1135 input.edit_issuetracker_update {
1152 input.edit_issuetracker_update {
1136 margin-right: 0;
1153 margin-right: 0;
1137 width: auto;
1154 width: auto;
1138 }
1155 }
1139 }
1156 }
1140
1157
1141 table.integrations {
1158 table.integrations {
1142 .td-icon {
1159 .td-icon {
1143 width: 20px;
1160 width: 20px;
1144 .integration-icon {
1161 .integration-icon {
1145 height: 20px;
1162 height: 20px;
1146 width: 20px;
1163 width: 20px;
1147 }
1164 }
1148 }
1165 }
1149 }
1166 }
1150
1167
1151 .integrations {
1168 .integrations {
1152 a.integration-box {
1169 a.integration-box {
1153 color: @text-color;
1170 color: @text-color;
1154 &:hover {
1171 &:hover {
1155 .panel {
1172 .panel {
1156 background: #fbfbfb;
1173 background: #fbfbfb;
1157 }
1174 }
1158 }
1175 }
1159 .integration-icon {
1176 .integration-icon {
1160 width: 30px;
1177 width: 30px;
1161 height: 30px;
1178 height: 30px;
1162 margin-right: 20px;
1179 margin-right: 20px;
1163 float: left;
1180 float: left;
1164 }
1181 }
1165
1182
1166 .panel-body {
1183 .panel-body {
1167 padding: 10px;
1184 padding: 10px;
1168 }
1185 }
1169 .panel {
1186 .panel {
1170 margin-bottom: 10px;
1187 margin-bottom: 10px;
1171 }
1188 }
1172 h2 {
1189 h2 {
1173 display: inline-block;
1190 display: inline-block;
1174 margin: 0;
1191 margin: 0;
1175 min-width: 140px;
1192 min-width: 140px;
1176 }
1193 }
1177 }
1194 }
1178 }
1195 }
1179
1196
1180 //Permissions Settings
1197 //Permissions Settings
1181 #add_perm {
1198 #add_perm {
1182 margin: 0 0 @padding;
1199 margin: 0 0 @padding;
1183 cursor: pointer;
1200 cursor: pointer;
1184 }
1201 }
1185
1202
1186 .perm_ac {
1203 .perm_ac {
1187 input {
1204 input {
1188 width: 95%;
1205 width: 95%;
1189 }
1206 }
1190 }
1207 }
1191
1208
1192 .autocomplete-suggestions {
1209 .autocomplete-suggestions {
1193 width: auto !important; // overrides autocomplete.js
1210 width: auto !important; // overrides autocomplete.js
1194 margin: 0;
1211 margin: 0;
1195 border: @border-thickness solid @rcblue;
1212 border: @border-thickness solid @rcblue;
1196 border-radius: @border-radius;
1213 border-radius: @border-radius;
1197 color: @rcblue;
1214 color: @rcblue;
1198 background-color: white;
1215 background-color: white;
1199 }
1216 }
1200 .autocomplete-selected {
1217 .autocomplete-selected {
1201 background: #F0F0F0;
1218 background: #F0F0F0;
1202 }
1219 }
1203 .ac-container-wrap {
1220 .ac-container-wrap {
1204 margin: 0;
1221 margin: 0;
1205 padding: 8px;
1222 padding: 8px;
1206 border-bottom: @border-thickness solid @rclightblue;
1223 border-bottom: @border-thickness solid @rclightblue;
1207 list-style-type: none;
1224 list-style-type: none;
1208 cursor: pointer;
1225 cursor: pointer;
1209
1226
1210 &:hover {
1227 &:hover {
1211 background-color: @rclightblue;
1228 background-color: @rclightblue;
1212 }
1229 }
1213
1230
1214 img {
1231 img {
1215 height: @gravatar-size;
1232 height: @gravatar-size;
1216 width: @gravatar-size;
1233 width: @gravatar-size;
1217 margin-right: 1em;
1234 margin-right: 1em;
1218 }
1235 }
1219
1236
1220 strong {
1237 strong {
1221 font-weight: normal;
1238 font-weight: normal;
1222 }
1239 }
1223 }
1240 }
1224
1241
1225 // Settings Dropdown
1242 // Settings Dropdown
1226 .user-menu .container {
1243 .user-menu .container {
1227 padding: 0 4px;
1244 padding: 0 4px;
1228 margin: 0;
1245 margin: 0;
1229 }
1246 }
1230
1247
1231 .user-menu .gravatar {
1248 .user-menu .gravatar {
1232 cursor: pointer;
1249 cursor: pointer;
1233 }
1250 }
1234
1251
1235 .codeblock {
1252 .codeblock {
1236 margin-bottom: @padding;
1253 margin-bottom: @padding;
1237 clear: both;
1254 clear: both;
1238
1255
1239 .stats{
1256 .stats{
1240 overflow: hidden;
1257 overflow: hidden;
1241 }
1258 }
1242
1259
1243 .message{
1260 .message{
1244 textarea{
1261 textarea{
1245 margin: 0;
1262 margin: 0;
1246 }
1263 }
1247 }
1264 }
1248
1265
1249 .code-header {
1266 .code-header {
1250 .stats {
1267 .stats {
1251 line-height: 2em;
1268 line-height: 2em;
1252
1269
1253 .revision_id {
1270 .revision_id {
1254 margin-left: 0;
1271 margin-left: 0;
1255 }
1272 }
1256 .buttons {
1273 .buttons {
1257 padding-right: 0;
1274 padding-right: 0;
1258 }
1275 }
1259 }
1276 }
1260
1277
1261 .item{
1278 .item{
1262 margin-right: 0.5em;
1279 margin-right: 0.5em;
1263 }
1280 }
1264 }
1281 }
1265
1282
1266 #editor_container{
1283 #editor_container{
1267 position: relative;
1284 position: relative;
1268 margin: @padding;
1285 margin: @padding;
1269 }
1286 }
1270 }
1287 }
1271
1288
1272 #file_history_container {
1289 #file_history_container {
1273 display: none;
1290 display: none;
1274 }
1291 }
1275
1292
1276 .file-history-inner {
1293 .file-history-inner {
1277 margin-bottom: 10px;
1294 margin-bottom: 10px;
1278 }
1295 }
1279
1296
1280 // Pull Requests
1297 // Pull Requests
1281 .summary-details {
1298 .summary-details {
1282 width: 72%;
1299 width: 72%;
1283 }
1300 }
1284 .pr-summary {
1301 .pr-summary {
1285 border-bottom: @border-thickness solid @grey5;
1302 border-bottom: @border-thickness solid @grey5;
1286 margin-bottom: @space;
1303 margin-bottom: @space;
1287 }
1304 }
1288 .reviewers-title {
1305 .reviewers-title {
1289 width: 25%;
1306 width: 25%;
1290 min-width: 200px;
1307 min-width: 200px;
1291 }
1308 }
1292 .reviewers {
1309 .reviewers {
1293 width: 25%;
1310 width: 25%;
1294 min-width: 200px;
1311 min-width: 200px;
1295 }
1312 }
1296 .reviewers ul li {
1313 .reviewers ul li {
1297 position: relative;
1314 position: relative;
1298 width: 100%;
1315 width: 100%;
1299 margin-bottom: 8px;
1316 margin-bottom: 8px;
1300 }
1317 }
1318
1319 .reviewer_entry {
1320 min-height: 55px;
1321 }
1322
1301 .reviewers_member {
1323 .reviewers_member {
1302 width: 100%;
1324 width: 100%;
1303 overflow: auto;
1325 overflow: auto;
1304 }
1326 }
1305 .reviewer_reason {
1327 .reviewer_reason {
1306 padding-left: 20px;
1328 padding-left: 20px;
1307 }
1329 }
1308 .reviewer_status {
1330 .reviewer_status {
1309 display: inline-block;
1331 display: inline-block;
1310 vertical-align: top;
1332 vertical-align: top;
1311 width: 7%;
1333 width: 7%;
1312 min-width: 20px;
1334 min-width: 20px;
1313 height: 1.2em;
1335 height: 1.2em;
1314 margin-top: 3px;
1336 margin-top: 3px;
1315 line-height: 1em;
1337 line-height: 1em;
1316 }
1338 }
1317
1339
1318 .reviewer_name {
1340 .reviewer_name {
1319 display: inline-block;
1341 display: inline-block;
1320 max-width: 83%;
1342 max-width: 83%;
1321 padding-right: 20px;
1343 padding-right: 20px;
1322 vertical-align: middle;
1344 vertical-align: middle;
1323 line-height: 1;
1345 line-height: 1;
1324
1346
1325 .rc-user {
1347 .rc-user {
1326 min-width: 0;
1348 min-width: 0;
1327 margin: -2px 1em 0 0;
1349 margin: -2px 1em 0 0;
1328 }
1350 }
1329
1351
1330 .reviewer {
1352 .reviewer {
1331 float: left;
1353 float: left;
1332 }
1354 }
1333 }
1355 }
1334
1356
1357 .reviewer_member_mandatory,
1358 .reviewer_member_mandatory_remove,
1335 .reviewer_member_remove {
1359 .reviewer_member_remove {
1336 position: absolute;
1360 position: absolute;
1337 right: 0;
1361 right: 0;
1338 top: 0;
1362 top: 0;
1339 width: 16px;
1363 width: 16px;
1340 margin-bottom: 10px;
1364 margin-bottom: 10px;
1341 padding: 0;
1365 padding: 0;
1342 color: black;
1366 color: black;
1343 }
1367 }
1368
1369 .reviewer_member_mandatory_remove {
1370 color: @grey4;
1371 }
1372
1373 .reviewer_member_mandatory {
1374 padding-top:20px;
1375 }
1376
1344 .reviewer_member_status {
1377 .reviewer_member_status {
1345 margin-top: 5px;
1378 margin-top: 5px;
1346 }
1379 }
1347 .pr-summary #summary{
1380 .pr-summary #summary{
1348 width: 100%;
1381 width: 100%;
1349 }
1382 }
1350 .pr-summary .action_button:hover {
1383 .pr-summary .action_button:hover {
1351 border: 0;
1384 border: 0;
1352 cursor: pointer;
1385 cursor: pointer;
1353 }
1386 }
1354 .pr-details-title {
1387 .pr-details-title {
1355 padding-bottom: 8px;
1388 padding-bottom: 8px;
1356 border-bottom: @border-thickness solid @grey5;
1389 border-bottom: @border-thickness solid @grey5;
1357
1390
1358 .action_button.disabled {
1391 .action_button.disabled {
1359 color: @grey4;
1392 color: @grey4;
1360 cursor: inherit;
1393 cursor: inherit;
1361 }
1394 }
1362 .action_button {
1395 .action_button {
1363 color: @rcblue;
1396 color: @rcblue;
1364 }
1397 }
1365 }
1398 }
1366 .pr-details-content {
1399 .pr-details-content {
1367 margin-top: @textmargin;
1400 margin-top: @textmargin;
1368 margin-bottom: @textmargin;
1401 margin-bottom: @textmargin;
1369 }
1402 }
1370 .pr-description {
1403 .pr-description {
1371 white-space:pre-wrap;
1404 white-space:pre-wrap;
1372 }
1405 }
1406
1407 .pr-reviewer-rules {
1408 padding: 10px 0px 20px 0px;
1409 }
1410
1373 .group_members {
1411 .group_members {
1374 margin-top: 0;
1412 margin-top: 0;
1375 padding: 0;
1413 padding: 0;
1376 list-style: outside none none;
1414 list-style: outside none none;
1377
1415
1378 img {
1416 img {
1379 height: @gravatar-size;
1417 height: @gravatar-size;
1380 width: @gravatar-size;
1418 width: @gravatar-size;
1381 margin-right: .5em;
1419 margin-right: .5em;
1382 margin-left: 3px;
1420 margin-left: 3px;
1383 }
1421 }
1384
1422
1385 .to-delete {
1423 .to-delete {
1386 .user {
1424 .user {
1387 text-decoration: line-through;
1425 text-decoration: line-through;
1388 }
1426 }
1389 }
1427 }
1390 }
1428 }
1391
1429
1392 .compare_view_commits_title {
1430 .compare_view_commits_title {
1393 .disabled {
1431 .disabled {
1394 cursor: inherit;
1432 cursor: inherit;
1395 &:hover{
1433 &:hover{
1396 background-color: inherit;
1434 background-color: inherit;
1397 color: inherit;
1435 color: inherit;
1398 }
1436 }
1399 }
1437 }
1400 }
1438 }
1401
1439
1402 .subtitle-compare {
1440 .subtitle-compare {
1403 margin: -15px 0px 0px 0px;
1441 margin: -15px 0px 0px 0px;
1404 }
1442 }
1405
1443
1406 .comments-summary-td {
1444 .comments-summary-td {
1407 border-top: 1px dashed @grey5;
1445 border-top: 1px dashed @grey5;
1408 }
1446 }
1409
1447
1410 // new entry in group_members
1448 // new entry in group_members
1411 .td-author-new-entry {
1449 .td-author-new-entry {
1412 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1450 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1413 }
1451 }
1414
1452
1415 .usergroup_member_remove {
1453 .usergroup_member_remove {
1416 width: 16px;
1454 width: 16px;
1417 margin-bottom: 10px;
1455 margin-bottom: 10px;
1418 padding: 0;
1456 padding: 0;
1419 color: black !important;
1457 color: black !important;
1420 cursor: pointer;
1458 cursor: pointer;
1421 }
1459 }
1422
1460
1423 .reviewer_ac .ac-input {
1461 .reviewer_ac .ac-input {
1424 width: 92%;
1462 width: 92%;
1425 margin-bottom: 1em;
1463 margin-bottom: 1em;
1426 }
1464 }
1427
1465
1428 .compare_view_commits tr{
1466 .compare_view_commits tr{
1429 height: 20px;
1467 height: 20px;
1430 }
1468 }
1431 .compare_view_commits td {
1469 .compare_view_commits td {
1432 vertical-align: top;
1470 vertical-align: top;
1433 padding-top: 10px;
1471 padding-top: 10px;
1434 }
1472 }
1435 .compare_view_commits .author {
1473 .compare_view_commits .author {
1436 margin-left: 5px;
1474 margin-left: 5px;
1437 }
1475 }
1438
1476
1439 .compare_view_commits {
1477 .compare_view_commits {
1440 .color-a {
1478 .color-a {
1441 color: @alert1;
1479 color: @alert1;
1442 }
1480 }
1443
1481
1444 .color-c {
1482 .color-c {
1445 color: @color3;
1483 color: @color3;
1446 }
1484 }
1447
1485
1448 .color-r {
1486 .color-r {
1449 color: @color5;
1487 color: @color5;
1450 }
1488 }
1451
1489
1452 .color-a-bg {
1490 .color-a-bg {
1453 background-color: @alert1;
1491 background-color: @alert1;
1454 }
1492 }
1455
1493
1456 .color-c-bg {
1494 .color-c-bg {
1457 background-color: @alert3;
1495 background-color: @alert3;
1458 }
1496 }
1459
1497
1460 .color-r-bg {
1498 .color-r-bg {
1461 background-color: @alert2;
1499 background-color: @alert2;
1462 }
1500 }
1463
1501
1464 .color-a-border {
1502 .color-a-border {
1465 border: 1px solid @alert1;
1503 border: 1px solid @alert1;
1466 }
1504 }
1467
1505
1468 .color-c-border {
1506 .color-c-border {
1469 border: 1px solid @alert3;
1507 border: 1px solid @alert3;
1470 }
1508 }
1471
1509
1472 .color-r-border {
1510 .color-r-border {
1473 border: 1px solid @alert2;
1511 border: 1px solid @alert2;
1474 }
1512 }
1475
1513
1476 .commit-change-indicator {
1514 .commit-change-indicator {
1477 width: 15px;
1515 width: 15px;
1478 height: 15px;
1516 height: 15px;
1479 position: relative;
1517 position: relative;
1480 left: 15px;
1518 left: 15px;
1481 }
1519 }
1482
1520
1483 .commit-change-content {
1521 .commit-change-content {
1484 text-align: center;
1522 text-align: center;
1485 vertical-align: middle;
1523 vertical-align: middle;
1486 line-height: 15px;
1524 line-height: 15px;
1487 }
1525 }
1488 }
1526 }
1489
1527
1490 .compare_view_files {
1528 .compare_view_files {
1491 width: 100%;
1529 width: 100%;
1492
1530
1493 td {
1531 td {
1494 vertical-align: middle;
1532 vertical-align: middle;
1495 }
1533 }
1496 }
1534 }
1497
1535
1498 .compare_view_filepath {
1536 .compare_view_filepath {
1499 color: @grey1;
1537 color: @grey1;
1500 }
1538 }
1501
1539
1502 .show_more {
1540 .show_more {
1503 display: inline-block;
1541 display: inline-block;
1504 position: relative;
1542 position: relative;
1505 vertical-align: middle;
1543 vertical-align: middle;
1506 width: 4px;
1544 width: 4px;
1507 height: @basefontsize;
1545 height: @basefontsize;
1508
1546
1509 &:after {
1547 &:after {
1510 content: "\00A0\25BE";
1548 content: "\00A0\25BE";
1511 display: inline-block;
1549 display: inline-block;
1512 width:10px;
1550 width:10px;
1513 line-height: 5px;
1551 line-height: 5px;
1514 font-size: 12px;
1552 font-size: 12px;
1515 cursor: pointer;
1553 cursor: pointer;
1516 }
1554 }
1517 }
1555 }
1518
1556
1519 .journal_more .show_more {
1557 .journal_more .show_more {
1520 display: inline;
1558 display: inline;
1521
1559
1522 &:after {
1560 &:after {
1523 content: none;
1561 content: none;
1524 }
1562 }
1525 }
1563 }
1526
1564
1527 .open .show_more:after,
1565 .open .show_more:after,
1528 .select2-dropdown-open .show_more:after {
1566 .select2-dropdown-open .show_more:after {
1529 .rotate(180deg);
1567 .rotate(180deg);
1530 margin-left: 4px;
1568 margin-left: 4px;
1531 }
1569 }
1532
1570
1533
1571
1534 .compare_view_commits .collapse_commit:after {
1572 .compare_view_commits .collapse_commit:after {
1535 cursor: pointer;
1573 cursor: pointer;
1536 content: "\00A0\25B4";
1574 content: "\00A0\25B4";
1537 margin-left: -3px;
1575 margin-left: -3px;
1538 font-size: 17px;
1576 font-size: 17px;
1539 color: @grey4;
1577 color: @grey4;
1540 }
1578 }
1541
1579
1542 .diff_links {
1580 .diff_links {
1543 margin-left: 8px;
1581 margin-left: 8px;
1544 }
1582 }
1545
1583
1546 div.ancestor {
1584 div.ancestor {
1547 margin: -30px 0px;
1585 margin: -30px 0px;
1548 }
1586 }
1549
1587
1550 .cs_icon_td input[type="checkbox"] {
1588 .cs_icon_td input[type="checkbox"] {
1551 display: none;
1589 display: none;
1552 }
1590 }
1553
1591
1554 .cs_icon_td .expand_file_icon:after {
1592 .cs_icon_td .expand_file_icon:after {
1555 cursor: pointer;
1593 cursor: pointer;
1556 content: "\00A0\25B6";
1594 content: "\00A0\25B6";
1557 font-size: 12px;
1595 font-size: 12px;
1558 color: @grey4;
1596 color: @grey4;
1559 }
1597 }
1560
1598
1561 .cs_icon_td .collapse_file_icon:after {
1599 .cs_icon_td .collapse_file_icon:after {
1562 cursor: pointer;
1600 cursor: pointer;
1563 content: "\00A0\25BC";
1601 content: "\00A0\25BC";
1564 font-size: 12px;
1602 font-size: 12px;
1565 color: @grey4;
1603 color: @grey4;
1566 }
1604 }
1567
1605
1568 /*new binary
1606 /*new binary
1569 NEW_FILENODE = 1
1607 NEW_FILENODE = 1
1570 DEL_FILENODE = 2
1608 DEL_FILENODE = 2
1571 MOD_FILENODE = 3
1609 MOD_FILENODE = 3
1572 RENAMED_FILENODE = 4
1610 RENAMED_FILENODE = 4
1573 COPIED_FILENODE = 5
1611 COPIED_FILENODE = 5
1574 CHMOD_FILENODE = 6
1612 CHMOD_FILENODE = 6
1575 BIN_FILENODE = 7
1613 BIN_FILENODE = 7
1576 */
1614 */
1577 .cs_files_expand {
1615 .cs_files_expand {
1578 font-size: @basefontsize + 5px;
1616 font-size: @basefontsize + 5px;
1579 line-height: 1.8em;
1617 line-height: 1.8em;
1580 float: right;
1618 float: right;
1581 }
1619 }
1582
1620
1583 .cs_files_expand span{
1621 .cs_files_expand span{
1584 color: @rcblue;
1622 color: @rcblue;
1585 cursor: pointer;
1623 cursor: pointer;
1586 }
1624 }
1587 .cs_files {
1625 .cs_files {
1588 clear: both;
1626 clear: both;
1589 padding-bottom: @padding;
1627 padding-bottom: @padding;
1590
1628
1591 .cur_cs {
1629 .cur_cs {
1592 margin: 10px 2px;
1630 margin: 10px 2px;
1593 font-weight: bold;
1631 font-weight: bold;
1594 }
1632 }
1595
1633
1596 .node {
1634 .node {
1597 float: left;
1635 float: left;
1598 }
1636 }
1599
1637
1600 .changes {
1638 .changes {
1601 float: right;
1639 float: right;
1602 color: white;
1640 color: white;
1603 font-size: @basefontsize - 4px;
1641 font-size: @basefontsize - 4px;
1604 margin-top: 4px;
1642 margin-top: 4px;
1605 opacity: 0.6;
1643 opacity: 0.6;
1606 filter: Alpha(opacity=60); /* IE8 and earlier */
1644 filter: Alpha(opacity=60); /* IE8 and earlier */
1607
1645
1608 .added {
1646 .added {
1609 background-color: @alert1;
1647 background-color: @alert1;
1610 float: left;
1648 float: left;
1611 text-align: center;
1649 text-align: center;
1612 }
1650 }
1613
1651
1614 .deleted {
1652 .deleted {
1615 background-color: @alert2;
1653 background-color: @alert2;
1616 float: left;
1654 float: left;
1617 text-align: center;
1655 text-align: center;
1618 }
1656 }
1619
1657
1620 .bin {
1658 .bin {
1621 background-color: @alert1;
1659 background-color: @alert1;
1622 text-align: center;
1660 text-align: center;
1623 }
1661 }
1624
1662
1625 /*new binary*/
1663 /*new binary*/
1626 .bin.bin1 {
1664 .bin.bin1 {
1627 background-color: @alert1;
1665 background-color: @alert1;
1628 text-align: center;
1666 text-align: center;
1629 }
1667 }
1630
1668
1631 /*deleted binary*/
1669 /*deleted binary*/
1632 .bin.bin2 {
1670 .bin.bin2 {
1633 background-color: @alert2;
1671 background-color: @alert2;
1634 text-align: center;
1672 text-align: center;
1635 }
1673 }
1636
1674
1637 /*mod binary*/
1675 /*mod binary*/
1638 .bin.bin3 {
1676 .bin.bin3 {
1639 background-color: @grey2;
1677 background-color: @grey2;
1640 text-align: center;
1678 text-align: center;
1641 }
1679 }
1642
1680
1643 /*rename file*/
1681 /*rename file*/
1644 .bin.bin4 {
1682 .bin.bin4 {
1645 background-color: @alert4;
1683 background-color: @alert4;
1646 text-align: center;
1684 text-align: center;
1647 }
1685 }
1648
1686
1649 /*copied file*/
1687 /*copied file*/
1650 .bin.bin5 {
1688 .bin.bin5 {
1651 background-color: @alert4;
1689 background-color: @alert4;
1652 text-align: center;
1690 text-align: center;
1653 }
1691 }
1654
1692
1655 /*chmod file*/
1693 /*chmod file*/
1656 .bin.bin6 {
1694 .bin.bin6 {
1657 background-color: @grey2;
1695 background-color: @grey2;
1658 text-align: center;
1696 text-align: center;
1659 }
1697 }
1660 }
1698 }
1661 }
1699 }
1662
1700
1663 .cs_files .cs_added, .cs_files .cs_A,
1701 .cs_files .cs_added, .cs_files .cs_A,
1664 .cs_files .cs_added, .cs_files .cs_M,
1702 .cs_files .cs_added, .cs_files .cs_M,
1665 .cs_files .cs_added, .cs_files .cs_D {
1703 .cs_files .cs_added, .cs_files .cs_D {
1666 height: 16px;
1704 height: 16px;
1667 padding-right: 10px;
1705 padding-right: 10px;
1668 margin-top: 7px;
1706 margin-top: 7px;
1669 text-align: left;
1707 text-align: left;
1670 }
1708 }
1671
1709
1672 .cs_icon_td {
1710 .cs_icon_td {
1673 min-width: 16px;
1711 min-width: 16px;
1674 width: 16px;
1712 width: 16px;
1675 }
1713 }
1676
1714
1677 .pull-request-merge {
1715 .pull-request-merge {
1678 border: 1px solid @grey5;
1716 border: 1px solid @grey5;
1679 padding: 10px 0px 20px;
1717 padding: 10px 0px 20px;
1680 margin-top: 10px;
1718 margin-top: 10px;
1681 margin-bottom: 20px;
1719 margin-bottom: 20px;
1682 }
1720 }
1683
1721
1684 .pull-request-merge ul {
1722 .pull-request-merge ul {
1685 padding: 0px 0px;
1723 padding: 0px 0px;
1686 }
1724 }
1687
1725
1688 .pull-request-merge li:before{
1726 .pull-request-merge li:before{
1689 content:none;
1727 content:none;
1690 }
1728 }
1691
1729
1692 .pull-request-merge .pull-request-wrap {
1730 .pull-request-merge .pull-request-wrap {
1693 height: auto;
1731 height: auto;
1694 padding: 0px 0px;
1732 padding: 0px 0px;
1695 text-align: right;
1733 text-align: right;
1696 }
1734 }
1697
1735
1698 .pull-request-merge span {
1736 .pull-request-merge span {
1699 margin-right: 5px;
1737 margin-right: 5px;
1700 }
1738 }
1701
1739
1702 .pull-request-merge-actions {
1740 .pull-request-merge-actions {
1703 height: 30px;
1741 height: 30px;
1704 padding: 0px 0px;
1742 padding: 0px 0px;
1705 }
1743 }
1706
1744
1707 .merge-status {
1745 .merge-status {
1708 margin-right: 5px;
1746 margin-right: 5px;
1709 }
1747 }
1710
1748
1711 .merge-message {
1749 .merge-message {
1712 font-size: 1.2em
1750 font-size: 1.2em
1713 }
1751 }
1714
1752
1715 .merge-message.success i,
1753 .merge-message.success i,
1716 .merge-icon.success i {
1754 .merge-icon.success i {
1717 color:@alert1;
1755 color:@alert1;
1718 }
1756 }
1719
1757
1720 .merge-message.warning i,
1758 .merge-message.warning i,
1721 .merge-icon.warning i {
1759 .merge-icon.warning i {
1722 color: @alert3;
1760 color: @alert3;
1723 }
1761 }
1724
1762
1725 .merge-message.error i,
1763 .merge-message.error i,
1726 .merge-icon.error i {
1764 .merge-icon.error i {
1727 color:@alert2;
1765 color:@alert2;
1728 }
1766 }
1729
1767
1730 .pr-versions {
1768 .pr-versions {
1731 font-size: 1.1em;
1769 font-size: 1.1em;
1732
1770
1733 table {
1771 table {
1734 padding: 0px 5px;
1772 padding: 0px 5px;
1735 }
1773 }
1736
1774
1737 td {
1775 td {
1738 line-height: 15px;
1776 line-height: 15px;
1739 }
1777 }
1740
1778
1741 .flag_status {
1779 .flag_status {
1742 margin: 0;
1780 margin: 0;
1743 }
1781 }
1744
1782
1745 .compare-radio-button {
1783 .compare-radio-button {
1746 position: relative;
1784 position: relative;
1747 top: -3px;
1785 top: -3px;
1748 }
1786 }
1749 }
1787 }
1750
1788
1751
1789
1752 #close_pull_request {
1790 #close_pull_request {
1753 margin-right: 0px;
1791 margin-right: 0px;
1754 }
1792 }
1755
1793
1756 .empty_data {
1794 .empty_data {
1757 color: @grey4;
1795 color: @grey4;
1758 }
1796 }
1759
1797
1760 #changeset_compare_view_content {
1798 #changeset_compare_view_content {
1761 margin-bottom: @space;
1799 margin-bottom: @space;
1762 clear: both;
1800 clear: both;
1763 width: 100%;
1801 width: 100%;
1764 box-sizing: border-box;
1802 box-sizing: border-box;
1765 .border-radius(@border-radius);
1803 .border-radius(@border-radius);
1766
1804
1767 .help-block {
1805 .help-block {
1768 margin: @padding 0;
1806 margin: @padding 0;
1769 color: @text-color;
1807 color: @text-color;
1770 }
1808 }
1771
1809
1772 .empty_data {
1810 .empty_data {
1773 margin: @padding 0;
1811 margin: @padding 0;
1774 }
1812 }
1775
1813
1776 .alert {
1814 .alert {
1777 margin-bottom: @space;
1815 margin-bottom: @space;
1778 }
1816 }
1779 }
1817 }
1780
1818
1781 .table_disp {
1819 .table_disp {
1782 .status {
1820 .status {
1783 width: auto;
1821 width: auto;
1784
1822
1785 .flag_status {
1823 .flag_status {
1786 float: left;
1824 float: left;
1787 }
1825 }
1788 }
1826 }
1789 }
1827 }
1790
1828
1791 .status_box_menu {
1829 .status_box_menu {
1792 margin: 0;
1830 margin: 0;
1793 }
1831 }
1794
1832
1795 .notification-table{
1833 .notification-table{
1796 margin-bottom: @space;
1834 margin-bottom: @space;
1797 display: table;
1835 display: table;
1798 width: 100%;
1836 width: 100%;
1799
1837
1800 .container{
1838 .container{
1801 display: table-row;
1839 display: table-row;
1802
1840
1803 .notification-header{
1841 .notification-header{
1804 border-bottom: @border-thickness solid @border-default-color;
1842 border-bottom: @border-thickness solid @border-default-color;
1805 }
1843 }
1806
1844
1807 .notification-subject{
1845 .notification-subject{
1808 display: table-cell;
1846 display: table-cell;
1809 }
1847 }
1810 }
1848 }
1811 }
1849 }
1812
1850
1813 // Notifications
1851 // Notifications
1814 .notification-header{
1852 .notification-header{
1815 display: table;
1853 display: table;
1816 width: 100%;
1854 width: 100%;
1817 padding: floor(@basefontsize/2) 0;
1855 padding: floor(@basefontsize/2) 0;
1818 line-height: 1em;
1856 line-height: 1em;
1819
1857
1820 .desc, .delete-notifications, .read-notifications{
1858 .desc, .delete-notifications, .read-notifications{
1821 display: table-cell;
1859 display: table-cell;
1822 text-align: left;
1860 text-align: left;
1823 }
1861 }
1824
1862
1825 .desc{
1863 .desc{
1826 width: 1163px;
1864 width: 1163px;
1827 }
1865 }
1828
1866
1829 .delete-notifications, .read-notifications{
1867 .delete-notifications, .read-notifications{
1830 width: 35px;
1868 width: 35px;
1831 min-width: 35px; //fixes when only one button is displayed
1869 min-width: 35px; //fixes when only one button is displayed
1832 }
1870 }
1833 }
1871 }
1834
1872
1835 .notification-body {
1873 .notification-body {
1836 .markdown-block,
1874 .markdown-block,
1837 .rst-block {
1875 .rst-block {
1838 padding: @padding 0;
1876 padding: @padding 0;
1839 }
1877 }
1840
1878
1841 .notification-subject {
1879 .notification-subject {
1842 padding: @textmargin 0;
1880 padding: @textmargin 0;
1843 border-bottom: @border-thickness solid @border-default-color;
1881 border-bottom: @border-thickness solid @border-default-color;
1844 }
1882 }
1845 }
1883 }
1846
1884
1847
1885
1848 .notifications_buttons{
1886 .notifications_buttons{
1849 float: right;
1887 float: right;
1850 }
1888 }
1851
1889
1852 #notification-status{
1890 #notification-status{
1853 display: inline;
1891 display: inline;
1854 }
1892 }
1855
1893
1856 // Repositories
1894 // Repositories
1857
1895
1858 #summary.fields{
1896 #summary.fields{
1859 display: table;
1897 display: table;
1860
1898
1861 .field{
1899 .field{
1862 display: table-row;
1900 display: table-row;
1863
1901
1864 .label-summary{
1902 .label-summary{
1865 display: table-cell;
1903 display: table-cell;
1866 min-width: @label-summary-minwidth;
1904 min-width: @label-summary-minwidth;
1867 padding-top: @padding/2;
1905 padding-top: @padding/2;
1868 padding-bottom: @padding/2;
1906 padding-bottom: @padding/2;
1869 padding-right: @padding/2;
1907 padding-right: @padding/2;
1870 }
1908 }
1871
1909
1872 .input{
1910 .input{
1873 display: table-cell;
1911 display: table-cell;
1874 padding: @padding/2;
1912 padding: @padding/2;
1875
1913
1876 input{
1914 input{
1877 min-width: 29em;
1915 min-width: 29em;
1878 padding: @padding/4;
1916 padding: @padding/4;
1879 }
1917 }
1880 }
1918 }
1881 .statistics, .downloads{
1919 .statistics, .downloads{
1882 .disabled{
1920 .disabled{
1883 color: @grey4;
1921 color: @grey4;
1884 }
1922 }
1885 }
1923 }
1886 }
1924 }
1887 }
1925 }
1888
1926
1889 #summary{
1927 #summary{
1890 width: 70%;
1928 width: 70%;
1891 }
1929 }
1892
1930
1893
1931
1894 // Journal
1932 // Journal
1895 .journal.title {
1933 .journal.title {
1896 h5 {
1934 h5 {
1897 float: left;
1935 float: left;
1898 margin: 0;
1936 margin: 0;
1899 width: 70%;
1937 width: 70%;
1900 }
1938 }
1901
1939
1902 ul {
1940 ul {
1903 float: right;
1941 float: right;
1904 display: inline-block;
1942 display: inline-block;
1905 margin: 0;
1943 margin: 0;
1906 width: 30%;
1944 width: 30%;
1907 text-align: right;
1945 text-align: right;
1908
1946
1909 li {
1947 li {
1910 display: inline;
1948 display: inline;
1911 font-size: @journal-fontsize;
1949 font-size: @journal-fontsize;
1912 line-height: 1em;
1950 line-height: 1em;
1913
1951
1914 &:before { content: none; }
1952 &:before { content: none; }
1915 }
1953 }
1916 }
1954 }
1917 }
1955 }
1918
1956
1919 .filterexample {
1957 .filterexample {
1920 position: absolute;
1958 position: absolute;
1921 top: 95px;
1959 top: 95px;
1922 left: @contentpadding;
1960 left: @contentpadding;
1923 color: @rcblue;
1961 color: @rcblue;
1924 font-size: 11px;
1962 font-size: 11px;
1925 font-family: @text-regular;
1963 font-family: @text-regular;
1926 cursor: help;
1964 cursor: help;
1927
1965
1928 &:hover {
1966 &:hover {
1929 color: @rcdarkblue;
1967 color: @rcdarkblue;
1930 }
1968 }
1931
1969
1932 @media (max-width:768px) {
1970 @media (max-width:768px) {
1933 position: relative;
1971 position: relative;
1934 top: auto;
1972 top: auto;
1935 left: auto;
1973 left: auto;
1936 display: block;
1974 display: block;
1937 }
1975 }
1938 }
1976 }
1939
1977
1940
1978
1941 #journal{
1979 #journal{
1942 margin-bottom: @space;
1980 margin-bottom: @space;
1943
1981
1944 .journal_day{
1982 .journal_day{
1945 margin-bottom: @textmargin/2;
1983 margin-bottom: @textmargin/2;
1946 padding-bottom: @textmargin/2;
1984 padding-bottom: @textmargin/2;
1947 font-size: @journal-fontsize;
1985 font-size: @journal-fontsize;
1948 border-bottom: @border-thickness solid @border-default-color;
1986 border-bottom: @border-thickness solid @border-default-color;
1949 }
1987 }
1950
1988
1951 .journal_container{
1989 .journal_container{
1952 margin-bottom: @space;
1990 margin-bottom: @space;
1953
1991
1954 .journal_user{
1992 .journal_user{
1955 display: inline-block;
1993 display: inline-block;
1956 }
1994 }
1957 .journal_action_container{
1995 .journal_action_container{
1958 display: block;
1996 display: block;
1959 margin-top: @textmargin;
1997 margin-top: @textmargin;
1960
1998
1961 div{
1999 div{
1962 display: inline;
2000 display: inline;
1963 }
2001 }
1964
2002
1965 div.journal_action_params{
2003 div.journal_action_params{
1966 display: block;
2004 display: block;
1967 }
2005 }
1968
2006
1969 div.journal_repo:after{
2007 div.journal_repo:after{
1970 content: "\A";
2008 content: "\A";
1971 white-space: pre;
2009 white-space: pre;
1972 }
2010 }
1973
2011
1974 div.date{
2012 div.date{
1975 display: block;
2013 display: block;
1976 margin-bottom: @textmargin;
2014 margin-bottom: @textmargin;
1977 }
2015 }
1978 }
2016 }
1979 }
2017 }
1980 }
2018 }
1981
2019
1982 // Files
2020 // Files
1983 .edit-file-title {
2021 .edit-file-title {
1984 border-bottom: @border-thickness solid @border-default-color;
2022 border-bottom: @border-thickness solid @border-default-color;
1985
2023
1986 .breadcrumbs {
2024 .breadcrumbs {
1987 margin-bottom: 0;
2025 margin-bottom: 0;
1988 }
2026 }
1989 }
2027 }
1990
2028
1991 .edit-file-fieldset {
2029 .edit-file-fieldset {
1992 margin-top: @sidebarpadding;
2030 margin-top: @sidebarpadding;
1993
2031
1994 .fieldset {
2032 .fieldset {
1995 .left-label {
2033 .left-label {
1996 width: 13%;
2034 width: 13%;
1997 }
2035 }
1998 .right-content {
2036 .right-content {
1999 width: 87%;
2037 width: 87%;
2000 max-width: 100%;
2038 max-width: 100%;
2001 }
2039 }
2002 .filename-label {
2040 .filename-label {
2003 margin-top: 13px;
2041 margin-top: 13px;
2004 }
2042 }
2005 .commit-message-label {
2043 .commit-message-label {
2006 margin-top: 4px;
2044 margin-top: 4px;
2007 }
2045 }
2008 .file-upload-input {
2046 .file-upload-input {
2009 input {
2047 input {
2010 display: none;
2048 display: none;
2011 }
2049 }
2012 margin-top: 10px;
2050 margin-top: 10px;
2013 }
2051 }
2014 .file-upload-label {
2052 .file-upload-label {
2015 margin-top: 10px;
2053 margin-top: 10px;
2016 }
2054 }
2017 p {
2055 p {
2018 margin-top: 5px;
2056 margin-top: 5px;
2019 }
2057 }
2020
2058
2021 }
2059 }
2022 .custom-path-link {
2060 .custom-path-link {
2023 margin-left: 5px;
2061 margin-left: 5px;
2024 }
2062 }
2025 #commit {
2063 #commit {
2026 resize: vertical;
2064 resize: vertical;
2027 }
2065 }
2028 }
2066 }
2029
2067
2030 .delete-file-preview {
2068 .delete-file-preview {
2031 max-height: 250px;
2069 max-height: 250px;
2032 }
2070 }
2033
2071
2034 .new-file,
2072 .new-file,
2035 #filter_activate,
2073 #filter_activate,
2036 #filter_deactivate {
2074 #filter_deactivate {
2037 float: left;
2075 float: left;
2038 margin: 0 0 0 15px;
2076 margin: 0 0 0 15px;
2039 }
2077 }
2040
2078
2041 h3.files_location{
2079 h3.files_location{
2042 line-height: 2.4em;
2080 line-height: 2.4em;
2043 }
2081 }
2044
2082
2045 .browser-nav {
2083 .browser-nav {
2046 display: table;
2084 display: table;
2047 margin-bottom: @space;
2085 margin-bottom: @space;
2048
2086
2049
2087
2050 .info_box {
2088 .info_box {
2051 display: inline-table;
2089 display: inline-table;
2052 height: 2.5em;
2090 height: 2.5em;
2053
2091
2054 .browser-cur-rev, .info_box_elem {
2092 .browser-cur-rev, .info_box_elem {
2055 display: table-cell;
2093 display: table-cell;
2056 vertical-align: middle;
2094 vertical-align: middle;
2057 }
2095 }
2058
2096
2059 .info_box_elem {
2097 .info_box_elem {
2060 border-top: @border-thickness solid @rcblue;
2098 border-top: @border-thickness solid @rcblue;
2061 border-bottom: @border-thickness solid @rcblue;
2099 border-bottom: @border-thickness solid @rcblue;
2062
2100
2063 #at_rev, a {
2101 #at_rev, a {
2064 padding: 0.6em 0.9em;
2102 padding: 0.6em 0.9em;
2065 margin: 0;
2103 margin: 0;
2066 .box-shadow(none);
2104 .box-shadow(none);
2067 border: 0;
2105 border: 0;
2068 height: 12px;
2106 height: 12px;
2069 }
2107 }
2070
2108
2071 input#at_rev {
2109 input#at_rev {
2072 max-width: 50px;
2110 max-width: 50px;
2073 text-align: right;
2111 text-align: right;
2074 }
2112 }
2075
2113
2076 &.previous {
2114 &.previous {
2077 border: @border-thickness solid @rcblue;
2115 border: @border-thickness solid @rcblue;
2078 .disabled {
2116 .disabled {
2079 color: @grey4;
2117 color: @grey4;
2080 cursor: not-allowed;
2118 cursor: not-allowed;
2081 }
2119 }
2082 }
2120 }
2083
2121
2084 &.next {
2122 &.next {
2085 border: @border-thickness solid @rcblue;
2123 border: @border-thickness solid @rcblue;
2086 .disabled {
2124 .disabled {
2087 color: @grey4;
2125 color: @grey4;
2088 cursor: not-allowed;
2126 cursor: not-allowed;
2089 }
2127 }
2090 }
2128 }
2091 }
2129 }
2092
2130
2093 .browser-cur-rev {
2131 .browser-cur-rev {
2094
2132
2095 span{
2133 span{
2096 margin: 0;
2134 margin: 0;
2097 color: @rcblue;
2135 color: @rcblue;
2098 height: 12px;
2136 height: 12px;
2099 display: inline-block;
2137 display: inline-block;
2100 padding: 0.7em 1em ;
2138 padding: 0.7em 1em ;
2101 border: @border-thickness solid @rcblue;
2139 border: @border-thickness solid @rcblue;
2102 margin-right: @padding;
2140 margin-right: @padding;
2103 }
2141 }
2104 }
2142 }
2105 }
2143 }
2106
2144
2107 .search_activate {
2145 .search_activate {
2108 display: table-cell;
2146 display: table-cell;
2109 vertical-align: middle;
2147 vertical-align: middle;
2110
2148
2111 input, label{
2149 input, label{
2112 margin: 0;
2150 margin: 0;
2113 padding: 0;
2151 padding: 0;
2114 }
2152 }
2115
2153
2116 input{
2154 input{
2117 margin-left: @textmargin;
2155 margin-left: @textmargin;
2118 }
2156 }
2119
2157
2120 }
2158 }
2121 }
2159 }
2122
2160
2123 .browser-cur-rev{
2161 .browser-cur-rev{
2124 margin-bottom: @textmargin;
2162 margin-bottom: @textmargin;
2125 }
2163 }
2126
2164
2127 #node_filter_box_loading{
2165 #node_filter_box_loading{
2128 .info_text;
2166 .info_text;
2129 }
2167 }
2130
2168
2131 .browser-search {
2169 .browser-search {
2132 margin: -25px 0px 5px 0px;
2170 margin: -25px 0px 5px 0px;
2133 }
2171 }
2134
2172
2135 .node-filter {
2173 .node-filter {
2136 font-size: @repo-title-fontsize;
2174 font-size: @repo-title-fontsize;
2137 padding: 4px 0px 0px 0px;
2175 padding: 4px 0px 0px 0px;
2138
2176
2139 .node-filter-path {
2177 .node-filter-path {
2140 float: left;
2178 float: left;
2141 color: @grey4;
2179 color: @grey4;
2142 }
2180 }
2143 .node-filter-input {
2181 .node-filter-input {
2144 float: left;
2182 float: left;
2145 margin: -2px 0px 0px 2px;
2183 margin: -2px 0px 0px 2px;
2146 input {
2184 input {
2147 padding: 2px;
2185 padding: 2px;
2148 border: none;
2186 border: none;
2149 font-size: @repo-title-fontsize;
2187 font-size: @repo-title-fontsize;
2150 }
2188 }
2151 }
2189 }
2152 }
2190 }
2153
2191
2154
2192
2155 .browser-result{
2193 .browser-result{
2156 td a{
2194 td a{
2157 margin-left: 0.5em;
2195 margin-left: 0.5em;
2158 display: inline-block;
2196 display: inline-block;
2159
2197
2160 em{
2198 em{
2161 font-family: @text-bold;
2199 font-family: @text-bold;
2162 }
2200 }
2163 }
2201 }
2164 }
2202 }
2165
2203
2166 .browser-highlight{
2204 .browser-highlight{
2167 background-color: @grey5-alpha;
2205 background-color: @grey5-alpha;
2168 }
2206 }
2169
2207
2170
2208
2171 // Search
2209 // Search
2172
2210
2173 .search-form{
2211 .search-form{
2174 #q {
2212 #q {
2175 width: @search-form-width;
2213 width: @search-form-width;
2176 }
2214 }
2177 .fields{
2215 .fields{
2178 margin: 0 0 @space;
2216 margin: 0 0 @space;
2179 }
2217 }
2180
2218
2181 label{
2219 label{
2182 display: inline-block;
2220 display: inline-block;
2183 margin-right: @textmargin;
2221 margin-right: @textmargin;
2184 padding-top: 0.25em;
2222 padding-top: 0.25em;
2185 }
2223 }
2186
2224
2187
2225
2188 .results{
2226 .results{
2189 clear: both;
2227 clear: both;
2190 margin: 0 0 @padding;
2228 margin: 0 0 @padding;
2191 }
2229 }
2192 }
2230 }
2193
2231
2194 div.search-feedback-items {
2232 div.search-feedback-items {
2195 display: inline-block;
2233 display: inline-block;
2196 padding:0px 0px 0px 96px;
2234 padding:0px 0px 0px 96px;
2197 }
2235 }
2198
2236
2199 div.search-code-body {
2237 div.search-code-body {
2200 background-color: #ffffff; padding: 5px 0 5px 10px;
2238 background-color: #ffffff; padding: 5px 0 5px 10px;
2201 pre {
2239 pre {
2202 .match { background-color: #faffa6;}
2240 .match { background-color: #faffa6;}
2203 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2241 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2204 }
2242 }
2205 }
2243 }
2206
2244
2207 .expand_commit.search {
2245 .expand_commit.search {
2208 .show_more.open {
2246 .show_more.open {
2209 height: auto;
2247 height: auto;
2210 max-height: none;
2248 max-height: none;
2211 }
2249 }
2212 }
2250 }
2213
2251
2214 .search-results {
2252 .search-results {
2215
2253
2216 h2 {
2254 h2 {
2217 margin-bottom: 0;
2255 margin-bottom: 0;
2218 }
2256 }
2219 .codeblock {
2257 .codeblock {
2220 border: none;
2258 border: none;
2221 background: transparent;
2259 background: transparent;
2222 }
2260 }
2223
2261
2224 .codeblock-header {
2262 .codeblock-header {
2225 border: none;
2263 border: none;
2226 background: transparent;
2264 background: transparent;
2227 }
2265 }
2228
2266
2229 .code-body {
2267 .code-body {
2230 border: @border-thickness solid @border-default-color;
2268 border: @border-thickness solid @border-default-color;
2231 .border-radius(@border-radius);
2269 .border-radius(@border-radius);
2232 }
2270 }
2233
2271
2234 .td-commit {
2272 .td-commit {
2235 &:extend(pre);
2273 &:extend(pre);
2236 border-bottom: @border-thickness solid @border-default-color;
2274 border-bottom: @border-thickness solid @border-default-color;
2237 }
2275 }
2238
2276
2239 .message {
2277 .message {
2240 height: auto;
2278 height: auto;
2241 max-width: 350px;
2279 max-width: 350px;
2242 white-space: normal;
2280 white-space: normal;
2243 text-overflow: initial;
2281 text-overflow: initial;
2244 overflow: visible;
2282 overflow: visible;
2245
2283
2246 .match { background-color: #faffa6;}
2284 .match { background-color: #faffa6;}
2247 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2285 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2248 }
2286 }
2249
2287
2250 }
2288 }
2251
2289
2252 table.rctable td.td-search-results div {
2290 table.rctable td.td-search-results div {
2253 max-width: 100%;
2291 max-width: 100%;
2254 }
2292 }
2255
2293
2256 #tip-box, .tip-box{
2294 #tip-box, .tip-box{
2257 padding: @menupadding/2;
2295 padding: @menupadding/2;
2258 display: block;
2296 display: block;
2259 border: @border-thickness solid @border-highlight-color;
2297 border: @border-thickness solid @border-highlight-color;
2260 .border-radius(@border-radius);
2298 .border-radius(@border-radius);
2261 background-color: white;
2299 background-color: white;
2262 z-index: 99;
2300 z-index: 99;
2263 white-space: pre-wrap;
2301 white-space: pre-wrap;
2264 }
2302 }
2265
2303
2266 #linktt {
2304 #linktt {
2267 width: 79px;
2305 width: 79px;
2268 }
2306 }
2269
2307
2270 #help_kb .modal-content{
2308 #help_kb .modal-content{
2271 max-width: 750px;
2309 max-width: 750px;
2272 margin: 10% auto;
2310 margin: 10% auto;
2273
2311
2274 table{
2312 table{
2275 td,th{
2313 td,th{
2276 border-bottom: none;
2314 border-bottom: none;
2277 line-height: 2.5em;
2315 line-height: 2.5em;
2278 }
2316 }
2279 th{
2317 th{
2280 padding-bottom: @textmargin/2;
2318 padding-bottom: @textmargin/2;
2281 }
2319 }
2282 td.keys{
2320 td.keys{
2283 text-align: center;
2321 text-align: center;
2284 }
2322 }
2285 }
2323 }
2286
2324
2287 .block-left{
2325 .block-left{
2288 width: 45%;
2326 width: 45%;
2289 margin-right: 5%;
2327 margin-right: 5%;
2290 }
2328 }
2291 .modal-footer{
2329 .modal-footer{
2292 clear: both;
2330 clear: both;
2293 }
2331 }
2294 .key.tag{
2332 .key.tag{
2295 padding: 0.5em;
2333 padding: 0.5em;
2296 background-color: @rcblue;
2334 background-color: @rcblue;
2297 color: white;
2335 color: white;
2298 border-color: @rcblue;
2336 border-color: @rcblue;
2299 .box-shadow(none);
2337 .box-shadow(none);
2300 }
2338 }
2301 }
2339 }
2302
2340
2303
2341
2304
2342
2305 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2343 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2306
2344
2307 @import 'statistics-graph';
2345 @import 'statistics-graph';
2308 @import 'tables';
2346 @import 'tables';
2309 @import 'forms';
2347 @import 'forms';
2310 @import 'diff';
2348 @import 'diff';
2311 @import 'summary';
2349 @import 'summary';
2312 @import 'navigation';
2350 @import 'navigation';
2313
2351
2314 //--- SHOW/HIDE SECTIONS --//
2352 //--- SHOW/HIDE SECTIONS --//
2315
2353
2316 .btn-collapse {
2354 .btn-collapse {
2317 float: right;
2355 float: right;
2318 text-align: right;
2356 text-align: right;
2319 font-family: @text-light;
2357 font-family: @text-light;
2320 font-size: @basefontsize;
2358 font-size: @basefontsize;
2321 cursor: pointer;
2359 cursor: pointer;
2322 border: none;
2360 border: none;
2323 color: @rcblue;
2361 color: @rcblue;
2324 }
2362 }
2325
2363
2326 table.rctable,
2364 table.rctable,
2327 table.dataTable {
2365 table.dataTable {
2328 .btn-collapse {
2366 .btn-collapse {
2329 float: right;
2367 float: right;
2330 text-align: right;
2368 text-align: right;
2331 }
2369 }
2332 }
2370 }
2333
2371
2334
2372
2335 // TODO: johbo: Fix for IE10, this avoids that we see a border
2373 // TODO: johbo: Fix for IE10, this avoids that we see a border
2336 // and padding around checkboxes and radio boxes. Move to the right place,
2374 // and padding around checkboxes and radio boxes. Move to the right place,
2337 // or better: Remove this once we did the form refactoring.
2375 // or better: Remove this once we did the form refactoring.
2338 input[type=checkbox],
2376 input[type=checkbox],
2339 input[type=radio] {
2377 input[type=radio] {
2340 padding: 0;
2378 padding: 0;
2341 border: none;
2379 border: none;
2342 }
2380 }
2343
2381
2344 .toggle-ajax-spinner{
2382 .toggle-ajax-spinner{
2345 height: 16px;
2383 height: 16px;
2346 width: 16px;
2384 width: 16px;
2347 }
2385 }
@@ -1,355 +1,600 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
20 var prButtonLockChecks = {
21 'compare': false,
22 'reviewers': false
23 };
24
19 /**
25 /**
20 * Pull request reviewers
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
28 * @param lockEnabled
29 * @param msg
30 * @param scope
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
45 }
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
48 }
49
50 if (msg) {
51 $('#pr_open_message').html(msg);
52 }
53 };
54
55
56 /**
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
61 - commitN
62 - commitN+1
63 ...
64
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
67
68 * @param sourceRef
69 * @param elements
70 * @param limit
71 * @returns {*[]}
21 */
72 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
75 var desc = '';
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
85 else {
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
89
90 return [title, desc]
91 };
92
93
94
95 ReviewersController = function () {
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
102
103 this.defaultForbidReviewUsers = function() {
104 return [
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
107 ];
108 };
109
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
112 };
113
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
116 };
117
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
121 };
122
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
127 // reset state of review rules
128 self.$rulesList.html('');
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
135 );
136 return self.forbidReviewUsers
137 }
138
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
144 )
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
150
151 } else {
152 self.$rulesList.append(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
156 }
157 }
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
162 )
163 }
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
169 )
170 }
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
177 }
178 return self.forbidReviewUsers
179 };
180
181 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182
183 if (self.currentRequest) {
184 // make sure we cleanup old running requests before triggering this
185 // again
186 self.currentRequest.abort();
187 }
188
189 $('.calculate-reviewers').show();
190 // reset reviewer members
191 self.$reviewMembers.empty();
192
193 prButtonLock(true, null, 'reviewers');
194 $('#user').hide(); // hide user autocomplete before load
195
196 var url = pyroutes.url('repo_default_reviewers_data',
197 {
198 'repo_name': templateContext.repo_name,
199 'source_repo': sourceRepo,
200 'source_ref': sourceRef[2],
201 'target_repo': targetRepo,
202 'target_ref': targetRef[2]
203 });
204
205 self.currentRequest = $.get(url)
206 .done(function(data) {
207 self.currentRequest = null;
208
209 // review rules
210 self.loadReviewRules(data);
211
212 for (var i = 0; i < data.reviewers.length; i++) {
213 var reviewer = data.reviewers[i];
214 self.addReviewMember(
215 reviewer.user_id, reviewer.firstname,
216 reviewer.lastname, reviewer.username,
217 reviewer.gravatar_link, reviewer.reasons,
218 reviewer.mandatory);
219 }
220 $('.calculate-reviewers').hide();
221 prButtonLock(false, null, 'reviewers');
222 $('#user').show(); // show user autocomplete after load
223 });
224 };
225
226 // check those, refactor
227 this.removeReviewMember = function(reviewer_id, mark_delete) {
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24
229
25 if(typeof(mark_delete) === undefined){
230 if(typeof(mark_delete) === undefined){
26 mark_delete = false;
231 mark_delete = false;
27 }
232 }
28
233
29 if(mark_delete === true){
234 if(mark_delete === true){
30 if (reviewer){
235 if (reviewer){
31 // now delete the input
236 // now delete the input
32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
237 $('#reviewer_{0} input'.format(reviewer_id)).remove();
33 // mark as to-delete
238 // mark as to-delete
34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
239 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
35 obj.addClass('to-delete');
240 obj.addClass('to-delete');
36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
241 obj.css({"text-decoration":"line-through", "opacity": 0.5});
37 }
242 }
38 }
243 }
39 else{
244 else{
40 $('#reviewer_{0}'.format(reviewer_id)).remove();
245 $('#reviewer_{0}'.format(reviewer_id)).remove();
41 }
246 }
42 };
247 };
43
248
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
45 var members = $('#review_members').get(0);
250 var members = self.$reviewMembers.get(0);
46 var reasons_html = '';
251 var reasons_html = '';
47 var reasons_inputs = '';
252 var reasons_inputs = '';
48 var reasons = reasons || [];
253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
255
49 if (reasons) {
256 if (reasons) {
50 for (var i = 0; i < reasons.length; i++) {
257 for (var i = 0; i < reasons.length; i++) {
51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
53 }
260 }
54 }
261 }
55 var tmpl = '<li id="reviewer_{2}">'+
262 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 '<div class="reviewer_status">'+
265 '<div class="reviewer_status">'+
58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
59 '</div>'+
267 '</div>'+
60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
268 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
61 '<span class="reviewer_name user">{1}</span>'+
269 '<span class="reviewer_name user">{1}</span>'+
62 reasons_html +
270 reasons_html +
63 '<input type="hidden" name="user_id" value="{2}">'+
271 '<input type="hidden" name="user_id" value="{2}">'+
64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 '{3}'+
273 '{3}'+
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
68 '<i class="icon-remove-sign"></i>'+
279 '<i class="icon-remove-sign"></i>'+
69 '</div>'+
280 '</div>' +
70 '</div>'+
281 '<input type="hidden" name="mandatory" value="true">'+
282 '<div class="reviewer_member_mandatory">' +
283 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 '</div>';
285
286 } else {
287 tmpl += ''+
288 '<input type="hidden" name="mandatory" value="false">'+
289 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 '<i class="icon-remove-sign"></i>'+
291 '</div>';
292 }
293 // continue template
294 tmpl += ''+
71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 '</li>' ;
296 '</li>' ;
73
297
74 var displayname = "{0} ({1} {2})".format(
298 var displayname = "{0} ({1} {2})".format(
75 nname, escapeHtml(fname), escapeHtml(lname));
299 nname, escapeHtml(fname), escapeHtml(lname));
76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 // check if we don't have this ID already in
301 // check if we don't have this ID already in
78 var ids = [];
302 var ids = [];
79 var _els = $('#review_members li').toArray();
303 var _els = self.$reviewMembers.find('li').toArray();
80 for (el in _els){
304 for (el in _els){
81 ids.push(_els[el].id)
305 ids.push(_els[el].id)
82 }
306 }
83 if(ids.indexOf('reviewer_'+id) == -1){
307
308 var userAllowedReview = function(userId) {
309 var allowed = true;
310 $.each(self.forbidReviewUsers, function(index, member_data) {
311 if (parseInt(userId) === member_data['user_id']) {
312 allowed = false;
313 return false // breaks the loop
314 }
315 });
316 return allowed
317 };
318
319 var userAllowed = userAllowedReview(id);
320 if (!userAllowed){
321 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 }
323 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324
325 if(shouldAdd) {
84 // only add if it's not there
326 // only add if it's not there
85 members.innerHTML += element;
327 members.innerHTML += element;
86 }
328 }
87
329
88 };
330 };
89
331
332 this.updateReviewers = function(repo_name, pull_request_id){
333 var postData = '_method=put&' + $('#reviewers input').serialize();
334 _updatePullRequest(repo_name, pull_request_id, postData);
335 };
336
337 };
338
339
90 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
340 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
91 var url = pyroutes.url(
341 var url = pyroutes.url(
92 'pullrequest_update',
342 'pullrequest_update',
93 {"repo_name": repo_name, "pull_request_id": pull_request_id});
343 {"repo_name": repo_name, "pull_request_id": pull_request_id});
94 if (typeof postData === 'string' ) {
344 if (typeof postData === 'string' ) {
95 postData += '&csrf_token=' + CSRF_TOKEN;
345 postData += '&csrf_token=' + CSRF_TOKEN;
96 } else {
346 } else {
97 postData.csrf_token = CSRF_TOKEN;
347 postData.csrf_token = CSRF_TOKEN;
98 }
348 }
99 var success = function(o) {
349 var success = function(o) {
100 window.location.reload();
350 window.location.reload();
101 };
351 };
102 ajaxPOST(url, postData, success);
352 ajaxPOST(url, postData, success);
103 };
353 };
104
354
105 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
106 if (reviewers_ids === undefined){
107 var postData = '_method=put&' + $('#reviewers input').serialize();
108 _updatePullRequest(repo_name, pull_request_id, postData);
109 }
110 };
111
112 /**
355 /**
113 * PULL REQUEST reject & close
356 * PULL REQUEST reject & close
114 */
357 */
115 var closePullRequest = function(repo_name, pull_request_id) {
358 var closePullRequest = function(repo_name, pull_request_id) {
116 var postData = {
359 var postData = {
117 '_method': 'put',
360 '_method': 'put',
118 'close_pull_request': true};
361 'close_pull_request': true};
119 _updatePullRequest(repo_name, pull_request_id, postData);
362 _updatePullRequest(repo_name, pull_request_id, postData);
120 };
363 };
121
364
122 /**
365 /**
123 * PULL REQUEST update commits
366 * PULL REQUEST update commits
124 */
367 */
125 var updateCommits = function(repo_name, pull_request_id) {
368 var updateCommits = function(repo_name, pull_request_id) {
126 var postData = {
369 var postData = {
127 '_method': 'put',
370 '_method': 'put',
128 'update_commits': true};
371 'update_commits': true};
129 _updatePullRequest(repo_name, pull_request_id, postData);
372 _updatePullRequest(repo_name, pull_request_id, postData);
130 };
373 };
131
374
132
375
133 /**
376 /**
134 * PULL REQUEST edit info
377 * PULL REQUEST edit info
135 */
378 */
136 var editPullRequest = function(repo_name, pull_request_id, title, description) {
379 var editPullRequest = function(repo_name, pull_request_id, title, description) {
137 var url = pyroutes.url(
380 var url = pyroutes.url(
138 'pullrequest_update',
381 'pullrequest_update',
139 {"repo_name": repo_name, "pull_request_id": pull_request_id});
382 {"repo_name": repo_name, "pull_request_id": pull_request_id});
140
383
141 var postData = {
384 var postData = {
142 '_method': 'put',
385 '_method': 'put',
143 'title': title,
386 'title': title,
144 'description': description,
387 'description': description,
145 'edit_pull_request': true,
388 'edit_pull_request': true,
146 'csrf_token': CSRF_TOKEN
389 'csrf_token': CSRF_TOKEN
147 };
390 };
148 var success = function(o) {
391 var success = function(o) {
149 window.location.reload();
392 window.location.reload();
150 };
393 };
151 ajaxPOST(url, postData, success);
394 ajaxPOST(url, postData, success);
152 };
395 };
153
396
154 var initPullRequestsCodeMirror = function (textAreaId) {
397 var initPullRequestsCodeMirror = function (textAreaId) {
155 var ta = $(textAreaId).get(0);
398 var ta = $(textAreaId).get(0);
156 var initialHeight = '100px';
399 var initialHeight = '100px';
157
400
158 // default options
401 // default options
159 var codeMirrorOptions = {
402 var codeMirrorOptions = {
160 mode: "text",
403 mode: "text",
161 lineNumbers: false,
404 lineNumbers: false,
162 indentUnit: 4,
405 indentUnit: 4,
163 theme: 'rc-input'
406 theme: 'rc-input'
164 };
407 };
165
408
166 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
409 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
167 // marker for manually set description
410 // marker for manually set description
168 codeMirrorInstance._userDefinedDesc = false;
411 codeMirrorInstance._userDefinedDesc = false;
169 codeMirrorInstance.setSize(null, initialHeight);
412 codeMirrorInstance.setSize(null, initialHeight);
170 codeMirrorInstance.on("change", function(instance, changeObj) {
413 codeMirrorInstance.on("change", function(instance, changeObj) {
171 var height = initialHeight;
414 var height = initialHeight;
172 var lines = instance.lineCount();
415 var lines = instance.lineCount();
173 if (lines > 6 && lines < 20) {
416 if (lines > 6 && lines < 20) {
174 height = "auto"
417 height = "auto"
175 }
418 }
176 else if (lines >= 20) {
419 else if (lines >= 20) {
177 height = 20 * 15;
420 height = 20 * 15;
178 }
421 }
179 instance.setSize(null, height);
422 instance.setSize(null, height);
180
423
181 // detect if the change was trigger by auto desc, or user input
424 // detect if the change was trigger by auto desc, or user input
182 changeOrigin = changeObj.origin;
425 changeOrigin = changeObj.origin;
183
426
184 if (changeOrigin === "setValue") {
427 if (changeOrigin === "setValue") {
185 cmLog.debug('Change triggered by setValue');
428 cmLog.debug('Change triggered by setValue');
186 }
429 }
187 else {
430 else {
188 cmLog.debug('user triggered change !');
431 cmLog.debug('user triggered change !');
189 // set special marker to indicate user has created an input.
432 // set special marker to indicate user has created an input.
190 instance._userDefinedDesc = true;
433 instance._userDefinedDesc = true;
191 }
434 }
192
435
193 });
436 });
194
437
195 return codeMirrorInstance
438 return codeMirrorInstance
196 };
439 };
197
440
198 /**
441 /**
199 * Reviewer autocomplete
442 * Reviewer autocomplete
200 */
443 */
201 var ReviewerAutoComplete = function(inputId) {
444 var ReviewerAutoComplete = function(inputId) {
202 $(inputId).autocomplete({
445 $(inputId).autocomplete({
203 serviceUrl: pyroutes.url('user_autocomplete_data'),
446 serviceUrl: pyroutes.url('user_autocomplete_data'),
204 minChars:2,
447 minChars:2,
205 maxHeight:400,
448 maxHeight:400,
206 deferRequestBy: 300, //miliseconds
449 deferRequestBy: 300, //miliseconds
207 showNoSuggestionNotice: true,
450 showNoSuggestionNotice: true,
208 tabDisabled: true,
451 tabDisabled: true,
209 autoSelectFirst: true,
452 autoSelectFirst: true,
210 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true },
453 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
211 formatResult: autocompleteFormatResult,
454 formatResult: autocompleteFormatResult,
212 lookupFilter: autocompleteFilterResult,
455 lookupFilter: autocompleteFilterResult,
213 onSelect: function(element, data) {
456 onSelect: function(element, data) {
214
457
215 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
458 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
216 if (data.value_type == 'user_group') {
459 if (data.value_type == 'user_group') {
217 reasons.push(_gettext('member of "{0}"').format(data.value_display));
460 reasons.push(_gettext('member of "{0}"').format(data.value_display));
218
461
219 $.each(data.members, function(index, member_data) {
462 $.each(data.members, function(index, member_data) {
220 addReviewMember(member_data.id, member_data.first_name, member_data.last_name,
463 reviewersController.addReviewMember(
464 member_data.id, member_data.first_name, member_data.last_name,
221 member_data.username, member_data.icon_link, reasons);
465 member_data.username, member_data.icon_link, reasons);
222 })
466 })
223
467
224 } else {
468 } else {
225 addReviewMember(data.id, data.first_name, data.last_name,
469 reviewersController.addReviewMember(
470 data.id, data.first_name, data.last_name,
226 data.username, data.icon_link, reasons);
471 data.username, data.icon_link, reasons);
227 }
472 }
228
473
229 $(inputId).val('');
474 $(inputId).val('');
230 }
475 }
231 });
476 });
232 };
477 };
233
478
234
479
235 VersionController = function () {
480 VersionController = function () {
236 var self = this;
481 var self = this;
237 this.$verSource = $('input[name=ver_source]');
482 this.$verSource = $('input[name=ver_source]');
238 this.$verTarget = $('input[name=ver_target]');
483 this.$verTarget = $('input[name=ver_target]');
239 this.$showVersionDiff = $('#show-version-diff');
484 this.$showVersionDiff = $('#show-version-diff');
240
485
241 this.adjustRadioSelectors = function (curNode) {
486 this.adjustRadioSelectors = function (curNode) {
242 var getVal = function (item) {
487 var getVal = function (item) {
243 if (item == 'latest') {
488 if (item == 'latest') {
244 return Number.MAX_SAFE_INTEGER
489 return Number.MAX_SAFE_INTEGER
245 }
490 }
246 else {
491 else {
247 return parseInt(item)
492 return parseInt(item)
248 }
493 }
249 };
494 };
250
495
251 var curVal = getVal($(curNode).val());
496 var curVal = getVal($(curNode).val());
252 var cleared = false;
497 var cleared = false;
253
498
254 $.each(self.$verSource, function (index, value) {
499 $.each(self.$verSource, function (index, value) {
255 var elVal = getVal($(value).val());
500 var elVal = getVal($(value).val());
256
501
257 if (elVal > curVal) {
502 if (elVal > curVal) {
258 if ($(value).is(':checked')) {
503 if ($(value).is(':checked')) {
259 cleared = true;
504 cleared = true;
260 }
505 }
261 $(value).attr('disabled', 'disabled');
506 $(value).attr('disabled', 'disabled');
262 $(value).removeAttr('checked');
507 $(value).removeAttr('checked');
263 $(value).css({'opacity': 0.1});
508 $(value).css({'opacity': 0.1});
264 }
509 }
265 else {
510 else {
266 $(value).css({'opacity': 1});
511 $(value).css({'opacity': 1});
267 $(value).removeAttr('disabled');
512 $(value).removeAttr('disabled');
268 }
513 }
269 });
514 });
270
515
271 if (cleared) {
516 if (cleared) {
272 // if we unchecked an active, set the next one to same loc.
517 // if we unchecked an active, set the next one to same loc.
273 $(this.$verSource).filter('[value={0}]'.format(
518 $(this.$verSource).filter('[value={0}]'.format(
274 curVal)).attr('checked', 'checked');
519 curVal)).attr('checked', 'checked');
275 }
520 }
276
521
277 self.setLockAction(false,
522 self.setLockAction(false,
278 $(curNode).data('verPos'),
523 $(curNode).data('verPos'),
279 $(this.$verSource).filter(':checked').data('verPos')
524 $(this.$verSource).filter(':checked').data('verPos')
280 );
525 );
281 };
526 };
282
527
283
528
284 this.attachVersionListener = function () {
529 this.attachVersionListener = function () {
285 self.$verTarget.change(function (e) {
530 self.$verTarget.change(function (e) {
286 self.adjustRadioSelectors(this)
531 self.adjustRadioSelectors(this)
287 });
532 });
288 self.$verSource.change(function (e) {
533 self.$verSource.change(function (e) {
289 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
534 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
290 });
535 });
291 };
536 };
292
537
293 this.init = function () {
538 this.init = function () {
294
539
295 var curNode = self.$verTarget.filter(':checked');
540 var curNode = self.$verTarget.filter(':checked');
296 self.adjustRadioSelectors(curNode);
541 self.adjustRadioSelectors(curNode);
297 self.setLockAction(true);
542 self.setLockAction(true);
298 self.attachVersionListener();
543 self.attachVersionListener();
299
544
300 };
545 };
301
546
302 this.setLockAction = function (state, selectedVersion, otherVersion) {
547 this.setLockAction = function (state, selectedVersion, otherVersion) {
303 var $showVersionDiff = this.$showVersionDiff;
548 var $showVersionDiff = this.$showVersionDiff;
304
549
305 if (state) {
550 if (state) {
306 $showVersionDiff.attr('disabled', 'disabled');
551 $showVersionDiff.attr('disabled', 'disabled');
307 $showVersionDiff.addClass('disabled');
552 $showVersionDiff.addClass('disabled');
308 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
553 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
309 }
554 }
310 else {
555 else {
311 $showVersionDiff.removeAttr('disabled');
556 $showVersionDiff.removeAttr('disabled');
312 $showVersionDiff.removeClass('disabled');
557 $showVersionDiff.removeClass('disabled');
313
558
314 if (selectedVersion == otherVersion) {
559 if (selectedVersion == otherVersion) {
315 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
560 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
316 } else {
561 } else {
317 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
562 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
318 }
563 }
319 }
564 }
320
565
321 };
566 };
322
567
323 this.showVersionDiff = function () {
568 this.showVersionDiff = function () {
324 var target = self.$verTarget.filter(':checked');
569 var target = self.$verTarget.filter(':checked');
325 var source = self.$verSource.filter(':checked');
570 var source = self.$verSource.filter(':checked');
326
571
327 if (target.val() && source.val()) {
572 if (target.val() && source.val()) {
328 var params = {
573 var params = {
329 'pull_request_id': templateContext.pull_request_data.pull_request_id,
574 'pull_request_id': templateContext.pull_request_data.pull_request_id,
330 'repo_name': templateContext.repo_name,
575 'repo_name': templateContext.repo_name,
331 'version': target.val(),
576 'version': target.val(),
332 'from_version': source.val()
577 'from_version': source.val()
333 };
578 };
334 window.location = pyroutes.url('pullrequest_show', params)
579 window.location = pyroutes.url('pullrequest_show', params)
335 }
580 }
336
581
337 return false;
582 return false;
338 };
583 };
339
584
340 this.toggleVersionView = function (elem) {
585 this.toggleVersionView = function (elem) {
341
586
342 if (this.$showVersionDiff.is(':visible')) {
587 if (this.$showVersionDiff.is(':visible')) {
343 $('.version-pr').hide();
588 $('.version-pr').hide();
344 this.$showVersionDiff.hide();
589 this.$showVersionDiff.hide();
345 $(elem).html($(elem).data('toggleOn'))
590 $(elem).html($(elem).data('toggleOn'))
346 } else {
591 } else {
347 $('.version-pr').show();
592 $('.version-pr').show();
348 this.$showVersionDiff.show();
593 this.$showVersionDiff.show();
349 $(elem).html($(elem).data('toggleOff'))
594 $(elem).html($(elem).data('toggleOff'))
350 }
595 }
351
596
352 return false
597 return false
353 }
598 }
354
599
355 }; No newline at end of file
600 };
@@ -1,95 +1,99 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##
2 ##
3 ## See also repo_settings.html
3 ## See also repo_settings.html
4 ##
4 ##
5 <%inherit file="/base/base.mako"/>
5 <%inherit file="/base/base.mako"/>
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('%s repository settings') % c.repo_info.repo_name}
8 ${_('%s repository settings') % c.repo_info.repo_name}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${_('Settings')}
15 ${_('Settings')}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='options')}
23 ${self.repo_menu(active='options')}
24 </%def>
24 </%def>
25
25
26 <%def name="main_content()">
26 <%def name="main_content()">
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
27 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
30 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
31 % endif
28 </%def>
32 </%def>
29
33
30
34
31 <%def name="main()">
35 <%def name="main()">
32 <div class="box">
36 <div class="box">
33 <div class="title">
37 <div class="title">
34 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
35 ${self.breadcrumbs()}
39 ${self.breadcrumbs()}
36 </div>
40 </div>
37
41
38 <div class="sidebar-col-wrapper scw-small">
42 <div class="sidebar-col-wrapper scw-small">
39 <div class="sidebar">
43 <div class="sidebar">
40 <ul class="nav nav-pills nav-stacked">
44 <ul class="nav nav-pills nav-stacked">
41 <li class="${'active' if c.active=='settings' else ''}">
45 <li class="${'active' if c.active=='settings' else ''}">
42 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
46 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
43 </li>
47 </li>
44 <li class="${'active' if c.active=='permissions' else ''}">
48 <li class="${'active' if c.active=='permissions' else ''}">
45 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
49 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
46 </li>
50 </li>
47 <li class="${'active' if c.active=='advanced' else ''}">
51 <li class="${'active' if c.active=='advanced' else ''}">
48 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
52 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
49 </li>
53 </li>
50 <li class="${'active' if c.active=='vcs' else ''}">
54 <li class="${'active' if c.active=='vcs' else ''}">
51 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
55 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
52 </li>
56 </li>
53 <li class="${'active' if c.active=='fields' else ''}">
57 <li class="${'active' if c.active=='fields' else ''}">
54 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
58 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
55 </li>
59 </li>
56 <li class="${'active' if c.active=='issuetracker' else ''}">
60 <li class="${'active' if c.active=='issuetracker' else ''}">
57 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
61 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
58 </li>
62 </li>
59 <li class="${'active' if c.active=='caches' else ''}">
63 <li class="${'active' if c.active=='caches' else ''}">
60 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
64 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
61 </li>
65 </li>
62 %if c.repo_info.repo_type != 'svn':
66 %if c.repo_info.repo_type != 'svn':
63 <li class="${'active' if c.active=='remote' else ''}">
67 <li class="${'active' if c.active=='remote' else ''}">
64 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
68 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
65 </li>
69 </li>
66 %endif
70 %endif
67 <li class="${'active' if c.active=='statistics' else ''}">
71 <li class="${'active' if c.active=='statistics' else ''}">
68 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
72 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
69 </li>
73 </li>
70 <li class="${'active' if c.active=='integrations' else ''}">
74 <li class="${'active' if c.active=='integrations' else ''}">
71 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
75 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
72 </li>
76 </li>
73 %if c.repo_info.repo_type != 'svn':
77 %if c.repo_info.repo_type != 'svn':
74 <li class="${'active' if c.active=='reviewers' else ''}">
78 <li class="${'active' if c.active=='reviewers' else ''}">
75 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
79 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
76 </li>
80 </li>
77 %endif
81 %endif
78 <li class="${'active' if c.active=='maintenance' else ''}">
82 <li class="${'active' if c.active=='maintenance' else ''}">
79 <a href="${h.route_path('repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
83 <a href="${h.route_path('repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
80 </li>
84 </li>
81 <li class="${'active' if c.active=='strip' else ''}">
85 <li class="${'active' if c.active=='strip' else ''}">
82 <a href="${h.route_path('strip', repo_name=c.repo_name)}">${_('Strip')}</a>
86 <a href="${h.route_path('strip', repo_name=c.repo_name)}">${_('Strip')}</a>
83 </li>
87 </li>
84
88
85 </ul>
89 </ul>
86 </div>
90 </div>
87
91
88 <div class="main-content-full-width">
92 <div class="main-content-full-width">
89 ${self.main_content()}
93 ${self.main_content()}
90 </div>
94 </div>
91
95
92 </div>
96 </div>
93 </div>
97 </div>
94
98
95 </%def> No newline at end of file
99 </%def>
@@ -1,113 +1,114 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
7 ${h.short_id(c.ancestor)}
7 ${h.short_id(c.ancestor)}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
9 </div>
10 </div>
10 %endif
11 %endif
11
12
12 <div class="container">
13 <div class="container">
13 <input type="hidden" name="__start__" value="revisions:sequence">
14 <input type="hidden" name="__start__" value="revisions:sequence">
14 <table class="rctable compare_view_commits">
15 <table class="rctable compare_view_commits">
15 <tr>
16 <tr>
16 <th>${_('Time')}</th>
17 <th>${_('Time')}</th>
17 <th>${_('Author')}</th>
18 <th>${_('Author')}</th>
18 <th>${_('Commit')}</th>
19 <th>${_('Commit')}</th>
19 <th></th>
20 <th></th>
20 <th>${_('Description')}</th>
21 <th>${_('Description')}</th>
21 </tr>
22 </tr>
22 %for commit in c.commit_ranges:
23 %for commit in c.commit_ranges:
23 <tr id="row-${commit.raw_id}"
24 <tr id="row-${commit.raw_id}"
24 commit_id="${commit.raw_id}"
25 commit_id="${commit.raw_id}"
25 class="compare_select"
26 class="compare_select"
26 style="${'display: none' if c.collapse_all_commits else ''}"
27 style="${'display: none' if c.collapse_all_commits else ''}"
27 >
28 >
28 <td class="td-time">
29 <td class="td-time">
29 ${h.age_component(commit.date)}
30 ${h.age_component(commit.date)}
30 </td>
31 </td>
31 <td class="td-user">
32 <td class="td-user">
32 ${base.gravatar_with_user(commit.author, 16)}
33 ${base.gravatar_with_user(commit.author, 16)}
33 </td>
34 </td>
34 <td class="td-hash">
35 <td class="td-hash">
35 <code>
36 <code>
36 <a href="${h.url('changeset_home',
37 <a href="${h.url('changeset_home',
37 repo_name=c.target_repo.repo_name,
38 repo_name=c.target_repo.repo_name,
38 revision=commit.raw_id)}">
39 revision=commit.raw_id)}">
39 r${commit.revision}:${h.short_id(commit.raw_id)}
40 r${commit.revision}:${h.short_id(commit.raw_id)}
40 </a>
41 </a>
41 ${h.hidden('revisions',commit.raw_id)}
42 ${h.hidden('revisions',commit.raw_id)}
42 </code>
43 </code>
43 </td>
44 </td>
44 <td class="expand_commit"
45 <td class="expand_commit"
45 data-commit-id="${commit.raw_id}"
46 data-commit-id="${commit.raw_id}"
46 title="${_( 'Expand commit message')}"
47 title="${_( 'Expand commit message')}"
47 >
48 >
48 <div class="show_more_col">
49 <div class="show_more_col">
49 <i class="show_more"></i>
50 <i class="show_more"></i>
50 </div>
51 </div>
51 </td>
52 </td>
52 <td class="mid td-description">
53 <td class="mid td-description">
53 <div class="log-container truncate-wrap">
54 <div class="log-container truncate-wrap">
54 <div
55 <div
55 id="c-${commit.raw_id}"
56 id="c-${commit.raw_id}"
56 class="message truncate"
57 class="message truncate"
57 data-message-raw="${commit.message}"
58 data-message-raw="${commit.message}"
58 >
59 >
59 ${h.urlify_commit_message(commit.message, c.repo_name)}
60 ${h.urlify_commit_message(commit.message, c.repo_name)}
60 </div>
61 </div>
61 </div>
62 </div>
62 </td>
63 </td>
63 </tr>
64 </tr>
64 %endfor
65 %endfor
65 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
66 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
66 <td colspan="5">
67 <td colspan="5">
67 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
68 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 </td>
70 </td>
70 </tr>
71 </tr>
71 % if not c.commit_ranges:
72 % if not c.commit_ranges:
72 <tr class="compare_select">
73 <tr class="compare_select">
73 <td colspan="5">
74 <td colspan="5">
74 ${_('No commits in this compare')}
75 ${_('No commits in this compare')}
75 </td>
76 </td>
76 </tr>
77 </tr>
77 % endif
78 % endif
78 </table>
79 </table>
79 <input type="hidden" name="__end__" value="revisions:sequence">
80 <input type="hidden" name="__end__" value="revisions:sequence">
80
81
81 </div>
82 </div>
82
83
83 <script>
84 <script>
84 $('.expand_commit').on('click',function(e){
85 $('.expand_commit').on('click',function(e){
85 var target_expand = $(this);
86 var target_expand = $(this);
86 var cid = target_expand.data('commitId');
87 var cid = target_expand.data('commitId');
87
88
88 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 if (target_expand.hasClass('open')){
90 if (target_expand.hasClass('open')){
90 $('#c-'+cid).css({
91 $('#c-'+cid).css({
91 'height': '1.5em',
92 'height': '1.5em',
92 'white-space': 'nowrap',
93 'white-space': 'nowrap',
93 'text-overflow': 'ellipsis',
94 'text-overflow': 'ellipsis',
94 'overflow':'hidden'
95 'overflow':'hidden'
95 });
96 });
96 target_expand.removeClass('open');
97 target_expand.removeClass('open');
97 }
98 }
98 else {
99 else {
99 $('#c-'+cid).css({
100 $('#c-'+cid).css({
100 'height': 'auto',
101 'height': 'auto',
101 'white-space': 'pre-line',
102 'white-space': 'pre-line',
102 'text-overflow': 'initial',
103 'text-overflow': 'initial',
103 'overflow':'visible'
104 'overflow':'visible'
104 });
105 });
105 target_expand.addClass('open');
106 target_expand.addClass('open');
106 }
107 }
107 });
108 });
108
109
109 $('.compare_select').on('click',function(e){
110 $('.compare_select').on('click',function(e){
110 var cid = $(this).attr('commit_id');
111 var cid = $(this).attr('commit_id');
111 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 });
113 });
113 </script>
114 </script>
@@ -1,105 +1,105 b''
1 <div tal:define="item_tmpl item_template|field.widget.item_template;
1 <div tal:define="item_tmpl item_template|field.widget.item_template;
2 oid oid|field.oid;
2 oid oid|field.oid;
3 name name|field.name;
3 name name|field.name;
4 min_len min_len|field.widget.min_len;
4 min_len min_len|field.widget.min_len;
5 min_len min_len or 0;
5 min_len min_len or 0;
6 max_len max_len|field.widget.max_len;
6 max_len max_len|field.widget.max_len;
7 max_len max_len or 100000;
7 max_len max_len or 100000;
8 now_len len(subfields);
8 now_len len(subfields);
9 orderable orderable|field.widget.orderable;
9 orderable orderable|field.widget.orderable;
10 orderable orderable and 1 or 0;
10 orderable orderable and 1 or 0;
11 prototype field.widget.prototype(field);
11 prototype field.widget.prototype(field);
12 title title|field.title;"
12 title title|field.title;"
13 class="deform-seq"
13 class="deform-seq"
14 id="${oid}">
14 id="${oid}">
15
15
16 <style>
16 <style>
17 body.dragging, body.dragging * {
17 body.dragging, body.dragging * {
18 cursor: move !important;
18 cursor: move !important;
19 }
19 }
20
20
21 .dragged {
21 .dragged {
22 position: absolute;
22 position: absolute;
23 opacity: 0.5;
23 opacity: 0.5;
24 z-index: 2000;
24 z-index: 2000;
25 }
25 }
26 </style>
26 </style>
27
27
28 <!-- sequence -->
28 <!-- sequence -->
29 <input type="hidden" name="__start__"
29 <input type="hidden" name="__start__"
30 value="${field.name}:sequence"
30 value="${field.name}:sequence"
31 class="deform-proto"
31 class="deform-proto"
32 tal:attributes="prototype prototype"/>
32 tal:attributes="prototype prototype"/>
33
33
34 <div class="panel panel-default">
34 <div class="panel panel-default">
35 <div class="panel-heading">${title}</div>
36 <div class="panel-body">
35 <div class="panel-body">
37
36
38 <div class="deform-seq-container"
37 <div class="deform-seq-container"
39 id="${oid}-orderable">
38 id="${oid}-orderable">
40 <div tal:define="subfields [ x[1] for x in subfields ]"
39 <div tal:define="subfields [ x[1] for x in subfields ]"
41 tal:repeat="subfield subfields"
40 tal:repeat="subfield subfields"
42 tal:replace="structure subfield.render_template(item_tmpl,
41 tal:replace="structure subfield.render_template(item_tmpl,parent=field)" />
43 parent=field)" />
44 <span class="deform-insert-before"
42 <span class="deform-insert-before"
45 tal:attributes="
43 tal:attributes="
46 min_len min_len;
44 min_len min_len;
47 max_len max_len;
45 max_len max_len;
48 now_len now_len;
46 now_len now_len;
49 orderable orderable;"></span>
47 orderable orderable;">
48
49 </span>
50 </div>
50 </div>
51
51
52 <div style="clear: both"></div>
52 <div style="clear: both"></div>
53 </div>
53 </div>
54
54
55 <div class="panel-footer">
55 <div class="panel-footer">
56 <a href="#"
56 <a href="#"
57 class="btn deform-seq-add"
57 class="btn deform-seq-add"
58 id="${field.oid}-seqAdd"
58 id="${field.oid}-seqAdd"
59 onclick="javascript: return deform.appendSequenceItem(this);">
59 onclick="javascript: return deform.appendSequenceItem(this);">
60 <small id="${field.oid}-addtext">${add_subitem_text}</small>
60 <small id="${field.oid}-addtext">${add_subitem_text}</small>
61 </a>
61 </a>
62
62
63 <script type="text/javascript">
63 <script type="text/javascript">
64 deform.addCallback(
64 deform.addCallback(
65 '${field.oid}',
65 '${field.oid}',
66 function(oid) {
66 function(oid) {
67 oid_node = $('#'+ oid);
67 oid_node = $('#'+ oid);
68 deform.processSequenceButtons(oid_node, ${min_len},
68 deform.processSequenceButtons(oid_node, ${min_len},
69 ${max_len}, ${now_len},
69 ${max_len}, ${now_len},
70 ${orderable});
70 ${orderable});
71 }
71 }
72 )
72 )
73 <tal:block condition="orderable">
73 <tal:block condition="orderable">
74 $( "#${oid}-orderable" ).sortable({
74 $( "#${oid}-orderable" ).sortable({
75 handle: ".deform-order-button, .panel-heading",
75 handle: ".deform-order-button, .panel-heading",
76 containerSelector: "#${oid}-orderable",
76 containerSelector: "#${oid}-orderable",
77 itemSelector: ".deform-seq-item",
77 itemSelector: ".deform-seq-item",
78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
79 onDragStart: function ($item, container, _super) {
79 onDragStart: function ($item, container, _super) {
80 var offset = $item.offset(),
80 var offset = $item.offset(),
81 pointer = container.rootGroup.pointer
81 pointer = container.rootGroup.pointer;
82
82
83 adjustment = {
83 adjustment = {
84 left: pointer.left - offset.left,
84 left: pointer.left - offset.left,
85 top: pointer.top - offset.top
85 top: pointer.top - offset.top
86 }
86 };
87
87
88 _super($item, container)
88 _super($item, container)
89 },
89 },
90 onDrag: function ($item, position) {
90 onDrag: function ($item, position) {
91 $item.css({
91 $item.css({
92 left: position.left - adjustment.left,
92 left: position.left - adjustment.left,
93 top: position.top - adjustment.top
93 top: position.top - adjustment.top
94 })
94 })
95 }
95 }
96 });
96 });
97 </tal:block>
97 </tal:block>
98 </script>
98 </script>
99
99
100 <input type="hidden" name="__end__" value="${field.name}:sequence"/>
100 <input type="hidden" name="__end__" value="${field.name}:sequence"/>
101 <!-- /sequence -->
101 <!-- /sequence -->
102 </div>
102 </div>
103
103
104 </div>
104 </div>
105 </div> No newline at end of file
105 </div>
@@ -1,35 +1,31 b''
1 <div tal:omit-tag="field.widget.hidden"
1 <div tal:omit-tag="field.widget.hidden"
2 tal:define="hidden hidden|field.widget.hidden;
2 tal:define="hidden hidden|field.widget.hidden;
3 error_class error_class|field.widget.error_class;
3 error_class error_class|field.widget.error_class;
4 description description|field.description;
4 description description|field.description;
5 title title|field.title;
5 title title|field.title;
6 oid oid|field.oid"
6 oid oid|field.oid"
7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
8 i18n:domain="deform">
8 i18n:domain="deform">
9
9 <div class="deform-seq-item-group">
10 <div class="deform-seq-item-group">
10 <span tal:replace="structure field.serialize(cstruct)"/>
11 <span tal:replace="structure field.serialize(cstruct)"/>
11 <tal:errors condition="field.error and not hidden"
12 <tal:errors condition="field.error and not hidden"
12 define="errstr 'error-%s' % oid"
13 define="errstr 'error-%s' % oid"
13 repeat="msg field.error.messages()">
14 repeat="msg field.error.messages()">
14 <p tal:condition="msg"
15 <p tal:condition="msg"
15 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 class="${error_class} help-block error-block"
17 class="${error_class} help-block error-block"
17 i18n:translate="">${msg}</p>
18 i18n:translate="">${msg}</p>
18 </tal:errors>
19 </tal:errors>
19 </div>
20 </div>
21
20 <div class="deform-seq-item-handle" style="padding:0">
22 <div class="deform-seq-item-handle" style="padding:0">
21 <!-- sequence_item -->
22 <span class="deform-order-button close glyphicon glyphicon-resize-vertical"
23 id="${oid}-order"
24 tal:condition="not hidden"
25 title="Reorder (via drag and drop)"
26 i18n:attributes="title"></span>
27 <a class="deform-close-button close"
23 <a class="deform-close-button close"
28 id="${oid}-close"
24 id="${oid}-close"
29 tal:condition="not field.widget.hidden"
25 tal:condition="not field.widget.hidden"
30 title="Remove"
26 title="Remove"
31 i18n:attributes="title"
27 i18n:attributes="title"
32 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
28 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
33 </div>
29 </div>
34 <!-- /sequence_item -->
30
35 </div>
31 </div>
@@ -1,591 +1,505 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('New pull request')}
8 ${_('New pull request')}
9 </%def>
9 </%def>
10
10
11 <%def name="menu_bar_nav()">
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='repositories')}
12 ${self.menu_items(active='repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_subnav()">
15 <%def name="menu_bar_subnav()">
16 ${self.repo_menu(active='showpullrequest')}
16 ${self.repo_menu(active='showpullrequest')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <div class="title">
21 <div class="title">
22 ${self.repo_page_title(c.rhodecode_db_repo)}
22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 <div class="box pr-summary">
27 <div class="box pr-summary">
28
28
29 <div class="summary-details block-left">
29 <div class="summary-details block-left">
30
30
31 <div class="form">
31 <div class="form">
32 <!-- fields -->
32 <!-- fields -->
33
33
34 <div class="fields" >
34 <div class="fields" >
35
35
36 <div class="field">
36 <div class="field">
37 <div class="label">
37 <div class="label">
38 <label for="pullrequest_title">${_('Title')}:</label>
38 <label for="pullrequest_title">${_('Title')}:</label>
39 </div>
39 </div>
40 <div class="input">
40 <div class="input">
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 </div>
42 </div>
43 </div>
43 </div>
44
44
45 <div class="field">
45 <div class="field">
46 <div class="label label-textarea">
46 <div class="label label-textarea">
47 <label for="pullrequest_desc">${_('Description')}:</label>
47 <label for="pullrequest_desc">${_('Description')}:</label>
48 </div>
48 </div>
49 <div class="textarea text-area editor">
49 <div class="textarea text-area editor">
50 ${h.textarea('pullrequest_desc',size=30, )}
50 ${h.textarea('pullrequest_desc',size=30, )}
51 <span class="help-block">${_('Write a short description on this pull request')}</span>
51 <span class="help-block">${_('Write a short description on this pull request')}</span>
52 </div>
52 </div>
53 </div>
53 </div>
54
54
55 <div class="field">
55 <div class="field">
56 <div class="label label-textarea">
56 <div class="label label-textarea">
57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
58 </div>
58 </div>
59
59
60 ## TODO: johbo: Abusing the "content" class here to get the
60 ## TODO: johbo: Abusing the "content" class here to get the
61 ## desired effect. Should be replaced by a proper solution.
61 ## desired effect. Should be replaced by a proper solution.
62
62
63 ##ORG
63 ##ORG
64 <div class="content">
64 <div class="content">
65 <strong>${_('Origin repository')}:</strong>
65 <strong>${_('Source repository')}:</strong>
66 ${c.rhodecode_db_repo.description}
66 ${c.rhodecode_db_repo.description}
67 </div>
67 </div>
68 <div class="content">
68 <div class="content">
69 ${h.hidden('source_repo')}
69 ${h.hidden('source_repo')}
70 ${h.hidden('source_ref')}
70 ${h.hidden('source_ref')}
71 </div>
71 </div>
72
72
73 ##OTHER, most Probably the PARENT OF THIS FORK
73 ##OTHER, most Probably the PARENT OF THIS FORK
74 <div class="content">
74 <div class="content">
75 ## filled with JS
75 ## filled with JS
76 <div id="target_repo_desc"></div>
76 <div id="target_repo_desc"></div>
77 </div>
77 </div>
78
78
79 <div class="content">
79 <div class="content">
80 ${h.hidden('target_repo')}
80 ${h.hidden('target_repo')}
81 ${h.hidden('target_ref')}
81 ${h.hidden('target_ref')}
82 <span id="target_ref_loading" style="display: none">
82 <span id="target_ref_loading" style="display: none">
83 ${_('Loading refs...')}
83 ${_('Loading refs...')}
84 </span>
84 </span>
85 </div>
85 </div>
86 </div>
86 </div>
87
87
88 <div class="field">
88 <div class="field">
89 <div class="label label-textarea">
89 <div class="label label-textarea">
90 <label for="pullrequest_submit"></label>
90 <label for="pullrequest_submit"></label>
91 </div>
91 </div>
92 <div class="input">
92 <div class="input">
93 <div class="pr-submit-button">
93 <div class="pr-submit-button">
94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
95 </div>
95 </div>
96 <div id="pr_open_message"></div>
96 <div id="pr_open_message"></div>
97 </div>
97 </div>
98 </div>
98 </div>
99
99
100 <div class="pr-spacing-container"></div>
100 <div class="pr-spacing-container"></div>
101 </div>
101 </div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div>
104 <div>
105 ## REIEW RULES
106 <div id="review_rules" style="display: none" class="reviewers-title block-right">
107 <div class="pr-details-title">
108 ${_('Reviewer rules')}
109 </div>
110 <div class="pr-reviewer-rules">
111 ## review rules will be appended here, by default reviewers logic
112 </div>
113 </div>
114
115 ## REVIEWERS
105 <div class="reviewers-title block-right">
116 <div class="reviewers-title block-right">
106 <div class="pr-details-title">
117 <div class="pr-details-title">
107 ${_('Pull request reviewers')}
118 ${_('Pull request reviewers')}
108 <span class="calculate-reviewers"> - ${_('loading...')}</span>
119 <span class="calculate-reviewers"> - ${_('loading...')}</span>
109 </div>
120 </div>
110 </div>
121 </div>
111 <div id="reviewers" class="block-right pr-details-content reviewers">
122 <div id="reviewers" class="block-right pr-details-content reviewers">
112 ## members goes here, filled via JS based on initial selection !
123 ## members goes here, filled via JS based on initial selection !
113 <input type="hidden" name="__start__" value="review_members:sequence">
124 <input type="hidden" name="__start__" value="review_members:sequence">
114 <ul id="review_members" class="group_members"></ul>
125 <ul id="review_members" class="group_members"></ul>
115 <input type="hidden" name="__end__" value="review_members:sequence">
126 <input type="hidden" name="__end__" value="review_members:sequence">
116 <div id="add_reviewer_input" class='ac'>
127 <div id="add_reviewer_input" class='ac'>
117 <div class="reviewer_ac">
128 <div class="reviewer_ac">
118 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
129 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
119 <div id="reviewers_container"></div>
130 <div id="reviewers_container"></div>
120 </div>
131 </div>
121 </div>
132 </div>
122 </div>
133 </div>
123 </div>
134 </div>
124 </div>
135 </div>
125 <div class="box">
136 <div class="box">
126 <div>
137 <div>
127 ## overview pulled by ajax
138 ## overview pulled by ajax
128 <div id="pull_request_overview"></div>
139 <div id="pull_request_overview"></div>
129 </div>
140 </div>
130 </div>
141 </div>
131 ${h.end_form()}
142 ${h.end_form()}
132 </div>
143 </div>
133
144
134 <script type="text/javascript">
145 <script type="text/javascript">
135 $(function(){
146 $(function(){
136 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
147 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
137 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
148 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
149 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
150 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 var targetRepoName = '${c.repo_name}';
141
151
142 var $pullRequestForm = $('#pull_request_form');
152 var $pullRequestForm = $('#pull_request_form');
143 var $sourceRepo = $('#source_repo', $pullRequestForm);
153 var $sourceRepo = $('#source_repo', $pullRequestForm);
144 var $targetRepo = $('#target_repo', $pullRequestForm);
154 var $targetRepo = $('#target_repo', $pullRequestForm);
145 var $sourceRef = $('#source_ref', $pullRequestForm);
155 var $sourceRef = $('#source_ref', $pullRequestForm);
146 var $targetRef = $('#target_ref', $pullRequestForm);
156 var $targetRef = $('#target_ref', $pullRequestForm);
147
157
158 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
159 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
160
161 var targetRepo = function() { return $targetRepo.eq(0).val() };
162 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
163
148 var calculateContainerWidth = function() {
164 var calculateContainerWidth = function() {
149 var maxWidth = 0;
165 var maxWidth = 0;
150 var repoSelect2Containers = ['#source_repo', '#target_repo'];
166 var repoSelect2Containers = ['#source_repo', '#target_repo'];
151 $.each(repoSelect2Containers, function(idx, value) {
167 $.each(repoSelect2Containers, function(idx, value) {
152 $(value).select2('container').width('auto');
168 $(value).select2('container').width('auto');
153 var curWidth = $(value).select2('container').width();
169 var curWidth = $(value).select2('container').width();
154 if (maxWidth <= curWidth) {
170 if (maxWidth <= curWidth) {
155 maxWidth = curWidth;
171 maxWidth = curWidth;
156 }
172 }
157 $.each(repoSelect2Containers, function(idx, value) {
173 $.each(repoSelect2Containers, function(idx, value) {
158 $(value).select2('container').width(maxWidth + 10);
174 $(value).select2('container').width(maxWidth + 10);
159 });
175 });
160 });
176 });
161 };
177 };
162
178
163 var initRefSelection = function(selectedRef) {
179 var initRefSelection = function(selectedRef) {
164 return function(element, callback) {
180 return function(element, callback) {
165 // translate our select2 id into a text, it's a mapping to show
181 // translate our select2 id into a text, it's a mapping to show
166 // simple label when selecting by internal ID.
182 // simple label when selecting by internal ID.
167 var id, refData;
183 var id, refData;
168 if (selectedRef === undefined) {
184 if (selectedRef === undefined) {
169 id = element.val();
185 id = element.val();
170 refData = element.val().split(':');
186 refData = element.val().split(':');
171 } else {
187 } else {
172 id = selectedRef;
188 id = selectedRef;
173 refData = selectedRef.split(':');
189 refData = selectedRef.split(':');
174 }
190 }
175
191
176 var text = refData[1];
192 var text = refData[1];
177 if (refData[0] === 'rev') {
193 if (refData[0] === 'rev') {
178 text = text.substring(0, 12);
194 text = text.substring(0, 12);
179 }
195 }
180
196
181 var data = {id: id, text: text};
197 var data = {id: id, text: text};
182
198
183 callback(data);
199 callback(data);
184 };
200 };
185 };
201 };
186
202
187 var formatRefSelection = function(item) {
203 var formatRefSelection = function(item) {
188 var prefix = '';
204 var prefix = '';
189 var refData = item.id.split(':');
205 var refData = item.id.split(':');
190 if (refData[0] === 'branch') {
206 if (refData[0] === 'branch') {
191 prefix = '<i class="icon-branch"></i>';
207 prefix = '<i class="icon-branch"></i>';
192 }
208 }
193 else if (refData[0] === 'book') {
209 else if (refData[0] === 'book') {
194 prefix = '<i class="icon-bookmark"></i>';
210 prefix = '<i class="icon-bookmark"></i>';
195 }
211 }
196 else if (refData[0] === 'tag') {
212 else if (refData[0] === 'tag') {
197 prefix = '<i class="icon-tag"></i>';
213 prefix = '<i class="icon-tag"></i>';
198 }
214 }
199
215
200 var originalOption = item.element;
216 var originalOption = item.element;
201 return prefix + item.text;
217 return prefix + item.text;
202 };
218 };
203
219
204 // custom code mirror
220 // custom code mirror
205 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
221 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206
222
223 reviewersController = new ReviewersController();
224
207 var queryTargetRepo = function(self, query) {
225 var queryTargetRepo = function(self, query) {
208 // cache ALL results if query is empty
226 // cache ALL results if query is empty
209 var cacheKey = query.term || '__';
227 var cacheKey = query.term || '__';
210 var cachedData = self.cachedDataSource[cacheKey];
228 var cachedData = self.cachedDataSource[cacheKey];
211
229
212 if (cachedData) {
230 if (cachedData) {
213 query.callback({results: cachedData.results});
231 query.callback({results: cachedData.results});
214 } else {
232 } else {
215 $.ajax({
233 $.ajax({
216 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
234 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
217 data: {query: query.term},
235 data: {query: query.term},
218 dataType: 'json',
236 dataType: 'json',
219 type: 'GET',
237 type: 'GET',
220 success: function(data) {
238 success: function(data) {
221 self.cachedDataSource[cacheKey] = data;
239 self.cachedDataSource[cacheKey] = data;
222 query.callback({results: data.results});
240 query.callback({results: data.results});
223 },
241 },
224 error: function(data, textStatus, errorThrown) {
242 error: function(data, textStatus, errorThrown) {
225 alert(
243 alert(
226 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
244 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
227 }
245 }
228 });
246 });
229 }
247 }
230 };
248 };
231
249
232 var queryTargetRefs = function(initialData, query) {
250 var queryTargetRefs = function(initialData, query) {
233 var data = {results: []};
251 var data = {results: []};
234 // filter initialData
252 // filter initialData
235 $.each(initialData, function() {
253 $.each(initialData, function() {
236 var section = this.text;
254 var section = this.text;
237 var children = [];
255 var children = [];
238 $.each(this.children, function() {
256 $.each(this.children, function() {
239 if (query.term.length === 0 ||
257 if (query.term.length === 0 ||
240 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
258 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
241 children.push({'id': this.id, 'text': this.text})
259 children.push({'id': this.id, 'text': this.text})
242 }
260 }
243 });
261 });
244 data.results.push({'text': section, 'children': children})
262 data.results.push({'text': section, 'children': children})
245 });
263 });
246 query.callback({results: data.results});
264 query.callback({results: data.results});
247 };
265 };
248
266
249
250 var prButtonLockChecks = {
251 'compare': false,
252 'reviewers': false
253 };
254
255 var prButtonLock = function(lockEnabled, msg, scope) {
256 scope = scope || 'all';
257 if (scope == 'all'){
258 prButtonLockChecks['compare'] = !lockEnabled;
259 prButtonLockChecks['reviewers'] = !lockEnabled;
260 } else if (scope == 'compare') {
261 prButtonLockChecks['compare'] = !lockEnabled;
262 } else if (scope == 'reviewers'){
263 prButtonLockChecks['reviewers'] = !lockEnabled;
264 }
265 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
266 if (lockEnabled) {
267 $('#save').attr('disabled', 'disabled');
268 }
269 else if (checksMeet) {
270 $('#save').removeAttr('disabled');
271 }
272
273 if (msg) {
274 $('#pr_open_message').html(msg);
275 }
276 };
277
278 var loadRepoRefDiffPreview = function() {
267 var loadRepoRefDiffPreview = function() {
279 var sourceRepo = $sourceRepo.eq(0).val();
280 var sourceRef = $sourceRef.eq(0).val().split(':');
281
282 var targetRepo = $targetRepo.eq(0).val();
283 var targetRef = $targetRef.eq(0).val().split(':');
284
268
285 var url_data = {
269 var url_data = {
286 'repo_name': targetRepo,
270 'repo_name': targetRepo(),
287 'target_repo': sourceRepo,
271 'target_repo': sourceRepo(),
288 'source_ref': targetRef[2],
272 'source_ref': targetRef()[2],
289 'source_ref_type': 'rev',
273 'source_ref_type': 'rev',
290 'target_ref': sourceRef[2],
274 'target_ref': sourceRef()[2],
291 'target_ref_type': 'rev',
275 'target_ref_type': 'rev',
292 'merge': true,
276 'merge': true,
293 '_': Date.now() // bypass browser caching
277 '_': Date.now() // bypass browser caching
294 }; // gather the source/target ref and repo here
278 }; // gather the source/target ref and repo here
295
279
296 if (sourceRef.length !== 3 || targetRef.length !== 3) {
280 if (sourceRef().length !== 3 || targetRef().length !== 3) {
297 prButtonLock(true, "${_('Please select origin and destination')}");
281 prButtonLock(true, "${_('Please select source and target')}");
298 return;
282 return;
299 }
283 }
300 var url = pyroutes.url('compare_url', url_data);
284 var url = pyroutes.url('compare_url', url_data);
301
285
302 // lock PR button, so we cannot send PR before it's calculated
286 // lock PR button, so we cannot send PR before it's calculated
303 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
287 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
304
288
305 if (loadRepoRefDiffPreview._currentRequest) {
289 if (loadRepoRefDiffPreview._currentRequest) {
306 loadRepoRefDiffPreview._currentRequest.abort();
290 loadRepoRefDiffPreview._currentRequest.abort();
307 }
291 }
308
292
309 loadRepoRefDiffPreview._currentRequest = $.get(url)
293 loadRepoRefDiffPreview._currentRequest = $.get(url)
310 .error(function(data, textStatus, errorThrown) {
294 .error(function(data, textStatus, errorThrown) {
311 alert(
295 alert(
312 "Error while processing request.\nError code {0} ({1}).".format(
296 "Error while processing request.\nError code {0} ({1}).".format(
313 data.status, data.statusText));
297 data.status, data.statusText));
314 })
298 })
315 .done(function(data) {
299 .done(function(data) {
316 loadRepoRefDiffPreview._currentRequest = null;
300 loadRepoRefDiffPreview._currentRequest = null;
317 $('#pull_request_overview').html(data);
301 $('#pull_request_overview').html(data);
302
318 var commitElements = $(data).find('tr[commit_id]');
303 var commitElements = $(data).find('tr[commit_id]');
319
304
320 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
305 var prTitleAndDesc = getTitleAndDescription(
321 commitElements, 5);
306 sourceRef()[1], commitElements, 5);
322
307
323 var title = prTitleAndDesc[0];
308 var title = prTitleAndDesc[0];
324 var proposedDescription = prTitleAndDesc[1];
309 var proposedDescription = prTitleAndDesc[1];
325
310
326 var useGeneratedTitle = (
311 var useGeneratedTitle = (
327 $('#pullrequest_title').hasClass('autogenerated-title') ||
312 $('#pullrequest_title').hasClass('autogenerated-title') ||
328 $('#pullrequest_title').val() === "");
313 $('#pullrequest_title').val() === "");
329
314
330 if (title && useGeneratedTitle) {
315 if (title && useGeneratedTitle) {
331 // use generated title if we haven't specified our own
316 // use generated title if we haven't specified our own
332 $('#pullrequest_title').val(title);
317 $('#pullrequest_title').val(title);
333 $('#pullrequest_title').addClass('autogenerated-title');
318 $('#pullrequest_title').addClass('autogenerated-title');
334
319
335 }
320 }
336
321
337 var useGeneratedDescription = (
322 var useGeneratedDescription = (
338 !codeMirrorInstance._userDefinedDesc ||
323 !codeMirrorInstance._userDefinedDesc ||
339 codeMirrorInstance.getValue() === "");
324 codeMirrorInstance.getValue() === "");
340
325
341 if (proposedDescription && useGeneratedDescription) {
326 if (proposedDescription && useGeneratedDescription) {
342 // set proposed content, if we haven't defined our own,
327 // set proposed content, if we haven't defined our own,
343 // or we don't have description written
328 // or we don't have description written
344 codeMirrorInstance._userDefinedDesc = false; // reset state
329 codeMirrorInstance._userDefinedDesc = false; // reset state
345 codeMirrorInstance.setValue(proposedDescription);
330 codeMirrorInstance.setValue(proposedDescription);
346 }
331 }
347
332
348 var msg = '';
333 var msg = '';
349 if (commitElements.length === 1) {
334 if (commitElements.length === 1) {
350 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
335 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
351 } else {
336 } else {
352 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
337 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
353 }
338 }
354
339
355 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
340 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
356
341
357 if (commitElements.length) {
342 if (commitElements.length) {
358 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
343 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
359 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
344 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
360 }
345 }
361 else {
346 else {
362 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
347 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
363 }
348 }
364
349
365
350
366 });
351 });
367 };
352 };
368
353
369 /**
370 Generate Title and Description for a PullRequest.
371 In case of 1 commits, the title and description is that one commit
372 in case of multiple commits, we iterate on them with max N number of commits,
373 and build description in a form
374 - commitN
375 - commitN+1
376 ...
377
378 Title is then constructed from branch names, or other references,
379 replacing '-' and '_' into spaces
380
381 * @param sourceRef
382 * @param elements
383 * @param limit
384 * @returns {*[]}
385 */
386 var getTitleAndDescription = function(sourceRef, elements, limit) {
387 var title = '';
388 var desc = '';
389
390 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
391 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
392 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
393 });
394 // only 1 commit, use commit message as title
395 if (elements.length == 1) {
396 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
397 }
398 else {
399 // use reference name
400 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
401 }
402
403 return [title, desc]
404 };
405
406 var Select2Box = function(element, overrides) {
354 var Select2Box = function(element, overrides) {
407 var globalDefaults = {
355 var globalDefaults = {
408 dropdownAutoWidth: true,
356 dropdownAutoWidth: true,
409 containerCssClass: "drop-menu",
357 containerCssClass: "drop-menu",
410 dropdownCssClass: "drop-menu-dropdown"
358 dropdownCssClass: "drop-menu-dropdown"
411 };
359 };
412
360
413 var initSelect2 = function(defaultOptions) {
361 var initSelect2 = function(defaultOptions) {
414 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
362 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
415 element.select2(options);
363 element.select2(options);
416 };
364 };
417
365
418 return {
366 return {
419 initRef: function() {
367 initRef: function() {
420 var defaultOptions = {
368 var defaultOptions = {
421 minimumResultsForSearch: 5,
369 minimumResultsForSearch: 5,
422 formatSelection: formatRefSelection
370 formatSelection: formatRefSelection
423 };
371 };
424
372
425 initSelect2(defaultOptions);
373 initSelect2(defaultOptions);
426 },
374 },
427
375
428 initRepo: function(defaultValue, readOnly) {
376 initRepo: function(defaultValue, readOnly) {
429 var defaultOptions = {
377 var defaultOptions = {
430 initSelection : function (element, callback) {
378 initSelection : function (element, callback) {
431 var data = {id: defaultValue, text: defaultValue};
379 var data = {id: defaultValue, text: defaultValue};
432 callback(data);
380 callback(data);
433 }
381 }
434 };
382 };
435
383
436 initSelect2(defaultOptions);
384 initSelect2(defaultOptions);
437
385
438 element.select2('val', defaultSourceRepo);
386 element.select2('val', defaultSourceRepo);
439 if (readOnly === true) {
387 if (readOnly === true) {
440 element.select2('readonly', true);
388 element.select2('readonly', true);
441 }
389 }
442 }
390 }
443 };
391 };
444 };
392 };
445
393
446 var initTargetRefs = function(refsData, selectedRef){
394 var initTargetRefs = function(refsData, selectedRef){
447 Select2Box($targetRef, {
395 Select2Box($targetRef, {
448 query: function(query) {
396 query: function(query) {
449 queryTargetRefs(refsData, query);
397 queryTargetRefs(refsData, query);
450 },
398 },
451 initSelection : initRefSelection(selectedRef)
399 initSelection : initRefSelection(selectedRef)
452 }).initRef();
400 }).initRef();
453
401
454 if (!(selectedRef === undefined)) {
402 if (!(selectedRef === undefined)) {
455 $targetRef.select2('val', selectedRef);
403 $targetRef.select2('val', selectedRef);
456 }
404 }
457 };
405 };
458
406
459 var targetRepoChanged = function(repoData) {
407 var targetRepoChanged = function(repoData) {
460 // generate new DESC of target repo displayed next to select
408 // generate new DESC of target repo displayed next to select
461 $('#target_repo_desc').html(
409 $('#target_repo_desc').html(
462 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
410 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
463 );
411 );
464
412
465 // generate dynamic select2 for refs.
413 // generate dynamic select2 for refs.
466 initTargetRefs(repoData['refs']['select2_refs'],
414 initTargetRefs(repoData['refs']['select2_refs'],
467 repoData['refs']['selected_ref']);
415 repoData['refs']['selected_ref']);
468
416
469 };
417 };
470
418
471 var sourceRefSelect2 = Select2Box(
419 var sourceRefSelect2 = Select2Box($sourceRef, {
472 $sourceRef, {
473 placeholder: "${_('Select commit reference')}",
420 placeholder: "${_('Select commit reference')}",
474 query: function(query) {
421 query: function(query) {
475 var initialData = defaultSourceRepoData['refs']['select2_refs'];
422 var initialData = defaultSourceRepoData['refs']['select2_refs'];
476 queryTargetRefs(initialData, query)
423 queryTargetRefs(initialData, query)
477 },
424 },
478 initSelection: initRefSelection()
425 initSelection: initRefSelection()
479 }
426 }
480 );
427 );
481
428
482 var sourceRepoSelect2 = Select2Box($sourceRepo, {
429 var sourceRepoSelect2 = Select2Box($sourceRepo, {
483 query: function(query) {}
430 query: function(query) {}
484 });
431 });
485
432
486 var targetRepoSelect2 = Select2Box($targetRepo, {
433 var targetRepoSelect2 = Select2Box($targetRepo, {
487 cachedDataSource: {},
434 cachedDataSource: {},
488 query: $.debounce(250, function(query) {
435 query: $.debounce(250, function(query) {
489 queryTargetRepo(this, query);
436 queryTargetRepo(this, query);
490 }),
437 }),
491 formatResult: formatResult
438 formatResult: formatResult
492 });
439 });
493
440
494 sourceRefSelect2.initRef();
441 sourceRefSelect2.initRef();
495
442
496 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
443 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
497
444
498 targetRepoSelect2.initRepo(defaultTargetRepo, false);
445 targetRepoSelect2.initRepo(defaultTargetRepo, false);
499
446
500 $sourceRef.on('change', function(e){
447 $sourceRef.on('change', function(e){
501 loadRepoRefDiffPreview();
448 loadRepoRefDiffPreview();
502 loadDefaultReviewers();
449 reviewersController.loadDefaultReviewers(
450 sourceRepo(), sourceRef(), targetRepo(), targetRef());
503 });
451 });
504
452
505 $targetRef.on('change', function(e){
453 $targetRef.on('change', function(e){
506 loadRepoRefDiffPreview();
454 loadRepoRefDiffPreview();
507 loadDefaultReviewers();
455 reviewersController.loadDefaultReviewers(
456 sourceRepo(), sourceRef(), targetRepo(), targetRef());
508 });
457 });
509
458
510 $targetRepo.on('change', function(e){
459 $targetRepo.on('change', function(e){
511 var repoName = $(this).val();
460 var repoName = $(this).val();
512 calculateContainerWidth();
461 calculateContainerWidth();
513 $targetRef.select2('destroy');
462 $targetRef.select2('destroy');
514 $('#target_ref_loading').show();
463 $('#target_ref_loading').show();
515
464
516 $.ajax({
465 $.ajax({
517 url: pyroutes.url('pullrequest_repo_refs',
466 url: pyroutes.url('pullrequest_repo_refs',
518 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
467 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
519 data: {},
468 data: {},
520 dataType: 'json',
469 dataType: 'json',
521 type: 'GET',
470 type: 'GET',
522 success: function(data) {
471 success: function(data) {
523 $('#target_ref_loading').hide();
472 $('#target_ref_loading').hide();
524 targetRepoChanged(data);
473 targetRepoChanged(data);
525 loadRepoRefDiffPreview();
474 loadRepoRefDiffPreview();
526 },
475 },
527 error: function(data, textStatus, errorThrown) {
476 error: function(data, textStatus, errorThrown) {
528 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
477 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
529 }
478 }
530 })
479 })
531
480
532 });
481 });
533
482
534 var loadDefaultReviewers = function() {
483 prButtonLock(true, "${_('Please select source and target')}", 'all');
535 if (loadDefaultReviewers._currentRequest) {
536 loadDefaultReviewers._currentRequest.abort();
537 }
538 $('.calculate-reviewers').show();
539 prButtonLock(true, null, 'reviewers');
540
541 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
542
543 var sourceRepo = $sourceRepo.eq(0).val();
544 var sourceRef = $sourceRef.eq(0).val().split(':');
545 var targetRepo = $targetRepo.eq(0).val();
546 var targetRef = $targetRef.eq(0).val().split(':');
547 url += '?source_repo=' + sourceRepo;
548 url += '&source_ref=' + sourceRef[2];
549 url += '&target_repo=' + targetRepo;
550 url += '&target_ref=' + targetRef[2];
551
552 loadDefaultReviewers._currentRequest = $.get(url)
553 .done(function(data) {
554 loadDefaultReviewers._currentRequest = null;
555
556 // reset && add the reviewer based on selected repo
557 $('#review_members').html('');
558 for (var i = 0; i < data.reviewers.length; i++) {
559 var reviewer = data.reviewers[i];
560 addReviewMember(
561 reviewer.user_id, reviewer.firstname,
562 reviewer.lastname, reviewer.username,
563 reviewer.gravatar_link, reviewer.reasons);
564 }
565 $('.calculate-reviewers').hide();
566 prButtonLock(false, null, 'reviewers');
567 });
568 };
569
570 prButtonLock(true, "${_('Please select origin and destination')}", 'all');
571
484
572 // auto-load on init, the target refs select2
485 // auto-load on init, the target refs select2
573 calculateContainerWidth();
486 calculateContainerWidth();
574 targetRepoChanged(defaultTargetRepoData);
487 targetRepoChanged(defaultTargetRepoData);
575
488
576 $('#pullrequest_title').on('keyup', function(e){
489 $('#pullrequest_title').on('keyup', function(e){
577 $(this).removeClass('autogenerated-title');
490 $(this).removeClass('autogenerated-title');
578 });
491 });
579
492
580 % if c.default_source_ref:
493 % if c.default_source_ref:
581 // in case we have a pre-selected value, use it now
494 // in case we have a pre-selected value, use it now
582 $sourceRef.select2('val', '${c.default_source_ref}');
495 $sourceRef.select2('val', '${c.default_source_ref}');
583 loadRepoRefDiffPreview();
496 loadRepoRefDiffPreview();
584 loadDefaultReviewers();
497 reviewersController.loadDefaultReviewers(
498 sourceRepo(), sourceRef(), targetRepo(), targetRef());
585 % endif
499 % endif
586
500
587 ReviewerAutoComplete('#user');
501 ReviewerAutoComplete('#user');
588 });
502 });
589 </script>
503 </script>
590
504
591 </%def>
505 </%def>
@@ -1,827 +1,865 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Origin')}:</label>
71 <label>${_('Source')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 <div class="pr-pullinfo">
92 <div class="pr-pullinfo">
93 %if h.is_hg(c.pull_request.source_repo):
93 %if h.is_hg(c.pull_request.source_repo):
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 %elif h.is_git(c.pull_request.source_repo):
95 %elif h.is_git(c.pull_request.source_repo):
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 %endif
97 %endif
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101 <div class="field">
101 <div class="field">
102 <div class="label-summary">
102 <div class="label-summary">
103 <label>${_('Target')}:</label>
103 <label>${_('Target')}:</label>
104 </div>
104 </div>
105 <div class="input">
105 <div class="input">
106 <div class="pr-targetinfo">
106 <div class="pr-targetinfo">
107 ## branch link is only valid if it is a branch
107 ## branch link is only valid if it is a branch
108 <span class="tag">
108 <span class="tag">
109 %if c.pull_request.target_ref_parts.type == 'branch':
109 %if c.pull_request.target_ref_parts.type == 'branch':
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 %else:
111 %else:
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 %endif
113 %endif
114 </span>
114 </span>
115 <span class="clone-url">
115 <span class="clone-url">
116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 </span>
117 </span>
118 </div>
118 </div>
119 </div>
119 </div>
120 </div>
120 </div>
121
121
122 ## Link to the shadow repository.
122 ## Link to the shadow repository.
123 <div class="field">
123 <div class="field">
124 <div class="label-summary">
124 <div class="label-summary">
125 <label>${_('Merge')}:</label>
125 <label>${_('Merge')}:</label>
126 </div>
126 </div>
127 <div class="input">
127 <div class="input">
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 <div class="pr-mergeinfo">
129 <div class="pr-mergeinfo">
130 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 %elif h.is_git(c.pull_request.target_repo):
132 %elif h.is_git(c.pull_request.target_repo):
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 %endif
134 %endif
135 </div>
135 </div>
136 % else:
136 % else:
137 <div class="">
137 <div class="">
138 ${_('Shadow repository data not available')}.
138 ${_('Shadow repository data not available')}.
139 </div>
139 </div>
140 % endif
140 % endif
141 </div>
141 </div>
142 </div>
142 </div>
143
143
144 <div class="field">
144 <div class="field">
145 <div class="label-summary">
145 <div class="label-summary">
146 <label>${_('Review')}:</label>
146 <label>${_('Review')}:</label>
147 </div>
147 </div>
148 <div class="input">
148 <div class="input">
149 %if c.pull_request_review_status:
149 %if c.pull_request_review_status:
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 <span class="changeset-status-lbl tooltip">
151 <span class="changeset-status-lbl tooltip">
152 %if c.pull_request.is_closed():
152 %if c.pull_request.is_closed():
153 ${_('Closed')},
153 ${_('Closed')},
154 %endif
154 %endif
155 ${h.commit_status_lbl(c.pull_request_review_status)}
155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 </span>
156 </span>
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 %endif
158 %endif
159 </div>
159 </div>
160 </div>
160 </div>
161 <div class="field">
161 <div class="field">
162 <div class="pr-description-label label-summary">
162 <div class="pr-description-label label-summary">
163 <label>${_('Description')}:</label>
163 <label>${_('Description')}:</label>
164 </div>
164 </div>
165 <div id="pr-desc" class="input">
165 <div id="pr-desc" class="input">
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 </div>
167 </div>
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 </div>
170 </div>
171 </div>
171 </div>
172
172
173 <div class="field">
173 <div class="field">
174 <div class="label-summary">
174 <div class="label-summary">
175 <label>${_('Versions')}:</label>
175 <label>${_('Versions')}:</label>
176 </div>
176 </div>
177
177
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180
180
181 <div class="pr-versions">
181 <div class="pr-versions">
182 % if c.show_version_changes:
182 % if c.show_version_changes:
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 data-toggle-off="${_('Hide all versions of this pull request')}">
187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 </a>
189 </a>
190 <table>
190 <table>
191 ## SHOW ALL VERSIONS OF PR
191 ## SHOW ALL VERSIONS OF PR
192 <% ver_pr = None %>
192 <% ver_pr = None %>
193
193
194 % for data in reversed(list(enumerate(c.versions, 1))):
194 % for data in reversed(list(enumerate(c.versions, 1))):
195 <% ver_pos = data[0] %>
195 <% ver_pos = data[0] %>
196 <% ver = data[1] %>
196 <% ver = data[1] %>
197 <% ver_pr = ver.pull_request_version_id %>
197 <% ver_pr = ver.pull_request_version_id %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199
199
200 <tr class="version-pr" style="display: ${display_row}">
200 <tr class="version-pr" style="display: ${display_row}">
201 <td>
201 <td>
202 <code>
202 <code>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 </code>
204 </code>
205 </td>
205 </td>
206 <td>
206 <td>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 </td>
209 </td>
210 <td>
210 <td>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 </div>
213 </div>
214 </td>
214 </td>
215 <td>
215 <td>
216 % if c.at_version_num != ver_pr:
216 % if c.at_version_num != ver_pr:
217 <i class="icon-comment"></i>
217 <i class="icon-comment"></i>
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 </code>
220 </code>
221 % endif
221 % endif
222 </td>
222 </td>
223 <td>
223 <td>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 </td>
225 </td>
226 <td>
226 <td>
227 ${h.age_component(ver.updated_on, time_is_local=True)}
227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 </td>
228 </td>
229 </tr>
229 </tr>
230 % endfor
230 % endfor
231
231
232 <tr>
232 <tr>
233 <td colspan="6">
233 <td colspan="6">
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 data-label-text-locked="${_('select versions to show changes')}"
235 data-label-text-locked="${_('select versions to show changes')}"
236 data-label-text-diff="${_('show changes between versions')}"
236 data-label-text-diff="${_('show changes between versions')}"
237 data-label-text-show="${_('show pull request for this version')}"
237 data-label-text-show="${_('show pull request for this version')}"
238 >
238 >
239 ${_('select versions to show changes')}
239 ${_('select versions to show changes')}
240 </button>
240 </button>
241 </td>
241 </td>
242 </tr>
242 </tr>
243
243
244 ## show comment/inline comments summary
244 ## show comment/inline comments summary
245 <%def name="comments_summary()">
245 <%def name="comments_summary()">
246 <tr>
246 <tr>
247 <td colspan="6" class="comments-summary-td">
247 <td colspan="6" class="comments-summary-td">
248
248
249 % if c.at_version:
249 % if c.at_version:
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 ${_('Comments at this version')}:
252 ${_('Comments at this version')}:
253 % else:
253 % else:
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 ${_('Comments for this pull request')}:
256 ${_('Comments for this pull request')}:
257 % endif
257 % endif
258
258
259
259
260 %if general_comm_count_ver:
260 %if general_comm_count_ver:
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 %else:
262 %else:
263 ${_("%d General ") % general_comm_count_ver}
263 ${_("%d General ") % general_comm_count_ver}
264 %endif
264 %endif
265
265
266 %if inline_comm_count_ver:
266 %if inline_comm_count_ver:
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 %else:
268 %else:
269 , ${_("%d Inline") % inline_comm_count_ver}
269 , ${_("%d Inline") % inline_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if outdated_comm_count_ver:
272 %if outdated_comm_count_ver:
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 %else:
276 %else:
277 , ${_("%d Outdated") % outdated_comm_count_ver}
277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 %endif
278 %endif
279 </td>
279 </td>
280 </tr>
280 </tr>
281 </%def>
281 </%def>
282 ${comments_summary()}
282 ${comments_summary()}
283 </table>
283 </table>
284 % else:
284 % else:
285 <div class="input">
285 <div class="input">
286 ${_('Pull request versions not available')}.
286 ${_('Pull request versions not available')}.
287 </div>
287 </div>
288 <div>
288 <div>
289 <table>
289 <table>
290 ${comments_summary()}
290 ${comments_summary()}
291 </table>
291 </table>
292 </div>
292 </div>
293 % endif
293 % endif
294 </div>
294 </div>
295 </div>
295 </div>
296
296
297 <div id="pr-save" class="field" style="display: none;">
297 <div id="pr-save" class="field" style="display: none;">
298 <div class="label-summary"></div>
298 <div class="label-summary"></div>
299 <div class="input">
299 <div class="input">
300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 </div>
301 </div>
302 </div>
302 </div>
303 </div>
303 </div>
304 </div>
304 </div>
305 <div>
305 <div>
306 ## AUTHOR
306 ## AUTHOR
307 <div class="reviewers-title block-right">
307 <div class="reviewers-title block-right">
308 <div class="pr-details-title">
308 <div class="pr-details-title">
309 ${_('Author')}
309 ${_('Author')}
310 </div>
310 </div>
311 </div>
311 </div>
312 <div class="block-right pr-details-content reviewers">
312 <div class="block-right pr-details-content reviewers">
313 <ul class="group_members">
313 <ul class="group_members">
314 <li>
314 <li>
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 </li>
316 </li>
317 </ul>
317 </ul>
318 </div>
318 </div>
319
320 ## REVIEW RULES
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 <div class="pr-details-title">
323 ${_('Reviewer rules')}
324 %if c.allowed_to_update:
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 %endif
327 </div>
328 <div class="pr-reviewer-rules">
329 ## review rules will be appended here, by default reviewers logic
330 </div>
331 <input id="review_data" type="hidden" name="review_data" value="">
332 </div>
333
319 ## REVIEWERS
334 ## REVIEWERS
320 <div class="reviewers-title block-right">
335 <div class="reviewers-title block-right">
321 <div class="pr-details-title">
336 <div class="pr-details-title">
322 ${_('Pull request reviewers')}
337 ${_('Pull request reviewers')}
323 %if c.allowed_to_update:
338 %if c.allowed_to_update:
324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
326 %endif
340 %endif
327 </div>
341 </div>
328 </div>
342 </div>
329 <div id="reviewers" class="block-right pr-details-content reviewers">
343 <div id="reviewers" class="block-right pr-details-content reviewers">
330 ## members goes here !
344 ## members goes here !
331 <input type="hidden" name="__start__" value="review_members:sequence">
345 <input type="hidden" name="__start__" value="review_members:sequence">
332 <ul id="review_members" class="group_members">
346 <ul id="review_members" class="group_members">
333 %for member,reasons,status in c.pull_request_reviewers:
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
334 <li id="reviewer_${member.user_id}">
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
335 <div class="reviewers_member">
349 <div class="reviewers_member">
336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
338 </div>
352 </div>
339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
340 ${self.gravatar_with_user(member.email, 16)}
354 ${self.gravatar_with_user(member.email, 16)}
341 </div>
355 </div>
342 <input type="hidden" name="__start__" value="reviewer:mapping">
356 <input type="hidden" name="__start__" value="reviewer:mapping">
343 <input type="hidden" name="__start__" value="reasons:sequence">
357 <input type="hidden" name="__start__" value="reasons:sequence">
344 %for reason in reasons:
358 %for reason in reasons:
345 <div class="reviewer_reason">- ${reason}</div>
359 <div class="reviewer_reason">- ${reason}</div>
346 <input type="hidden" name="reason" value="${reason}">
360 <input type="hidden" name="reason" value="${reason}">
347
361
348 %endfor
362 %endfor
349 <input type="hidden" name="__end__" value="reasons:sequence">
363 <input type="hidden" name="__end__" value="reasons:sequence">
350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
351 <input type="hidden" name="__end__" value="reviewer:mapping">
366 <input type="hidden" name="__end__" value="reviewer:mapping">
352 %if c.allowed_to_update:
367 % if mandatory:
353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
368 <div class="reviewer_member_mandatory_remove">
354 <i class="icon-remove-sign" ></i>
369 <i class="icon-remove-sign"></i>
355 </div>
370 </div>
371 <div class="reviewer_member_mandatory">
372 <i class="icon-lock" title="Mandatory reviewer"></i>
373 </div>
374 % else:
375 %if c.allowed_to_update:
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 <i class="icon-remove-sign" ></i>
378 </div>
379 %endif
356 %endif
380 % endif
357 </div>
381 </div>
358 </li>
382 </li>
359 %endfor
383 %endfor
360 </ul>
384 </ul>
361 <input type="hidden" name="__end__" value="review_members:sequence">
385 <input type="hidden" name="__end__" value="review_members:sequence">
386
362 %if not c.pull_request.is_closed():
387 %if not c.pull_request.is_closed():
363 <div id="add_reviewer_input" class='ac' style="display: none;">
388 <div id="add_reviewer" class="ac" style="display: none;">
364 %if c.allowed_to_update:
389 %if c.allowed_to_update:
365 <div class="reviewer_ac">
390 % if not c.forbid_adding_reviewers:
391 <div id="add_reviewer_input" class="reviewer_ac">
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
367 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
368 </div>
394 </div>
369 <div>
395 % endif
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
396 <div class="pull-right">
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
371 </div>
398 </div>
372 %endif
399 %endif
373 </div>
400 </div>
374 %endif
401 %endif
375 </div>
402 </div>
376 </div>
403 </div>
377 </div>
404 </div>
378 <div class="box">
405 <div class="box">
379 ##DIFF
406 ##DIFF
380 <div class="table" >
407 <div class="table" >
381 <div id="changeset_compare_view_content">
408 <div id="changeset_compare_view_content">
382 ##CS
409 ##CS
383 % if c.missing_requirements:
410 % if c.missing_requirements:
384 <div class="box">
411 <div class="box">
385 <div class="alert alert-warning">
412 <div class="alert alert-warning">
386 <div>
413 <div>
387 <strong>${_('Missing requirements:')}</strong>
414 <strong>${_('Missing requirements:')}</strong>
388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
389 </div>
416 </div>
390 </div>
417 </div>
391 </div>
418 </div>
392 % elif c.missing_commits:
419 % elif c.missing_commits:
393 <div class="box">
420 <div class="box">
394 <div class="alert alert-warning">
421 <div class="alert alert-warning">
395 <div>
422 <div>
396 <strong>${_('Missing commits')}:</strong>
423 <strong>${_('Missing commits')}:</strong>
397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
399 </div>
426 </div>
400 </div>
427 </div>
401 </div>
428 </div>
402 % endif
429 % endif
403
430
404 <div class="compare_view_commits_title">
431 <div class="compare_view_commits_title">
405 % if not c.compare_mode:
432 % if not c.compare_mode:
406
433
407 % if c.at_version_pos:
434 % if c.at_version_pos:
408 <h4>
435 <h4>
409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
410 </h4>
437 </h4>
411 % endif
438 % endif
412
439
413 <div class="pull-left">
440 <div class="pull-left">
414 <div class="btn-group">
441 <div class="btn-group">
415 <a
442 <a
416 class="btn"
443 class="btn"
417 href="#"
444 href="#"
418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
446 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
420 </a>
447 </a>
421 <a
448 <a
422 class="btn"
449 class="btn"
423 href="#"
450 href="#"
424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
452 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
426 </a>
453 </a>
427 </div>
454 </div>
428 </div>
455 </div>
429
456
430 <div class="pull-right">
457 <div class="pull-right">
431 % if c.allowed_to_update and not c.pull_request.is_closed():
458 % if c.allowed_to_update and not c.pull_request.is_closed():
432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
433 % else:
460 % else:
434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
435 % endif
462 % endif
436
463
437 </div>
464 </div>
438 % endif
465 % endif
439 </div>
466 </div>
440
467
441 % if not c.missing_commits:
468 % if not c.missing_commits:
442 % if c.compare_mode:
469 % if c.compare_mode:
443 % if c.at_version:
470 % if c.at_version:
444 <h4>
471 <h4>
445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
446 </h4>
473 </h4>
447
474
448 <div class="subtitle-compare">
475 <div class="subtitle-compare">
449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
450 </div>
477 </div>
451
478
452 <div class="container">
479 <div class="container">
453 <table class="rctable compare_view_commits">
480 <table class="rctable compare_view_commits">
454 <tr>
481 <tr>
455 <th></th>
482 <th></th>
456 <th>${_('Time')}</th>
483 <th>${_('Time')}</th>
457 <th>${_('Author')}</th>
484 <th>${_('Author')}</th>
458 <th>${_('Commit')}</th>
485 <th>${_('Commit')}</th>
459 <th></th>
486 <th></th>
460 <th>${_('Description')}</th>
487 <th>${_('Description')}</th>
461 </tr>
488 </tr>
462
489
463 % for c_type, commit in c.commit_changes:
490 % for c_type, commit in c.commit_changes:
464 % if c_type in ['a', 'r']:
491 % if c_type in ['a', 'r']:
465 <%
492 <%
466 if c_type == 'a':
493 if c_type == 'a':
467 cc_title = _('Commit added in displayed changes')
494 cc_title = _('Commit added in displayed changes')
468 elif c_type == 'r':
495 elif c_type == 'r':
469 cc_title = _('Commit removed in displayed changes')
496 cc_title = _('Commit removed in displayed changes')
470 else:
497 else:
471 cc_title = ''
498 cc_title = ''
472 %>
499 %>
473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
474 <td>
501 <td>
475 <div class="commit-change-indicator color-${c_type}-border">
502 <div class="commit-change-indicator color-${c_type}-border">
476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
503 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
477 ${c_type.upper()}
504 ${c_type.upper()}
478 </div>
505 </div>
479 </div>
506 </div>
480 </td>
507 </td>
481 <td class="td-time">
508 <td class="td-time">
482 ${h.age_component(commit.date)}
509 ${h.age_component(commit.date)}
483 </td>
510 </td>
484 <td class="td-user">
511 <td class="td-user">
485 ${base.gravatar_with_user(commit.author, 16)}
512 ${base.gravatar_with_user(commit.author, 16)}
486 </td>
513 </td>
487 <td class="td-hash">
514 <td class="td-hash">
488 <code>
515 <code>
489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
516 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
490 r${commit.revision}:${h.short_id(commit.raw_id)}
517 r${commit.revision}:${h.short_id(commit.raw_id)}
491 </a>
518 </a>
492 ${h.hidden('revisions', commit.raw_id)}
519 ${h.hidden('revisions', commit.raw_id)}
493 </code>
520 </code>
494 </td>
521 </td>
495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
496 <div class="show_more_col">
523 <div class="show_more_col">
497 <i class="show_more"></i>
524 <i class="show_more"></i>
498 </div>
525 </div>
499 </td>
526 </td>
500 <td class="mid td-description">
527 <td class="mid td-description">
501 <div class="log-container truncate-wrap">
528 <div class="log-container truncate-wrap">
502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
503 ${h.urlify_commit_message(commit.message, c.repo_name)}
530 ${h.urlify_commit_message(commit.message, c.repo_name)}
504 </div>
531 </div>
505 </div>
532 </div>
506 </td>
533 </td>
507 </tr>
534 </tr>
508 % endif
535 % endif
509 % endfor
536 % endfor
510 </table>
537 </table>
511 </div>
538 </div>
512
539
513 <script>
540 <script>
514 $('.expand_commit').on('click',function(e){
541 $('.expand_commit').on('click',function(e){
515 var target_expand = $(this);
542 var target_expand = $(this);
516 var cid = target_expand.data('commitId');
543 var cid = target_expand.data('commitId');
517
544
518 if (target_expand.hasClass('open')){
545 if (target_expand.hasClass('open')){
519 $('#c-'+cid).css({
546 $('#c-'+cid).css({
520 'height': '1.5em',
547 'height': '1.5em',
521 'white-space': 'nowrap',
548 'white-space': 'nowrap',
522 'text-overflow': 'ellipsis',
549 'text-overflow': 'ellipsis',
523 'overflow':'hidden'
550 'overflow':'hidden'
524 });
551 });
525 target_expand.removeClass('open');
552 target_expand.removeClass('open');
526 }
553 }
527 else {
554 else {
528 $('#c-'+cid).css({
555 $('#c-'+cid).css({
529 'height': 'auto',
556 'height': 'auto',
530 'white-space': 'pre-line',
557 'white-space': 'pre-line',
531 'text-overflow': 'initial',
558 'text-overflow': 'initial',
532 'overflow':'visible'
559 'overflow':'visible'
533 });
560 });
534 target_expand.addClass('open');
561 target_expand.addClass('open');
535 }
562 }
536 });
563 });
537 </script>
564 </script>
538
565
539 % endif
566 % endif
540
567
541 % else:
568 % else:
542 <%include file="/compare/compare_commits.mako" />
569 <%include file="/compare/compare_commits.mako" />
543 % endif
570 % endif
544
571
545 <div class="cs_files">
572 <div class="cs_files">
546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
547 ${cbdiffs.render_diffset_menu()}
574 ${cbdiffs.render_diffset_menu()}
548 ${cbdiffs.render_diffset(
575 ${cbdiffs.render_diffset(
549 c.diffset, use_comments=True,
576 c.diffset, use_comments=True,
550 collapse_when_files_over=30,
577 collapse_when_files_over=30,
551 disable_new_comments=not c.allowed_to_comment,
578 disable_new_comments=not c.allowed_to_comment,
552 deleted_files_comments=c.deleted_files_comments)}
579 deleted_files_comments=c.deleted_files_comments)}
553 </div>
580 </div>
554 % else:
581 % else:
555 ## skipping commits we need to clear the view for missing commits
582 ## skipping commits we need to clear the view for missing commits
556 <div style="clear:both;"></div>
583 <div style="clear:both;"></div>
557 % endif
584 % endif
558
585
559 </div>
586 </div>
560 </div>
587 </div>
561
588
562 ## template for inline comment form
589 ## template for inline comment form
563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
564
591
565 ## render general comments
592 ## render general comments
566
593
567 <div id="comment-tr-show">
594 <div id="comment-tr-show">
568 <div class="comment">
595 <div class="comment">
569 % if general_outdated_comm_count_ver:
596 % if general_outdated_comm_count_ver:
570 <div class="meta">
597 <div class="meta">
571 % if general_outdated_comm_count_ver == 1:
598 % if general_outdated_comm_count_ver == 1:
572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
574 % else:
601 % else:
575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
577 % endif
604 % endif
578 </div>
605 </div>
579 % endif
606 % endif
580 </div>
607 </div>
581 </div>
608 </div>
582
609
583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
584
611
585 % if not c.pull_request.is_closed():
612 % if not c.pull_request.is_closed():
586 ## merge status, and merge action
613 ## merge status, and merge action
587 <div class="pull-request-merge">
614 <div class="pull-request-merge">
588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
589 </div>
616 </div>
590
617
591 ## main comment form and it status
618 ## main comment form and it status
592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
593 pull_request_id=c.pull_request.pull_request_id),
620 pull_request_id=c.pull_request.pull_request_id),
594 c.pull_request_review_status,
621 c.pull_request_review_status,
595 is_pull_request=True, change_status=c.allowed_to_change_status)}
622 is_pull_request=True, change_status=c.allowed_to_change_status)}
596 %endif
623 %endif
597
624
598 <script type="text/javascript">
625 <script type="text/javascript">
599 if (location.hash) {
626 if (location.hash) {
600 var result = splitDelimitedHash(location.hash);
627 var result = splitDelimitedHash(location.hash);
601 var line = $('html').find(result.loc);
628 var line = $('html').find(result.loc);
602 // show hidden comments if we use location.hash
629 // show hidden comments if we use location.hash
603 if (line.hasClass('comment-general')) {
630 if (line.hasClass('comment-general')) {
604 $(line).show();
631 $(line).show();
605 } else if (line.hasClass('comment-inline')) {
632 } else if (line.hasClass('comment-inline')) {
606 $(line).show();
633 $(line).show();
607 var $cb = $(line).closest('.cb');
634 var $cb = $(line).closest('.cb');
608 $cb.removeClass('cb-collapsed')
635 $cb.removeClass('cb-collapsed')
609 }
636 }
610 if (line.length > 0){
637 if (line.length > 0){
611 offsetScroll(line, 70);
638 offsetScroll(line, 70);
612 }
639 }
613 }
640 }
614
641
615 versionController = new VersionController();
642 versionController = new VersionController();
616 versionController.init();
643 versionController.init();
617
644
645 reviewersController = new ReviewersController();
618
646
619 $(function(){
647 $(function(){
620 ReviewerAutoComplete('#user');
648
621 // custom code mirror
649 // custom code mirror
622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
623
651
624 var PRDetails = {
652 var PRDetails = {
625 editButton: $('#open_edit_pullrequest'),
653 editButton: $('#open_edit_pullrequest'),
626 closeButton: $('#close_edit_pullrequest'),
654 closeButton: $('#close_edit_pullrequest'),
627 deleteButton: $('#delete_pullrequest'),
655 deleteButton: $('#delete_pullrequest'),
628 viewFields: $('#pr-desc, #pr-title'),
656 viewFields: $('#pr-desc, #pr-title'),
629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
630
658
631 init: function() {
659 init: function() {
632 var that = this;
660 var that = this;
633 this.editButton.on('click', function(e) { that.edit(); });
661 this.editButton.on('click', function(e) { that.edit(); });
634 this.closeButton.on('click', function(e) { that.view(); });
662 this.closeButton.on('click', function(e) { that.view(); });
635 },
663 },
636
664
637 edit: function(event) {
665 edit: function(event) {
638 this.viewFields.hide();
666 this.viewFields.hide();
639 this.editButton.hide();
667 this.editButton.hide();
640 this.deleteButton.hide();
668 this.deleteButton.hide();
641 this.closeButton.show();
669 this.closeButton.show();
642 this.editFields.show();
670 this.editFields.show();
643 codeMirrorInstance.refresh();
671 codeMirrorInstance.refresh();
644 },
672 },
645
673
646 view: function(event) {
674 view: function(event) {
647 this.editButton.show();
675 this.editButton.show();
648 this.deleteButton.show();
676 this.deleteButton.show();
649 this.editFields.hide();
677 this.editFields.hide();
650 this.closeButton.hide();
678 this.closeButton.hide();
651 this.viewFields.show();
679 this.viewFields.show();
652 }
680 }
653 };
681 };
654
682
655 var ReviewersPanel = {
683 var ReviewersPanel = {
656 editButton: $('#open_edit_reviewers'),
684 editButton: $('#open_edit_reviewers'),
657 closeButton: $('#close_edit_reviewers'),
685 closeButton: $('#close_edit_reviewers'),
658 addButton: $('#add_reviewer_input'),
686 addButton: $('#add_reviewer'),
659 removeButtons: $('.reviewer_member_remove'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
660
688
661 init: function() {
689 init: function() {
662 var that = this;
690 var self = this;
663 this.editButton.on('click', function(e) { that.edit(); });
691 this.editButton.on('click', function(e) { self.edit(); });
664 this.closeButton.on('click', function(e) { that.close(); });
692 this.closeButton.on('click', function(e) { self.close(); });
665 },
693 },
666
694
667 edit: function(event) {
695 edit: function(event) {
668 this.editButton.hide();
696 this.editButton.hide();
669 this.closeButton.show();
697 this.closeButton.show();
670 this.addButton.show();
698 this.addButton.show();
671 this.removeButtons.css('visibility', 'visible');
699 this.removeButtons.css('visibility', 'visible');
700 // review rules
701 reviewersController.loadReviewRules(
702 ${c.pull_request.reviewer_data_json | n});
672 },
703 },
673
704
674 close: function(event) {
705 close: function(event) {
675 this.editButton.show();
706 this.editButton.show();
676 this.closeButton.hide();
707 this.closeButton.hide();
677 this.addButton.hide();
708 this.addButton.hide();
678 this.removeButtons.css('visibility', 'hidden');
709 this.removeButtons.css('visibility', 'hidden');
710 // hide review rules
711 reviewersController.hideReviewRules()
679 }
712 }
680 };
713 };
681
714
682 PRDetails.init();
715 PRDetails.init();
683 ReviewersPanel.init();
716 ReviewersPanel.init();
684
717
685 showOutdated = function(self){
718 showOutdated = function(self){
686 $('.comment-inline.comment-outdated').show();
719 $('.comment-inline.comment-outdated').show();
687 $('.filediff-outdated').show();
720 $('.filediff-outdated').show();
688 $('.showOutdatedComments').hide();
721 $('.showOutdatedComments').hide();
689 $('.hideOutdatedComments').show();
722 $('.hideOutdatedComments').show();
690 };
723 };
691
724
692 hideOutdated = function(self){
725 hideOutdated = function(self){
693 $('.comment-inline.comment-outdated').hide();
726 $('.comment-inline.comment-outdated').hide();
694 $('.filediff-outdated').hide();
727 $('.filediff-outdated').hide();
695 $('.hideOutdatedComments').hide();
728 $('.hideOutdatedComments').hide();
696 $('.showOutdatedComments').show();
729 $('.showOutdatedComments').show();
697 };
730 };
698
731
699 refreshMergeChecks = function(){
732 refreshMergeChecks = function(){
700 var loadUrl = "${h.url.current(merge_checks=1)}";
733 var loadUrl = "${h.url.current(merge_checks=1)}";
701 $('.pull-request-merge').css('opacity', 0.3);
734 $('.pull-request-merge').css('opacity', 0.3);
702 $('.action-buttons-extra').css('opacity', 0.3);
735 $('.action-buttons-extra').css('opacity', 0.3);
703
736
704 $('.pull-request-merge').load(
737 $('.pull-request-merge').load(
705 loadUrl, function() {
738 loadUrl, function() {
706 $('.pull-request-merge').css('opacity', 1);
739 $('.pull-request-merge').css('opacity', 1);
707
740
708 $('.action-buttons-extra').css('opacity', 1);
741 $('.action-buttons-extra').css('opacity', 1);
709 injectCloseAction();
742 injectCloseAction();
710 }
743 }
711 );
744 );
712 };
745 };
713
746
714 injectCloseAction = function() {
747 injectCloseAction = function() {
715 var closeAction = $('#close-pull-request-action').html();
748 var closeAction = $('#close-pull-request-action').html();
716 var $actionButtons = $('.action-buttons-extra');
749 var $actionButtons = $('.action-buttons-extra');
717 // clear the action before
750 // clear the action before
718 $actionButtons.html("");
751 $actionButtons.html("");
719 $actionButtons.html(closeAction);
752 $actionButtons.html(closeAction);
720 };
753 };
721
754
722 closePullRequest = function (status) {
755 closePullRequest = function (status) {
723 // inject closing flag
756 // inject closing flag
724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
726 $(generalCommentForm.submitForm).submit();
759 $(generalCommentForm.submitForm).submit();
727 };
760 };
728
761
729 $('#show-outdated-comments').on('click', function(e){
762 $('#show-outdated-comments').on('click', function(e){
730 var button = $(this);
763 var button = $(this);
731 var outdated = $('.comment-outdated');
764 var outdated = $('.comment-outdated');
732
765
733 if (button.html() === "(Show)") {
766 if (button.html() === "(Show)") {
734 button.html("(Hide)");
767 button.html("(Hide)");
735 outdated.show();
768 outdated.show();
736 } else {
769 } else {
737 button.html("(Show)");
770 button.html("(Show)");
738 outdated.hide();
771 outdated.hide();
739 }
772 }
740 });
773 });
741
774
742 $('.show-inline-comments').on('change', function(e){
775 $('.show-inline-comments').on('change', function(e){
743 var show = 'none';
776 var show = 'none';
744 var target = e.currentTarget;
777 var target = e.currentTarget;
745 if(target.checked){
778 if(target.checked){
746 show = ''
779 show = ''
747 }
780 }
748 var boxid = $(target).attr('id_for');
781 var boxid = $(target).attr('id_for');
749 var comments = $('#{0} .inline-comments'.format(boxid));
782 var comments = $('#{0} .inline-comments'.format(boxid));
750 var fn_display = function(idx){
783 var fn_display = function(idx){
751 $(this).css('display', show);
784 $(this).css('display', show);
752 };
785 };
753 $(comments).each(fn_display);
786 $(comments).each(fn_display);
754 var btns = $('#{0} .inline-comments-button'.format(boxid));
787 var btns = $('#{0} .inline-comments-button'.format(boxid));
755 $(btns).each(fn_display);
788 $(btns).each(fn_display);
756 });
789 });
757
790
758 $('#merge_pull_request_form').submit(function() {
791 $('#merge_pull_request_form').submit(function() {
759 if (!$('#merge_pull_request').attr('disabled')) {
792 if (!$('#merge_pull_request').attr('disabled')) {
760 $('#merge_pull_request').attr('disabled', 'disabled');
793 $('#merge_pull_request').attr('disabled', 'disabled');
761 }
794 }
762 return true;
795 return true;
763 });
796 });
764
797
765 $('#edit_pull_request').on('click', function(e){
798 $('#edit_pull_request').on('click', function(e){
766 var title = $('#pr-title-input').val();
799 var title = $('#pr-title-input').val();
767 var description = codeMirrorInstance.getValue();
800 var description = codeMirrorInstance.getValue();
768 editPullRequest(
801 editPullRequest(
769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
770 title, description);
803 title, description);
771 });
804 });
772
805
773 $('#update_pull_request').on('click', function(e){
806 $('#update_pull_request').on('click', function(e){
774 $(this).attr('disabled', 'disabled');
807 $(this).attr('disabled', 'disabled');
775 $(this).addClass('disabled');
808 $(this).addClass('disabled');
776 $(this).html(_gettext('Saving...'));
809 $(this).html(_gettext('Saving...'));
777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
810 reviewersController.updateReviewers(
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
778 });
812 });
779
813
780 $('#update_commits').on('click', function(e){
814 $('#update_commits').on('click', function(e){
781 var isDisabled = !$(e.currentTarget).attr('disabled');
815 var isDisabled = !$(e.currentTarget).attr('disabled');
782 $(e.currentTarget).attr('disabled', 'disabled');
816 $(e.currentTarget).attr('disabled', 'disabled');
783 $(e.currentTarget).addClass('disabled');
817 $(e.currentTarget).addClass('disabled');
784 $(e.currentTarget).removeClass('btn-primary');
818 $(e.currentTarget).removeClass('btn-primary');
785 $(e.currentTarget).text(_gettext('Updating...'));
819 $(e.currentTarget).text(_gettext('Updating...'));
786 if(isDisabled){
820 if(isDisabled){
787 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
821 updateCommits(
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
788 }
823 }
789 });
824 });
790 // fixing issue with caches on firefox
825 // fixing issue with caches on firefox
791 $('#update_commits').removeAttr("disabled");
826 $('#update_commits').removeAttr("disabled");
792
827
793 $('#close_pull_request').on('click', function(e){
828 $('#close_pull_request').on('click', function(e){
794 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
829 closePullRequest(
830 "${c.repo_name}", "${c.pull_request.pull_request_id}");
795 });
831 });
796
832
797 $('.show-inline-comments').on('click', function(e){
833 $('.show-inline-comments').on('click', function(e){
798 var boxid = $(this).attr('data-comment-id');
834 var boxid = $(this).attr('data-comment-id');
799 var button = $(this);
835 var button = $(this);
800
836
801 if(button.hasClass("comments-visible")) {
837 if(button.hasClass("comments-visible")) {
802 $('#{0} .inline-comments'.format(boxid)).each(function(index){
838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
803 $(this).hide();
839 $(this).hide();
804 });
840 });
805 button.removeClass("comments-visible");
841 button.removeClass("comments-visible");
806 } else {
842 } else {
807 $('#{0} .inline-comments'.format(boxid)).each(function(index){
843 $('#{0} .inline-comments'.format(boxid)).each(function(index){
808 $(this).show();
844 $(this).show();
809 });
845 });
810 button.addClass("comments-visible");
846 button.addClass("comments-visible");
811 }
847 }
812 });
848 });
813
849
814 // register submit callback on commentForm form to track TODOs
850 // register submit callback on commentForm form to track TODOs
815 window.commentFormGlobalSubmitSuccessCallback = function(){
851 window.commentFormGlobalSubmitSuccessCallback = function(){
816 refreshMergeChecks();
852 refreshMergeChecks();
817 };
853 };
818 // initial injection
854 // initial injection
819 injectCloseAction();
855 injectCloseAction();
820
856
857 ReviewerAutoComplete('#user');
858
821 })
859 })
822 </script>
860 </script>
823
861
824 </div>
862 </div>
825 </div>
863 </div>
826
864
827 </%def>
865 </%def>
@@ -1,511 +1,512 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.config.routing import ADMIN_PREFIX
27 from rhodecode.tests import (
27 from rhodecode.tests import (
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN,
29 no_newline_id_generator)
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.utils import AssertResponse, get_session_from_response
31 from rhodecode.tests.utils import AssertResponse, get_session_from_response
31 from rhodecode.lib.auth import check_password
32 from rhodecode.lib.auth import check_password
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model import validators
34 from rhodecode.model import validators
34 from rhodecode.model.db import User, Notification, UserApiKeys
35 from rhodecode.model.db import User, Notification, UserApiKeys
35 from rhodecode.model.meta import Session
36 from rhodecode.model.meta import Session
36
37
37 fixture = Fixture()
38 fixture = Fixture()
38
39
39 # Hardcode URLs because we don't have a request object to use
40 # Hardcode URLs because we don't have a request object to use
40 # pyramids URL generation methods.
41 # pyramids URL generation methods.
41 index_url = '/'
42 index_url = '/'
42 login_url = ADMIN_PREFIX + '/login'
43 login_url = ADMIN_PREFIX + '/login'
43 logut_url = ADMIN_PREFIX + '/logout'
44 logut_url = ADMIN_PREFIX + '/logout'
44 register_url = ADMIN_PREFIX + '/register'
45 register_url = ADMIN_PREFIX + '/register'
45 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
46 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
46 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
47 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
47
48
48
49
49 @pytest.mark.usefixtures('app')
50 @pytest.mark.usefixtures('app')
50 class TestLoginController(object):
51 class TestLoginController(object):
51 destroy_users = set()
52 destroy_users = set()
52
53
53 @classmethod
54 @classmethod
54 def teardown_class(cls):
55 def teardown_class(cls):
55 fixture.destroy_users(cls.destroy_users)
56 fixture.destroy_users(cls.destroy_users)
56
57
57 def teardown_method(self, method):
58 def teardown_method(self, method):
58 for n in Notification.query().all():
59 for n in Notification.query().all():
59 Session().delete(n)
60 Session().delete(n)
60
61
61 Session().commit()
62 Session().commit()
62 assert Notification.query().all() == []
63 assert Notification.query().all() == []
63
64
64 def test_index(self):
65 def test_index(self):
65 response = self.app.get(login_url)
66 response = self.app.get(login_url)
66 assert response.status == '200 OK'
67 assert response.status == '200 OK'
67 # Test response...
68 # Test response...
68
69
69 def test_login_admin_ok(self):
70 def test_login_admin_ok(self):
70 response = self.app.post(login_url,
71 response = self.app.post(login_url,
71 {'username': 'test_admin',
72 {'username': 'test_admin',
72 'password': 'test12'})
73 'password': 'test12'})
73 assert response.status == '302 Found'
74 assert response.status == '302 Found'
74 session = get_session_from_response(response)
75 session = get_session_from_response(response)
75 username = session['rhodecode_user'].get('username')
76 username = session['rhodecode_user'].get('username')
76 assert username == 'test_admin'
77 assert username == 'test_admin'
77 response = response.follow()
78 response = response.follow()
78 response.mustcontain('/%s' % HG_REPO)
79 response.mustcontain('/%s' % HG_REPO)
79
80
80 def test_login_regular_ok(self):
81 def test_login_regular_ok(self):
81 response = self.app.post(login_url,
82 response = self.app.post(login_url,
82 {'username': 'test_regular',
83 {'username': 'test_regular',
83 'password': 'test12'})
84 'password': 'test12'})
84
85
85 assert response.status == '302 Found'
86 assert response.status == '302 Found'
86 session = get_session_from_response(response)
87 session = get_session_from_response(response)
87 username = session['rhodecode_user'].get('username')
88 username = session['rhodecode_user'].get('username')
88 assert username == 'test_regular'
89 assert username == 'test_regular'
89 response = response.follow()
90 response = response.follow()
90 response.mustcontain('/%s' % HG_REPO)
91 response.mustcontain('/%s' % HG_REPO)
91
92
92 def test_login_ok_came_from(self):
93 def test_login_ok_came_from(self):
93 test_came_from = '/_admin/users?branch=stable'
94 test_came_from = '/_admin/users?branch=stable'
94 _url = '{}?came_from={}'.format(login_url, test_came_from)
95 _url = '{}?came_from={}'.format(login_url, test_came_from)
95 response = self.app.post(
96 response = self.app.post(
96 _url, {'username': 'test_admin', 'password': 'test12'})
97 _url, {'username': 'test_admin', 'password': 'test12'})
97 assert response.status == '302 Found'
98 assert response.status == '302 Found'
98 assert 'branch=stable' in response.location
99 assert 'branch=stable' in response.location
99 response = response.follow()
100 response = response.follow()
100
101
101 assert response.status == '200 OK'
102 assert response.status == '200 OK'
102 response.mustcontain('Users administration')
103 response.mustcontain('Users administration')
103
104
104 def test_redirect_to_login_with_get_args(self):
105 def test_redirect_to_login_with_get_args(self):
105 with fixture.anon_access(False):
106 with fixture.anon_access(False):
106 kwargs = {'branch': 'stable'}
107 kwargs = {'branch': 'stable'}
107 response = self.app.get(
108 response = self.app.get(
108 url('summary_home', repo_name=HG_REPO, **kwargs))
109 url('summary_home', repo_name=HG_REPO, **kwargs))
109 assert response.status == '302 Found'
110 assert response.status == '302 Found'
110 response_query = urlparse.parse_qsl(response.location)
111 response_query = urlparse.parse_qsl(response.location)
111 assert 'branch=stable' in response_query[0][1]
112 assert 'branch=stable' in response_query[0][1]
112
113
113 def test_login_form_with_get_args(self):
114 def test_login_form_with_get_args(self):
114 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
115 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
115 response = self.app.get(_url)
116 response = self.app.get(_url)
116 assert 'branch%3Dstable' in response.form.action
117 assert 'branch%3Dstable' in response.form.action
117
118
118 @pytest.mark.parametrize("url_came_from", [
119 @pytest.mark.parametrize("url_came_from", [
119 'data:text/html,<script>window.alert("xss")</script>',
120 'data:text/html,<script>window.alert("xss")</script>',
120 'mailto:test@rhodecode.org',
121 'mailto:test@rhodecode.org',
121 'file:///etc/passwd',
122 'file:///etc/passwd',
122 'ftp://some.ftp.server',
123 'ftp://some.ftp.server',
123 'http://other.domain',
124 'http://other.domain',
124 '/\r\nX-Forwarded-Host: http://example.org',
125 '/\r\nX-Forwarded-Host: http://example.org',
125 ])
126 ], ids=no_newline_id_generator)
126 def test_login_bad_came_froms(self, url_came_from):
127 def test_login_bad_came_froms(self, url_came_from):
127 _url = '{}?came_from={}'.format(login_url, url_came_from)
128 _url = '{}?came_from={}'.format(login_url, url_came_from)
128 response = self.app.post(
129 response = self.app.post(
129 _url,
130 _url,
130 {'username': 'test_admin', 'password': 'test12'})
131 {'username': 'test_admin', 'password': 'test12'})
131 assert response.status == '302 Found'
132 assert response.status == '302 Found'
132 response = response.follow()
133 response = response.follow()
133 assert response.status == '200 OK'
134 assert response.status == '200 OK'
134 assert response.request.path == '/'
135 assert response.request.path == '/'
135
136
136 def test_login_short_password(self):
137 def test_login_short_password(self):
137 response = self.app.post(login_url,
138 response = self.app.post(login_url,
138 {'username': 'test_admin',
139 {'username': 'test_admin',
139 'password': 'as'})
140 'password': 'as'})
140 assert response.status == '200 OK'
141 assert response.status == '200 OK'
141
142
142 response.mustcontain('Enter 3 characters or more')
143 response.mustcontain('Enter 3 characters or more')
143
144
144 def test_login_wrong_non_ascii_password(self, user_regular):
145 def test_login_wrong_non_ascii_password(self, user_regular):
145 response = self.app.post(
146 response = self.app.post(
146 login_url,
147 login_url,
147 {'username': user_regular.username,
148 {'username': user_regular.username,
148 'password': u'invalid-non-asci\xe4'.encode('utf8')})
149 'password': u'invalid-non-asci\xe4'.encode('utf8')})
149
150
150 response.mustcontain('invalid user name')
151 response.mustcontain('invalid user name')
151 response.mustcontain('invalid password')
152 response.mustcontain('invalid password')
152
153
153 def test_login_with_non_ascii_password(self, user_util):
154 def test_login_with_non_ascii_password(self, user_util):
154 password = u'valid-non-ascii\xe4'
155 password = u'valid-non-ascii\xe4'
155 user = user_util.create_user(password=password)
156 user = user_util.create_user(password=password)
156 response = self.app.post(
157 response = self.app.post(
157 login_url,
158 login_url,
158 {'username': user.username,
159 {'username': user.username,
159 'password': password.encode('utf-8')})
160 'password': password.encode('utf-8')})
160 assert response.status_code == 302
161 assert response.status_code == 302
161
162
162 def test_login_wrong_username_password(self):
163 def test_login_wrong_username_password(self):
163 response = self.app.post(login_url,
164 response = self.app.post(login_url,
164 {'username': 'error',
165 {'username': 'error',
165 'password': 'test12'})
166 'password': 'test12'})
166
167
167 response.mustcontain('invalid user name')
168 response.mustcontain('invalid user name')
168 response.mustcontain('invalid password')
169 response.mustcontain('invalid password')
169
170
170 def test_login_admin_ok_password_migration(self, real_crypto_backend):
171 def test_login_admin_ok_password_migration(self, real_crypto_backend):
171 from rhodecode.lib import auth
172 from rhodecode.lib import auth
172
173
173 # create new user, with sha256 password
174 # create new user, with sha256 password
174 temp_user = 'test_admin_sha256'
175 temp_user = 'test_admin_sha256'
175 user = fixture.create_user(temp_user)
176 user = fixture.create_user(temp_user)
176 user.password = auth._RhodeCodeCryptoSha256().hash_create(
177 user.password = auth._RhodeCodeCryptoSha256().hash_create(
177 b'test123')
178 b'test123')
178 Session().add(user)
179 Session().add(user)
179 Session().commit()
180 Session().commit()
180 self.destroy_users.add(temp_user)
181 self.destroy_users.add(temp_user)
181 response = self.app.post(login_url,
182 response = self.app.post(login_url,
182 {'username': temp_user,
183 {'username': temp_user,
183 'password': 'test123'})
184 'password': 'test123'})
184
185
185 assert response.status == '302 Found'
186 assert response.status == '302 Found'
186 session = get_session_from_response(response)
187 session = get_session_from_response(response)
187 username = session['rhodecode_user'].get('username')
188 username = session['rhodecode_user'].get('username')
188 assert username == temp_user
189 assert username == temp_user
189 response = response.follow()
190 response = response.follow()
190 response.mustcontain('/%s' % HG_REPO)
191 response.mustcontain('/%s' % HG_REPO)
191
192
192 # new password should be bcrypted, after log-in and transfer
193 # new password should be bcrypted, after log-in and transfer
193 user = User.get_by_username(temp_user)
194 user = User.get_by_username(temp_user)
194 assert user.password.startswith('$')
195 assert user.password.startswith('$')
195
196
196 # REGISTRATIONS
197 # REGISTRATIONS
197 def test_register(self):
198 def test_register(self):
198 response = self.app.get(register_url)
199 response = self.app.get(register_url)
199 response.mustcontain('Create an Account')
200 response.mustcontain('Create an Account')
200
201
201 def test_register_err_same_username(self):
202 def test_register_err_same_username(self):
202 uname = 'test_admin'
203 uname = 'test_admin'
203 response = self.app.post(
204 response = self.app.post(
204 register_url,
205 register_url,
205 {
206 {
206 'username': uname,
207 'username': uname,
207 'password': 'test12',
208 'password': 'test12',
208 'password_confirmation': 'test12',
209 'password_confirmation': 'test12',
209 'email': 'goodmail@domain.com',
210 'email': 'goodmail@domain.com',
210 'firstname': 'test',
211 'firstname': 'test',
211 'lastname': 'test'
212 'lastname': 'test'
212 }
213 }
213 )
214 )
214
215
215 assertr = AssertResponse(response)
216 assertr = AssertResponse(response)
216 msg = validators.ValidUsername()._messages['username_exists']
217 msg = validators.ValidUsername()._messages['username_exists']
217 msg = msg % {'username': uname}
218 msg = msg % {'username': uname}
218 assertr.element_contains('#username+.error-message', msg)
219 assertr.element_contains('#username+.error-message', msg)
219
220
220 def test_register_err_same_email(self):
221 def test_register_err_same_email(self):
221 response = self.app.post(
222 response = self.app.post(
222 register_url,
223 register_url,
223 {
224 {
224 'username': 'test_admin_0',
225 'username': 'test_admin_0',
225 'password': 'test12',
226 'password': 'test12',
226 'password_confirmation': 'test12',
227 'password_confirmation': 'test12',
227 'email': 'test_admin@mail.com',
228 'email': 'test_admin@mail.com',
228 'firstname': 'test',
229 'firstname': 'test',
229 'lastname': 'test'
230 'lastname': 'test'
230 }
231 }
231 )
232 )
232
233
233 assertr = AssertResponse(response)
234 assertr = AssertResponse(response)
234 msg = validators.UniqSystemEmail()()._messages['email_taken']
235 msg = validators.UniqSystemEmail()()._messages['email_taken']
235 assertr.element_contains('#email+.error-message', msg)
236 assertr.element_contains('#email+.error-message', msg)
236
237
237 def test_register_err_same_email_case_sensitive(self):
238 def test_register_err_same_email_case_sensitive(self):
238 response = self.app.post(
239 response = self.app.post(
239 register_url,
240 register_url,
240 {
241 {
241 'username': 'test_admin_1',
242 'username': 'test_admin_1',
242 'password': 'test12',
243 'password': 'test12',
243 'password_confirmation': 'test12',
244 'password_confirmation': 'test12',
244 'email': 'TesT_Admin@mail.COM',
245 'email': 'TesT_Admin@mail.COM',
245 'firstname': 'test',
246 'firstname': 'test',
246 'lastname': 'test'
247 'lastname': 'test'
247 }
248 }
248 )
249 )
249 assertr = AssertResponse(response)
250 assertr = AssertResponse(response)
250 msg = validators.UniqSystemEmail()()._messages['email_taken']
251 msg = validators.UniqSystemEmail()()._messages['email_taken']
251 assertr.element_contains('#email+.error-message', msg)
252 assertr.element_contains('#email+.error-message', msg)
252
253
253 def test_register_err_wrong_data(self):
254 def test_register_err_wrong_data(self):
254 response = self.app.post(
255 response = self.app.post(
255 register_url,
256 register_url,
256 {
257 {
257 'username': 'xs',
258 'username': 'xs',
258 'password': 'test',
259 'password': 'test',
259 'password_confirmation': 'test',
260 'password_confirmation': 'test',
260 'email': 'goodmailm',
261 'email': 'goodmailm',
261 'firstname': 'test',
262 'firstname': 'test',
262 'lastname': 'test'
263 'lastname': 'test'
263 }
264 }
264 )
265 )
265 assert response.status == '200 OK'
266 assert response.status == '200 OK'
266 response.mustcontain('An email address must contain a single @')
267 response.mustcontain('An email address must contain a single @')
267 response.mustcontain('Enter a value 6 characters long or more')
268 response.mustcontain('Enter a value 6 characters long or more')
268
269
269 def test_register_err_username(self):
270 def test_register_err_username(self):
270 response = self.app.post(
271 response = self.app.post(
271 register_url,
272 register_url,
272 {
273 {
273 'username': 'error user',
274 'username': 'error user',
274 'password': 'test12',
275 'password': 'test12',
275 'password_confirmation': 'test12',
276 'password_confirmation': 'test12',
276 'email': 'goodmailm',
277 'email': 'goodmailm',
277 'firstname': 'test',
278 'firstname': 'test',
278 'lastname': 'test'
279 'lastname': 'test'
279 }
280 }
280 )
281 )
281
282
282 response.mustcontain('An email address must contain a single @')
283 response.mustcontain('An email address must contain a single @')
283 response.mustcontain(
284 response.mustcontain(
284 'Username may only contain '
285 'Username may only contain '
285 'alphanumeric characters underscores, '
286 'alphanumeric characters underscores, '
286 'periods or dashes and must begin with '
287 'periods or dashes and must begin with '
287 'alphanumeric character')
288 'alphanumeric character')
288
289
289 def test_register_err_case_sensitive(self):
290 def test_register_err_case_sensitive(self):
290 usr = 'Test_Admin'
291 usr = 'Test_Admin'
291 response = self.app.post(
292 response = self.app.post(
292 register_url,
293 register_url,
293 {
294 {
294 'username': usr,
295 'username': usr,
295 'password': 'test12',
296 'password': 'test12',
296 'password_confirmation': 'test12',
297 'password_confirmation': 'test12',
297 'email': 'goodmailm',
298 'email': 'goodmailm',
298 'firstname': 'test',
299 'firstname': 'test',
299 'lastname': 'test'
300 'lastname': 'test'
300 }
301 }
301 )
302 )
302
303
303 assertr = AssertResponse(response)
304 assertr = AssertResponse(response)
304 msg = validators.ValidUsername()._messages['username_exists']
305 msg = validators.ValidUsername()._messages['username_exists']
305 msg = msg % {'username': usr}
306 msg = msg % {'username': usr}
306 assertr.element_contains('#username+.error-message', msg)
307 assertr.element_contains('#username+.error-message', msg)
307
308
308 def test_register_special_chars(self):
309 def test_register_special_chars(self):
309 response = self.app.post(
310 response = self.app.post(
310 register_url,
311 register_url,
311 {
312 {
312 'username': 'xxxaxn',
313 'username': 'xxxaxn',
313 'password': 'ąćźżąśśśś',
314 'password': 'ąćźżąśśśś',
314 'password_confirmation': 'ąćźżąśśśś',
315 'password_confirmation': 'ąćźżąśśśś',
315 'email': 'goodmailm@test.plx',
316 'email': 'goodmailm@test.plx',
316 'firstname': 'test',
317 'firstname': 'test',
317 'lastname': 'test'
318 'lastname': 'test'
318 }
319 }
319 )
320 )
320
321
321 msg = validators.ValidPassword()._messages['invalid_password']
322 msg = validators.ValidPassword()._messages['invalid_password']
322 response.mustcontain(msg)
323 response.mustcontain(msg)
323
324
324 def test_register_password_mismatch(self):
325 def test_register_password_mismatch(self):
325 response = self.app.post(
326 response = self.app.post(
326 register_url,
327 register_url,
327 {
328 {
328 'username': 'xs',
329 'username': 'xs',
329 'password': '123qwe',
330 'password': '123qwe',
330 'password_confirmation': 'qwe123',
331 'password_confirmation': 'qwe123',
331 'email': 'goodmailm@test.plxa',
332 'email': 'goodmailm@test.plxa',
332 'firstname': 'test',
333 'firstname': 'test',
333 'lastname': 'test'
334 'lastname': 'test'
334 }
335 }
335 )
336 )
336 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
337 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
337 response.mustcontain(msg)
338 response.mustcontain(msg)
338
339
339 def test_register_ok(self):
340 def test_register_ok(self):
340 username = 'test_regular4'
341 username = 'test_regular4'
341 password = 'qweqwe'
342 password = 'qweqwe'
342 email = 'marcin@test.com'
343 email = 'marcin@test.com'
343 name = 'testname'
344 name = 'testname'
344 lastname = 'testlastname'
345 lastname = 'testlastname'
345
346
346 response = self.app.post(
347 response = self.app.post(
347 register_url,
348 register_url,
348 {
349 {
349 'username': username,
350 'username': username,
350 'password': password,
351 'password': password,
351 'password_confirmation': password,
352 'password_confirmation': password,
352 'email': email,
353 'email': email,
353 'firstname': name,
354 'firstname': name,
354 'lastname': lastname,
355 'lastname': lastname,
355 'admin': True
356 'admin': True
356 }
357 }
357 ) # This should be overriden
358 ) # This should be overriden
358 assert response.status == '302 Found'
359 assert response.status == '302 Found'
359 assert_session_flash(
360 assert_session_flash(
360 response, 'You have successfully registered with RhodeCode')
361 response, 'You have successfully registered with RhodeCode')
361
362
362 ret = Session().query(User).filter(
363 ret = Session().query(User).filter(
363 User.username == 'test_regular4').one()
364 User.username == 'test_regular4').one()
364 assert ret.username == username
365 assert ret.username == username
365 assert check_password(password, ret.password)
366 assert check_password(password, ret.password)
366 assert ret.email == email
367 assert ret.email == email
367 assert ret.name == name
368 assert ret.name == name
368 assert ret.lastname == lastname
369 assert ret.lastname == lastname
369 assert ret.auth_tokens is not None
370 assert ret.auth_tokens is not None
370 assert not ret.admin
371 assert not ret.admin
371
372
372 def test_forgot_password_wrong_mail(self):
373 def test_forgot_password_wrong_mail(self):
373 bad_email = 'marcin@wrongmail.org'
374 bad_email = 'marcin@wrongmail.org'
374 response = self.app.post(
375 response = self.app.post(
375 pwd_reset_url, {'email': bad_email, }
376 pwd_reset_url, {'email': bad_email, }
376 )
377 )
377 assert_session_flash(response,
378 assert_session_flash(response,
378 'If such email exists, a password reset link was sent to it.')
379 'If such email exists, a password reset link was sent to it.')
379
380
380 def test_forgot_password(self, user_util):
381 def test_forgot_password(self, user_util):
381 response = self.app.get(pwd_reset_url)
382 response = self.app.get(pwd_reset_url)
382 assert response.status == '200 OK'
383 assert response.status == '200 OK'
383
384
384 user = user_util.create_user()
385 user = user_util.create_user()
385 user_id = user.user_id
386 user_id = user.user_id
386 email = user.email
387 email = user.email
387
388
388 response = self.app.post(pwd_reset_url, {'email': email, })
389 response = self.app.post(pwd_reset_url, {'email': email, })
389
390
390 assert_session_flash(response,
391 assert_session_flash(response,
391 'If such email exists, a password reset link was sent to it.')
392 'If such email exists, a password reset link was sent to it.')
392
393
393 # BAD KEY
394 # BAD KEY
394 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey')
395 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey')
395 response = self.app.get(confirm_url)
396 response = self.app.get(confirm_url)
396 assert response.status == '302 Found'
397 assert response.status == '302 Found'
397 assert response.location.endswith(pwd_reset_url)
398 assert response.location.endswith(pwd_reset_url)
398 assert_session_flash(response, 'Given reset token is invalid')
399 assert_session_flash(response, 'Given reset token is invalid')
399
400
400 response.follow() # cleanup flash
401 response.follow() # cleanup flash
401
402
402 # GOOD KEY
403 # GOOD KEY
403 key = UserApiKeys.query()\
404 key = UserApiKeys.query()\
404 .filter(UserApiKeys.user_id == user_id)\
405 .filter(UserApiKeys.user_id == user_id)\
405 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
406 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
406 .first()
407 .first()
407
408
408 assert key
409 assert key
409
410
410 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key)
411 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key)
411 response = self.app.get(confirm_url)
412 response = self.app.get(confirm_url)
412 assert response.status == '302 Found'
413 assert response.status == '302 Found'
413 assert response.location.endswith(login_url)
414 assert response.location.endswith(login_url)
414
415
415 assert_session_flash(
416 assert_session_flash(
416 response,
417 response,
417 'Your password reset was successful, '
418 'Your password reset was successful, '
418 'a new password has been sent to your email')
419 'a new password has been sent to your email')
419
420
420 response.follow()
421 response.follow()
421
422
422 def _get_api_whitelist(self, values=None):
423 def _get_api_whitelist(self, values=None):
423 config = {'api_access_controllers_whitelist': values or []}
424 config = {'api_access_controllers_whitelist': values or []}
424 return config
425 return config
425
426
426 @pytest.mark.parametrize("test_name, auth_token", [
427 @pytest.mark.parametrize("test_name, auth_token", [
427 ('none', None),
428 ('none', None),
428 ('empty_string', ''),
429 ('empty_string', ''),
429 ('fake_number', '123456'),
430 ('fake_number', '123456'),
430 ('proper_auth_token', None)
431 ('proper_auth_token', None)
431 ])
432 ])
432 def test_access_not_whitelisted_page_via_auth_token(
433 def test_access_not_whitelisted_page_via_auth_token(
433 self, test_name, auth_token, user_admin):
434 self, test_name, auth_token, user_admin):
434
435
435 whitelist = self._get_api_whitelist([])
436 whitelist = self._get_api_whitelist([])
436 with mock.patch.dict('rhodecode.CONFIG', whitelist):
437 with mock.patch.dict('rhodecode.CONFIG', whitelist):
437 assert [] == whitelist['api_access_controllers_whitelist']
438 assert [] == whitelist['api_access_controllers_whitelist']
438 if test_name == 'proper_auth_token':
439 if test_name == 'proper_auth_token':
439 # use builtin if api_key is None
440 # use builtin if api_key is None
440 auth_token = user_admin.api_key
441 auth_token = user_admin.api_key
441
442
442 with fixture.anon_access(False):
443 with fixture.anon_access(False):
443 self.app.get(url(controller='changeset',
444 self.app.get(url(controller='changeset',
444 action='changeset_raw',
445 action='changeset_raw',
445 repo_name=HG_REPO, revision='tip',
446 repo_name=HG_REPO, revision='tip',
446 api_key=auth_token),
447 api_key=auth_token),
447 status=302)
448 status=302)
448
449
449 @pytest.mark.parametrize("test_name, auth_token, code", [
450 @pytest.mark.parametrize("test_name, auth_token, code", [
450 ('none', None, 302),
451 ('none', None, 302),
451 ('empty_string', '', 302),
452 ('empty_string', '', 302),
452 ('fake_number', '123456', 302),
453 ('fake_number', '123456', 302),
453 ('proper_auth_token', None, 200)
454 ('proper_auth_token', None, 200)
454 ])
455 ])
455 def test_access_whitelisted_page_via_auth_token(
456 def test_access_whitelisted_page_via_auth_token(
456 self, test_name, auth_token, code, user_admin):
457 self, test_name, auth_token, code, user_admin):
457
458
458 whitelist_entry = ['ChangesetController:changeset_raw']
459 whitelist_entry = ['ChangesetController:changeset_raw']
459 whitelist = self._get_api_whitelist(whitelist_entry)
460 whitelist = self._get_api_whitelist(whitelist_entry)
460
461
461 with mock.patch.dict('rhodecode.CONFIG', whitelist):
462 with mock.patch.dict('rhodecode.CONFIG', whitelist):
462 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
463 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
463
464
464 if test_name == 'proper_auth_token':
465 if test_name == 'proper_auth_token':
465 auth_token = user_admin.api_key
466 auth_token = user_admin.api_key
466 assert auth_token
467 assert auth_token
467
468
468 with fixture.anon_access(False):
469 with fixture.anon_access(False):
469 self.app.get(url(controller='changeset',
470 self.app.get(url(controller='changeset',
470 action='changeset_raw',
471 action='changeset_raw',
471 repo_name=HG_REPO, revision='tip',
472 repo_name=HG_REPO, revision='tip',
472 api_key=auth_token),
473 api_key=auth_token),
473 status=code)
474 status=code)
474
475
475 def test_access_page_via_extra_auth_token(self):
476 def test_access_page_via_extra_auth_token(self):
476 whitelist = self._get_api_whitelist(
477 whitelist = self._get_api_whitelist(
477 ['ChangesetController:changeset_raw'])
478 ['ChangesetController:changeset_raw'])
478 with mock.patch.dict('rhodecode.CONFIG', whitelist):
479 with mock.patch.dict('rhodecode.CONFIG', whitelist):
479 assert ['ChangesetController:changeset_raw'] == \
480 assert ['ChangesetController:changeset_raw'] == \
480 whitelist['api_access_controllers_whitelist']
481 whitelist['api_access_controllers_whitelist']
481
482
482 new_auth_token = AuthTokenModel().create(
483 new_auth_token = AuthTokenModel().create(
483 TEST_USER_ADMIN_LOGIN, 'test')
484 TEST_USER_ADMIN_LOGIN, 'test')
484 Session().commit()
485 Session().commit()
485 with fixture.anon_access(False):
486 with fixture.anon_access(False):
486 self.app.get(url(controller='changeset',
487 self.app.get(url(controller='changeset',
487 action='changeset_raw',
488 action='changeset_raw',
488 repo_name=HG_REPO, revision='tip',
489 repo_name=HG_REPO, revision='tip',
489 api_key=new_auth_token.api_key),
490 api_key=new_auth_token.api_key),
490 status=200)
491 status=200)
491
492
492 def test_access_page_via_expired_auth_token(self):
493 def test_access_page_via_expired_auth_token(self):
493 whitelist = self._get_api_whitelist(
494 whitelist = self._get_api_whitelist(
494 ['ChangesetController:changeset_raw'])
495 ['ChangesetController:changeset_raw'])
495 with mock.patch.dict('rhodecode.CONFIG', whitelist):
496 with mock.patch.dict('rhodecode.CONFIG', whitelist):
496 assert ['ChangesetController:changeset_raw'] == \
497 assert ['ChangesetController:changeset_raw'] == \
497 whitelist['api_access_controllers_whitelist']
498 whitelist['api_access_controllers_whitelist']
498
499
499 new_auth_token = AuthTokenModel().create(
500 new_auth_token = AuthTokenModel().create(
500 TEST_USER_ADMIN_LOGIN, 'test')
501 TEST_USER_ADMIN_LOGIN, 'test')
501 Session().commit()
502 Session().commit()
502 # patch the api key and make it expired
503 # patch the api key and make it expired
503 new_auth_token.expires = 0
504 new_auth_token.expires = 0
504 Session().add(new_auth_token)
505 Session().add(new_auth_token)
505 Session().commit()
506 Session().commit()
506 with fixture.anon_access(False):
507 with fixture.anon_access(False):
507 self.app.get(url(controller='changeset',
508 self.app.get(url(controller='changeset',
508 action='changeset_raw',
509 action='changeset_raw',
509 repo_name=HG_REPO, revision='tip',
510 repo_name=HG_REPO, revision='tip',
510 api_key=new_auth_token.api_key),
511 api_key=new_auth_token.api_key),
511 status=302)
512 status=302)
@@ -1,1090 +1,1090 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification)
29 PullRequest, ChangesetStatus, UserLog, Notification)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 from rhodecode.tests.utils import AssertResponse
36 from rhodecode.tests.utils import AssertResponse
37
37
38
38
39 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.usefixtures('app', 'autologin_user')
40 @pytest.mark.backends("git", "hg")
40 @pytest.mark.backends("git", "hg")
41 class TestPullrequestsController:
41 class TestPullrequestsController:
42
42
43 def test_index(self, backend):
43 def test_index(self, backend):
44 self.app.get(url(
44 self.app.get(url(
45 controller='pullrequests', action='index',
45 controller='pullrequests', action='index',
46 repo_name=backend.repo_name))
46 repo_name=backend.repo_name))
47
47
48 def test_option_menu_create_pull_request_exists(self, backend):
48 def test_option_menu_create_pull_request_exists(self, backend):
49 repo_name = backend.repo_name
49 repo_name = backend.repo_name
50 response = self.app.get(url('summary_home', repo_name=repo_name))
50 response = self.app.get(url('summary_home', repo_name=repo_name))
51
51
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
53 'pullrequest', repo_name=repo_name)
53 'pullrequest', repo_name=repo_name)
54 response.mustcontain(create_pr_link)
54 response.mustcontain(create_pr_link)
55
55
56 def test_create_pr_form_with_raw_commit_id(self, backend):
56 def test_create_pr_form_with_raw_commit_id(self, backend):
57 repo = backend.repo
57 repo = backend.repo
58
58
59 self.app.get(
59 self.app.get(
60 url(controller='pullrequests', action='index',
60 url(controller='pullrequests', action='index',
61 repo_name=repo.repo_name,
61 repo_name=repo.repo_name,
62 commit=repo.get_commit().raw_id),
62 commit=repo.get_commit().raw_id),
63 status=200)
63 status=200)
64
64
65 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
65 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
66 def test_show(self, pr_util, pr_merge_enabled):
66 def test_show(self, pr_util, pr_merge_enabled):
67 pull_request = pr_util.create_pull_request(
67 pull_request = pr_util.create_pull_request(
68 mergeable=pr_merge_enabled, enable_notifications=False)
68 mergeable=pr_merge_enabled, enable_notifications=False)
69
69
70 response = self.app.get(url(
70 response = self.app.get(url(
71 controller='pullrequests', action='show',
71 controller='pullrequests', action='show',
72 repo_name=pull_request.target_repo.scm_instance().name,
72 repo_name=pull_request.target_repo.scm_instance().name,
73 pull_request_id=str(pull_request.pull_request_id)))
73 pull_request_id=str(pull_request.pull_request_id)))
74
74
75 for commit_id in pull_request.revisions:
75 for commit_id in pull_request.revisions:
76 response.mustcontain(commit_id)
76 response.mustcontain(commit_id)
77
77
78 assert pull_request.target_ref_parts.type in response
78 assert pull_request.target_ref_parts.type in response
79 assert pull_request.target_ref_parts.name in response
79 assert pull_request.target_ref_parts.name in response
80 target_clone_url = pull_request.target_repo.clone_url()
80 target_clone_url = pull_request.target_repo.clone_url()
81 assert target_clone_url in response
81 assert target_clone_url in response
82
82
83 assert 'class="pull-request-merge"' in response
83 assert 'class="pull-request-merge"' in response
84 assert (
84 assert (
85 'Server-side pull request merging is disabled.'
85 'Server-side pull request merging is disabled.'
86 in response) != pr_merge_enabled
86 in response) != pr_merge_enabled
87
87
88 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
88 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
89 from rhodecode.tests.functional.test_login import login_url, logut_url
89 from rhodecode.tests.functional.test_login import login_url, logut_url
90 # Logout
90 # Logout
91 response = self.app.post(
91 response = self.app.post(
92 logut_url,
92 logut_url,
93 params={'csrf_token': csrf_token})
93 params={'csrf_token': csrf_token})
94 # Login as regular user
94 # Login as regular user
95 response = self.app.post(login_url,
95 response = self.app.post(login_url,
96 {'username': TEST_USER_REGULAR_LOGIN,
96 {'username': TEST_USER_REGULAR_LOGIN,
97 'password': 'test12'})
97 'password': 'test12'})
98
98
99 pull_request = pr_util.create_pull_request(
99 pull_request = pr_util.create_pull_request(
100 author=TEST_USER_REGULAR_LOGIN)
100 author=TEST_USER_REGULAR_LOGIN)
101
101
102 response = self.app.get(url(
102 response = self.app.get(url(
103 controller='pullrequests', action='show',
103 controller='pullrequests', action='show',
104 repo_name=pull_request.target_repo.scm_instance().name,
104 repo_name=pull_request.target_repo.scm_instance().name,
105 pull_request_id=str(pull_request.pull_request_id)))
105 pull_request_id=str(pull_request.pull_request_id)))
106
106
107 response.mustcontain('Server-side pull request merging is disabled.')
107 response.mustcontain('Server-side pull request merging is disabled.')
108
108
109 assert_response = response.assert_response()
109 assert_response = response.assert_response()
110 # for regular user without a merge permissions, we don't see it
110 # for regular user without a merge permissions, we don't see it
111 assert_response.no_element_exists('#close-pull-request-action')
111 assert_response.no_element_exists('#close-pull-request-action')
112
112
113 user_util.grant_user_permission_to_repo(
113 user_util.grant_user_permission_to_repo(
114 pull_request.target_repo,
114 pull_request.target_repo,
115 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
115 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
116 'repository.write')
116 'repository.write')
117 response = self.app.get(url(
117 response = self.app.get(url(
118 controller='pullrequests', action='show',
118 controller='pullrequests', action='show',
119 repo_name=pull_request.target_repo.scm_instance().name,
119 repo_name=pull_request.target_repo.scm_instance().name,
120 pull_request_id=str(pull_request.pull_request_id)))
120 pull_request_id=str(pull_request.pull_request_id)))
121
121
122 response.mustcontain('Server-side pull request merging is disabled.')
122 response.mustcontain('Server-side pull request merging is disabled.')
123
123
124 assert_response = response.assert_response()
124 assert_response = response.assert_response()
125 # now regular user has a merge permissions, we have CLOSE button
125 # now regular user has a merge permissions, we have CLOSE button
126 assert_response.one_element_exists('#close-pull-request-action')
126 assert_response.one_element_exists('#close-pull-request-action')
127
127
128 def test_show_invalid_commit_id(self, pr_util):
128 def test_show_invalid_commit_id(self, pr_util):
129 # Simulating invalid revisions which will cause a lookup error
129 # Simulating invalid revisions which will cause a lookup error
130 pull_request = pr_util.create_pull_request()
130 pull_request = pr_util.create_pull_request()
131 pull_request.revisions = ['invalid']
131 pull_request.revisions = ['invalid']
132 Session().add(pull_request)
132 Session().add(pull_request)
133 Session().commit()
133 Session().commit()
134
134
135 response = self.app.get(url(
135 response = self.app.get(url(
136 controller='pullrequests', action='show',
136 controller='pullrequests', action='show',
137 repo_name=pull_request.target_repo.scm_instance().name,
137 repo_name=pull_request.target_repo.scm_instance().name,
138 pull_request_id=str(pull_request.pull_request_id)))
138 pull_request_id=str(pull_request.pull_request_id)))
139
139
140 for commit_id in pull_request.revisions:
140 for commit_id in pull_request.revisions:
141 response.mustcontain(commit_id)
141 response.mustcontain(commit_id)
142
142
143 def test_show_invalid_source_reference(self, pr_util):
143 def test_show_invalid_source_reference(self, pr_util):
144 pull_request = pr_util.create_pull_request()
144 pull_request = pr_util.create_pull_request()
145 pull_request.source_ref = 'branch:b:invalid'
145 pull_request.source_ref = 'branch:b:invalid'
146 Session().add(pull_request)
146 Session().add(pull_request)
147 Session().commit()
147 Session().commit()
148
148
149 self.app.get(url(
149 self.app.get(url(
150 controller='pullrequests', action='show',
150 controller='pullrequests', action='show',
151 repo_name=pull_request.target_repo.scm_instance().name,
151 repo_name=pull_request.target_repo.scm_instance().name,
152 pull_request_id=str(pull_request.pull_request_id)))
152 pull_request_id=str(pull_request.pull_request_id)))
153
153
154 def test_edit_title_description(self, pr_util, csrf_token):
154 def test_edit_title_description(self, pr_util, csrf_token):
155 pull_request = pr_util.create_pull_request()
155 pull_request = pr_util.create_pull_request()
156 pull_request_id = pull_request.pull_request_id
156 pull_request_id = pull_request.pull_request_id
157
157
158 response = self.app.post(
158 response = self.app.post(
159 url(controller='pullrequests', action='update',
159 url(controller='pullrequests', action='update',
160 repo_name=pull_request.target_repo.repo_name,
160 repo_name=pull_request.target_repo.repo_name,
161 pull_request_id=str(pull_request_id)),
161 pull_request_id=str(pull_request_id)),
162 params={
162 params={
163 'edit_pull_request': 'true',
163 'edit_pull_request': 'true',
164 '_method': 'put',
164 '_method': 'put',
165 'title': 'New title',
165 'title': 'New title',
166 'description': 'New description',
166 'description': 'New description',
167 'csrf_token': csrf_token})
167 'csrf_token': csrf_token})
168
168
169 assert_session_flash(
169 assert_session_flash(
170 response, u'Pull request title & description updated.',
170 response, u'Pull request title & description updated.',
171 category='success')
171 category='success')
172
172
173 pull_request = PullRequest.get(pull_request_id)
173 pull_request = PullRequest.get(pull_request_id)
174 assert pull_request.title == 'New title'
174 assert pull_request.title == 'New title'
175 assert pull_request.description == 'New description'
175 assert pull_request.description == 'New description'
176
176
177 def test_edit_title_description_closed(self, pr_util, csrf_token):
177 def test_edit_title_description_closed(self, pr_util, csrf_token):
178 pull_request = pr_util.create_pull_request()
178 pull_request = pr_util.create_pull_request()
179 pull_request_id = pull_request.pull_request_id
179 pull_request_id = pull_request.pull_request_id
180 pr_util.close()
180 pr_util.close()
181
181
182 response = self.app.post(
182 response = self.app.post(
183 url(controller='pullrequests', action='update',
183 url(controller='pullrequests', action='update',
184 repo_name=pull_request.target_repo.repo_name,
184 repo_name=pull_request.target_repo.repo_name,
185 pull_request_id=str(pull_request_id)),
185 pull_request_id=str(pull_request_id)),
186 params={
186 params={
187 'edit_pull_request': 'true',
187 'edit_pull_request': 'true',
188 '_method': 'put',
188 '_method': 'put',
189 'title': 'New title',
189 'title': 'New title',
190 'description': 'New description',
190 'description': 'New description',
191 'csrf_token': csrf_token})
191 'csrf_token': csrf_token})
192
192
193 assert_session_flash(
193 assert_session_flash(
194 response, u'Cannot update closed pull requests.',
194 response, u'Cannot update closed pull requests.',
195 category='error')
195 category='error')
196
196
197 def test_update_invalid_source_reference(self, pr_util, csrf_token):
197 def test_update_invalid_source_reference(self, pr_util, csrf_token):
198 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
198 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
199
199
200 pull_request = pr_util.create_pull_request()
200 pull_request = pr_util.create_pull_request()
201 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
201 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
202 Session().add(pull_request)
202 Session().add(pull_request)
203 Session().commit()
203 Session().commit()
204
204
205 pull_request_id = pull_request.pull_request_id
205 pull_request_id = pull_request.pull_request_id
206
206
207 response = self.app.post(
207 response = self.app.post(
208 url(controller='pullrequests', action='update',
208 url(controller='pullrequests', action='update',
209 repo_name=pull_request.target_repo.repo_name,
209 repo_name=pull_request.target_repo.repo_name,
210 pull_request_id=str(pull_request_id)),
210 pull_request_id=str(pull_request_id)),
211 params={'update_commits': 'true', '_method': 'put',
211 params={'update_commits': 'true', '_method': 'put',
212 'csrf_token': csrf_token})
212 'csrf_token': csrf_token})
213
213
214 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
214 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
215 UpdateFailureReason.MISSING_SOURCE_REF]
215 UpdateFailureReason.MISSING_SOURCE_REF]
216 assert_session_flash(response, expected_msg, category='error')
216 assert_session_flash(response, expected_msg, category='error')
217
217
218 def test_missing_target_reference(self, pr_util, csrf_token):
218 def test_missing_target_reference(self, pr_util, csrf_token):
219 from rhodecode.lib.vcs.backends.base import MergeFailureReason
219 from rhodecode.lib.vcs.backends.base import MergeFailureReason
220 pull_request = pr_util.create_pull_request(
220 pull_request = pr_util.create_pull_request(
221 approved=True, mergeable=True)
221 approved=True, mergeable=True)
222 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
222 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
223 Session().add(pull_request)
223 Session().add(pull_request)
224 Session().commit()
224 Session().commit()
225
225
226 pull_request_id = pull_request.pull_request_id
226 pull_request_id = pull_request.pull_request_id
227 pull_request_url = url(
227 pull_request_url = url(
228 controller='pullrequests', action='show',
228 controller='pullrequests', action='show',
229 repo_name=pull_request.target_repo.repo_name,
229 repo_name=pull_request.target_repo.repo_name,
230 pull_request_id=str(pull_request_id))
230 pull_request_id=str(pull_request_id))
231
231
232 response = self.app.get(pull_request_url)
232 response = self.app.get(pull_request_url)
233
233
234 assertr = AssertResponse(response)
234 assertr = AssertResponse(response)
235 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
235 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
236 MergeFailureReason.MISSING_TARGET_REF]
236 MergeFailureReason.MISSING_TARGET_REF]
237 assertr.element_contains(
237 assertr.element_contains(
238 'span[data-role="merge-message"]', str(expected_msg))
238 'span[data-role="merge-message"]', str(expected_msg))
239
239
240 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
240 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
241 pull_request = pr_util.create_pull_request(approved=True)
241 pull_request = pr_util.create_pull_request(approved=True)
242 pull_request_id = pull_request.pull_request_id
242 pull_request_id = pull_request.pull_request_id
243 author = pull_request.user_id
243 author = pull_request.user_id
244 repo = pull_request.target_repo.repo_id
244 repo = pull_request.target_repo.repo_id
245
245
246 self.app.post(
246 self.app.post(
247 url(controller='pullrequests',
247 url(controller='pullrequests',
248 action='comment',
248 action='comment',
249 repo_name=pull_request.target_repo.scm_instance().name,
249 repo_name=pull_request.target_repo.scm_instance().name,
250 pull_request_id=str(pull_request_id)),
250 pull_request_id=str(pull_request_id)),
251 params={
251 params={
252 'changeset_status': ChangesetStatus.STATUS_APPROVED,
252 'changeset_status': ChangesetStatus.STATUS_APPROVED,
253 'close_pull_request': '1',
253 'close_pull_request': '1',
254 'text': 'Closing a PR',
254 'text': 'Closing a PR',
255 'csrf_token': csrf_token},
255 'csrf_token': csrf_token},
256 status=302)
256 status=302)
257
257
258 action = 'user_closed_pull_request:%d' % pull_request_id
258 action = 'user_closed_pull_request:%d' % pull_request_id
259 journal = UserLog.query()\
259 journal = UserLog.query()\
260 .filter(UserLog.user_id == author)\
260 .filter(UserLog.user_id == author)\
261 .filter(UserLog.repository_id == repo)\
261 .filter(UserLog.repository_id == repo)\
262 .filter(UserLog.action == action)\
262 .filter(UserLog.action == action)\
263 .all()
263 .all()
264 assert len(journal) == 1
264 assert len(journal) == 1
265
265
266 pull_request = PullRequest.get(pull_request_id)
266 pull_request = PullRequest.get(pull_request_id)
267 assert pull_request.is_closed()
267 assert pull_request.is_closed()
268
268
269 # check only the latest status, not the review status
269 # check only the latest status, not the review status
270 status = ChangesetStatusModel().get_status(
270 status = ChangesetStatusModel().get_status(
271 pull_request.source_repo, pull_request=pull_request)
271 pull_request.source_repo, pull_request=pull_request)
272 assert status == ChangesetStatus.STATUS_APPROVED
272 assert status == ChangesetStatus.STATUS_APPROVED
273
273
274 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
274 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
275 pull_request = pr_util.create_pull_request()
275 pull_request = pr_util.create_pull_request()
276 pull_request_id = pull_request.pull_request_id
276 pull_request_id = pull_request.pull_request_id
277 response = self.app.post(
277 response = self.app.post(
278 url(controller='pullrequests',
278 url(controller='pullrequests',
279 action='update',
279 action='update',
280 repo_name=pull_request.target_repo.scm_instance().name,
280 repo_name=pull_request.target_repo.scm_instance().name,
281 pull_request_id=str(pull_request.pull_request_id)),
281 pull_request_id=str(pull_request.pull_request_id)),
282 params={'close_pull_request': 'true', '_method': 'put',
282 params={'close_pull_request': 'true', '_method': 'put',
283 'csrf_token': csrf_token})
283 'csrf_token': csrf_token})
284
284
285 pull_request = PullRequest.get(pull_request_id)
285 pull_request = PullRequest.get(pull_request_id)
286
286
287 assert response.json is True
287 assert response.json is True
288 assert pull_request.is_closed()
288 assert pull_request.is_closed()
289
289
290 # check only the latest status, not the review status
290 # check only the latest status, not the review status
291 status = ChangesetStatusModel().get_status(
291 status = ChangesetStatusModel().get_status(
292 pull_request.source_repo, pull_request=pull_request)
292 pull_request.source_repo, pull_request=pull_request)
293 assert status == ChangesetStatus.STATUS_REJECTED
293 assert status == ChangesetStatus.STATUS_REJECTED
294
294
295 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
295 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
296 pull_request = pr_util.create_pull_request()
296 pull_request = pr_util.create_pull_request()
297 pull_request_id = pull_request.pull_request_id
297 pull_request_id = pull_request.pull_request_id
298 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
298 PullRequestModel().update_reviewers(
299 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
299 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)])
300 author = pull_request.user_id
300 author = pull_request.user_id
301 repo = pull_request.target_repo.repo_id
301 repo = pull_request.target_repo.repo_id
302 self.app.post(
302 self.app.post(
303 url(controller='pullrequests',
303 url(controller='pullrequests',
304 action='comment',
304 action='comment',
305 repo_name=pull_request.target_repo.scm_instance().name,
305 repo_name=pull_request.target_repo.scm_instance().name,
306 pull_request_id=str(pull_request_id)),
306 pull_request_id=str(pull_request_id)),
307 params={
307 params={
308 'changeset_status': 'rejected',
308 'changeset_status': 'rejected',
309 'close_pull_request': '1',
309 'close_pull_request': '1',
310 'csrf_token': csrf_token},
310 'csrf_token': csrf_token},
311 status=302)
311 status=302)
312
312
313 pull_request = PullRequest.get(pull_request_id)
313 pull_request = PullRequest.get(pull_request_id)
314
314
315 action = 'user_closed_pull_request:%d' % pull_request_id
315 action = 'user_closed_pull_request:%d' % pull_request_id
316 journal = UserLog.query().filter(
316 journal = UserLog.query().filter(
317 UserLog.user_id == author,
317 UserLog.user_id == author,
318 UserLog.repository_id == repo,
318 UserLog.repository_id == repo,
319 UserLog.action == action).all()
319 UserLog.action == action).all()
320 assert len(journal) == 1
320 assert len(journal) == 1
321
321
322 # check only the latest status, not the review status
322 # check only the latest status, not the review status
323 status = ChangesetStatusModel().get_status(
323 status = ChangesetStatusModel().get_status(
324 pull_request.source_repo, pull_request=pull_request)
324 pull_request.source_repo, pull_request=pull_request)
325 assert status == ChangesetStatus.STATUS_REJECTED
325 assert status == ChangesetStatus.STATUS_REJECTED
326
326
327 def test_create_pull_request(self, backend, csrf_token):
327 def test_create_pull_request(self, backend, csrf_token):
328 commits = [
328 commits = [
329 {'message': 'ancestor'},
329 {'message': 'ancestor'},
330 {'message': 'change'},
330 {'message': 'change'},
331 {'message': 'change2'},
331 {'message': 'change2'},
332 ]
332 ]
333 commit_ids = backend.create_master_repo(commits)
333 commit_ids = backend.create_master_repo(commits)
334 target = backend.create_repo(heads=['ancestor'])
334 target = backend.create_repo(heads=['ancestor'])
335 source = backend.create_repo(heads=['change2'])
335 source = backend.create_repo(heads=['change2'])
336
336
337 response = self.app.post(
337 response = self.app.post(
338 url(
338 url(
339 controller='pullrequests',
339 controller='pullrequests',
340 action='create',
340 action='create',
341 repo_name=source.repo_name
341 repo_name=source.repo_name
342 ),
342 ),
343 [
343 [
344 ('source_repo', source.repo_name),
344 ('source_repo', source.repo_name),
345 ('source_ref', 'branch:default:' + commit_ids['change2']),
345 ('source_ref', 'branch:default:' + commit_ids['change2']),
346 ('target_repo', target.repo_name),
346 ('target_repo', target.repo_name),
347 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
347 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
348 ('common_ancestor', commit_ids['ancestor']),
348 ('pullrequest_desc', 'Description'),
349 ('pullrequest_desc', 'Description'),
349 ('pullrequest_title', 'Title'),
350 ('pullrequest_title', 'Title'),
350 ('__start__', 'review_members:sequence'),
351 ('__start__', 'review_members:sequence'),
351 ('__start__', 'reviewer:mapping'),
352 ('__start__', 'reviewer:mapping'),
352 ('user_id', '1'),
353 ('user_id', '1'),
353 ('__start__', 'reasons:sequence'),
354 ('__start__', 'reasons:sequence'),
354 ('reason', 'Some reason'),
355 ('reason', 'Some reason'),
355 ('__end__', 'reasons:sequence'),
356 ('__end__', 'reasons:sequence'),
357 ('mandatory', 'False'),
356 ('__end__', 'reviewer:mapping'),
358 ('__end__', 'reviewer:mapping'),
357 ('__end__', 'review_members:sequence'),
359 ('__end__', 'review_members:sequence'),
358 ('__start__', 'revisions:sequence'),
360 ('__start__', 'revisions:sequence'),
359 ('revisions', commit_ids['change']),
361 ('revisions', commit_ids['change']),
360 ('revisions', commit_ids['change2']),
362 ('revisions', commit_ids['change2']),
361 ('__end__', 'revisions:sequence'),
363 ('__end__', 'revisions:sequence'),
362 ('user', ''),
364 ('user', ''),
363 ('csrf_token', csrf_token),
365 ('csrf_token', csrf_token),
364 ],
366 ],
365 status=302)
367 status=302)
366
368
367 location = response.headers['Location']
369 location = response.headers['Location']
368 pull_request_id = int(location.rsplit('/', 1)[1])
370 pull_request_id = location.rsplit('/', 1)[1]
369 pull_request = PullRequest.get(pull_request_id)
371 assert pull_request_id != 'new'
372 pull_request = PullRequest.get(int(pull_request_id))
370
373
371 # check that we have now both revisions
374 # check that we have now both revisions
372 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
375 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
373 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
376 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
374 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
377 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
375 assert pull_request.target_ref == expected_target_ref
378 assert pull_request.target_ref == expected_target_ref
376
379
377 def test_reviewer_notifications(self, backend, csrf_token):
380 def test_reviewer_notifications(self, backend, csrf_token):
378 # We have to use the app.post for this test so it will create the
381 # We have to use the app.post for this test so it will create the
379 # notifications properly with the new PR
382 # notifications properly with the new PR
380 commits = [
383 commits = [
381 {'message': 'ancestor',
384 {'message': 'ancestor',
382 'added': [FileNode('file_A', content='content_of_ancestor')]},
385 'added': [FileNode('file_A', content='content_of_ancestor')]},
383 {'message': 'change',
386 {'message': 'change',
384 'added': [FileNode('file_a', content='content_of_change')]},
387 'added': [FileNode('file_a', content='content_of_change')]},
385 {'message': 'change-child'},
388 {'message': 'change-child'},
386 {'message': 'ancestor-child', 'parents': ['ancestor'],
389 {'message': 'ancestor-child', 'parents': ['ancestor'],
387 'added': [
390 'added': [
388 FileNode('file_B', content='content_of_ancestor_child')]},
391 FileNode('file_B', content='content_of_ancestor_child')]},
389 {'message': 'ancestor-child-2'},
392 {'message': 'ancestor-child-2'},
390 ]
393 ]
391 commit_ids = backend.create_master_repo(commits)
394 commit_ids = backend.create_master_repo(commits)
392 target = backend.create_repo(heads=['ancestor-child'])
395 target = backend.create_repo(heads=['ancestor-child'])
393 source = backend.create_repo(heads=['change'])
396 source = backend.create_repo(heads=['change'])
394
397
395 response = self.app.post(
398 response = self.app.post(
396 url(
399 url(
397 controller='pullrequests',
400 controller='pullrequests',
398 action='create',
401 action='create',
399 repo_name=source.repo_name
402 repo_name=source.repo_name
400 ),
403 ),
401 [
404 [
402 ('source_repo', source.repo_name),
405 ('source_repo', source.repo_name),
403 ('source_ref', 'branch:default:' + commit_ids['change']),
406 ('source_ref', 'branch:default:' + commit_ids['change']),
404 ('target_repo', target.repo_name),
407 ('target_repo', target.repo_name),
405 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
408 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
409 ('common_ancestor', commit_ids['ancestor']),
406 ('pullrequest_desc', 'Description'),
410 ('pullrequest_desc', 'Description'),
407 ('pullrequest_title', 'Title'),
411 ('pullrequest_title', 'Title'),
408 ('__start__', 'review_members:sequence'),
412 ('__start__', 'review_members:sequence'),
409 ('__start__', 'reviewer:mapping'),
413 ('__start__', 'reviewer:mapping'),
410 ('user_id', '2'),
414 ('user_id', '2'),
411 ('__start__', 'reasons:sequence'),
415 ('__start__', 'reasons:sequence'),
412 ('reason', 'Some reason'),
416 ('reason', 'Some reason'),
413 ('__end__', 'reasons:sequence'),
417 ('__end__', 'reasons:sequence'),
418 ('mandatory', 'False'),
414 ('__end__', 'reviewer:mapping'),
419 ('__end__', 'reviewer:mapping'),
415 ('__end__', 'review_members:sequence'),
420 ('__end__', 'review_members:sequence'),
416 ('__start__', 'revisions:sequence'),
421 ('__start__', 'revisions:sequence'),
417 ('revisions', commit_ids['change']),
422 ('revisions', commit_ids['change']),
418 ('__end__', 'revisions:sequence'),
423 ('__end__', 'revisions:sequence'),
419 ('user', ''),
424 ('user', ''),
420 ('csrf_token', csrf_token),
425 ('csrf_token', csrf_token),
421 ],
426 ],
422 status=302)
427 status=302)
423
428
424 location = response.headers['Location']
429 location = response.headers['Location']
425 pull_request_id = int(location.rsplit('/', 1)[1])
430
426 pull_request = PullRequest.get(pull_request_id)
431 pull_request_id = location.rsplit('/', 1)[1]
432 assert pull_request_id != 'new'
433 pull_request = PullRequest.get(int(pull_request_id))
427
434
428 # Check that a notification was made
435 # Check that a notification was made
429 notifications = Notification.query()\
436 notifications = Notification.query()\
430 .filter(Notification.created_by == pull_request.author.user_id,
437 .filter(Notification.created_by == pull_request.author.user_id,
431 Notification.type_ == Notification.TYPE_PULL_REQUEST,
438 Notification.type_ == Notification.TYPE_PULL_REQUEST,
432 Notification.subject.contains("wants you to review "
439 Notification.subject.contains(
433 "pull request #%d"
440 "wants you to review pull request #%s" % pull_request_id))
434 % pull_request_id))
435 assert len(notifications.all()) == 1
441 assert len(notifications.all()) == 1
436
442
437 # Change reviewers and check that a notification was made
443 # Change reviewers and check that a notification was made
438 PullRequestModel().update_reviewers(
444 PullRequestModel().update_reviewers(
439 pull_request.pull_request_id, [(1, [])])
445 pull_request.pull_request_id, [(1, [], False)])
440 assert len(notifications.all()) == 2
446 assert len(notifications.all()) == 2
441
447
442 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
448 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
443 csrf_token):
449 csrf_token):
444 commits = [
450 commits = [
445 {'message': 'ancestor',
451 {'message': 'ancestor',
446 'added': [FileNode('file_A', content='content_of_ancestor')]},
452 'added': [FileNode('file_A', content='content_of_ancestor')]},
447 {'message': 'change',
453 {'message': 'change',
448 'added': [FileNode('file_a', content='content_of_change')]},
454 'added': [FileNode('file_a', content='content_of_change')]},
449 {'message': 'change-child'},
455 {'message': 'change-child'},
450 {'message': 'ancestor-child', 'parents': ['ancestor'],
456 {'message': 'ancestor-child', 'parents': ['ancestor'],
451 'added': [
457 'added': [
452 FileNode('file_B', content='content_of_ancestor_child')]},
458 FileNode('file_B', content='content_of_ancestor_child')]},
453 {'message': 'ancestor-child-2'},
459 {'message': 'ancestor-child-2'},
454 ]
460 ]
455 commit_ids = backend.create_master_repo(commits)
461 commit_ids = backend.create_master_repo(commits)
456 target = backend.create_repo(heads=['ancestor-child'])
462 target = backend.create_repo(heads=['ancestor-child'])
457 source = backend.create_repo(heads=['change'])
463 source = backend.create_repo(heads=['change'])
458
464
459 response = self.app.post(
465 response = self.app.post(
460 url(
466 url(
461 controller='pullrequests',
467 controller='pullrequests',
462 action='create',
468 action='create',
463 repo_name=source.repo_name
469 repo_name=source.repo_name
464 ),
470 ),
465 [
471 [
466 ('source_repo', source.repo_name),
472 ('source_repo', source.repo_name),
467 ('source_ref', 'branch:default:' + commit_ids['change']),
473 ('source_ref', 'branch:default:' + commit_ids['change']),
468 ('target_repo', target.repo_name),
474 ('target_repo', target.repo_name),
469 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
475 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
476 ('common_ancestor', commit_ids['ancestor']),
470 ('pullrequest_desc', 'Description'),
477 ('pullrequest_desc', 'Description'),
471 ('pullrequest_title', 'Title'),
478 ('pullrequest_title', 'Title'),
472 ('__start__', 'review_members:sequence'),
479 ('__start__', 'review_members:sequence'),
473 ('__start__', 'reviewer:mapping'),
480 ('__start__', 'reviewer:mapping'),
474 ('user_id', '1'),
481 ('user_id', '1'),
475 ('__start__', 'reasons:sequence'),
482 ('__start__', 'reasons:sequence'),
476 ('reason', 'Some reason'),
483 ('reason', 'Some reason'),
477 ('__end__', 'reasons:sequence'),
484 ('__end__', 'reasons:sequence'),
485 ('mandatory', 'False'),
478 ('__end__', 'reviewer:mapping'),
486 ('__end__', 'reviewer:mapping'),
479 ('__end__', 'review_members:sequence'),
487 ('__end__', 'review_members:sequence'),
480 ('__start__', 'revisions:sequence'),
488 ('__start__', 'revisions:sequence'),
481 ('revisions', commit_ids['change']),
489 ('revisions', commit_ids['change']),
482 ('__end__', 'revisions:sequence'),
490 ('__end__', 'revisions:sequence'),
483 ('user', ''),
491 ('user', ''),
484 ('csrf_token', csrf_token),
492 ('csrf_token', csrf_token),
485 ],
493 ],
486 status=302)
494 status=302)
487
495
488 location = response.headers['Location']
496 location = response.headers['Location']
489 pull_request_id = int(location.rsplit('/', 1)[1])
497
490 pull_request = PullRequest.get(pull_request_id)
498 pull_request_id = location.rsplit('/', 1)[1]
499 assert pull_request_id != 'new'
500 pull_request = PullRequest.get(int(pull_request_id))
491
501
492 # target_ref has to point to the ancestor's commit_id in order to
502 # target_ref has to point to the ancestor's commit_id in order to
493 # show the correct diff
503 # show the correct diff
494 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
504 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
495 assert pull_request.target_ref == expected_target_ref
505 assert pull_request.target_ref == expected_target_ref
496
506
497 # Check generated diff contents
507 # Check generated diff contents
498 response = response.follow()
508 response = response.follow()
499 assert 'content_of_ancestor' not in response.body
509 assert 'content_of_ancestor' not in response.body
500 assert 'content_of_ancestor-child' not in response.body
510 assert 'content_of_ancestor-child' not in response.body
501 assert 'content_of_change' in response.body
511 assert 'content_of_change' in response.body
502
512
503 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
513 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
504 # Clear any previous calls to rcextensions
514 # Clear any previous calls to rcextensions
505 rhodecode.EXTENSIONS.calls.clear()
515 rhodecode.EXTENSIONS.calls.clear()
506
516
507 pull_request = pr_util.create_pull_request(
517 pull_request = pr_util.create_pull_request(
508 approved=True, mergeable=True)
518 approved=True, mergeable=True)
509 pull_request_id = pull_request.pull_request_id
519 pull_request_id = pull_request.pull_request_id
510 repo_name = pull_request.target_repo.scm_instance().name,
520 repo_name = pull_request.target_repo.scm_instance().name,
511
521
512 response = self.app.post(
522 response = self.app.post(
513 url(controller='pullrequests',
523 url(controller='pullrequests',
514 action='merge',
524 action='merge',
515 repo_name=str(repo_name[0]),
525 repo_name=str(repo_name[0]),
516 pull_request_id=str(pull_request_id)),
526 pull_request_id=str(pull_request_id)),
517 params={'csrf_token': csrf_token}).follow()
527 params={'csrf_token': csrf_token}).follow()
518
528
519 pull_request = PullRequest.get(pull_request_id)
529 pull_request = PullRequest.get(pull_request_id)
520
530
521 assert response.status_int == 200
531 assert response.status_int == 200
522 assert pull_request.is_closed()
532 assert pull_request.is_closed()
523 assert_pull_request_status(
533 assert_pull_request_status(
524 pull_request, ChangesetStatus.STATUS_APPROVED)
534 pull_request, ChangesetStatus.STATUS_APPROVED)
525
535
526 # Check the relevant log entries were added
536 # Check the relevant log entries were added
527 user_logs = UserLog.query() \
537 user_logs = UserLog.query() \
528 .filter(UserLog.version == UserLog.VERSION_1) \
538 .filter(UserLog.version == UserLog.VERSION_1) \
529 .order_by('-user_log_id').limit(3)
539 .order_by('-user_log_id').limit(3)
530 actions = [log.action for log in user_logs]
540 actions = [log.action for log in user_logs]
531 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
541 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
532 expected_actions = [
542 expected_actions = [
533 u'user_closed_pull_request:%d' % pull_request_id,
543 u'user_closed_pull_request:%d' % pull_request_id,
534 u'user_merged_pull_request:%d' % pull_request_id,
544 u'user_merged_pull_request:%d' % pull_request_id,
535 # The action below reflect that the post push actions were executed
545 # The action below reflect that the post push actions were executed
536 u'user_commented_pull_request:%d' % pull_request_id,
546 u'user_commented_pull_request:%d' % pull_request_id,
537 ]
547 ]
538 assert actions == expected_actions
548 assert actions == expected_actions
539
549
540 user_logs = UserLog.query() \
550 user_logs = UserLog.query() \
541 .filter(UserLog.version == UserLog.VERSION_2) \
551 .filter(UserLog.version == UserLog.VERSION_2) \
542 .order_by('-user_log_id').limit(1)
552 .order_by('-user_log_id').limit(1)
543 actions = [log.action for log in user_logs]
553 actions = [log.action for log in user_logs]
544 assert actions == ['user.push']
554 assert actions == ['user.push']
545 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
555 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
546
556
547 # Check post_push rcextension was really executed
557 # Check post_push rcextension was really executed
548 push_calls = rhodecode.EXTENSIONS.calls['post_push']
558 push_calls = rhodecode.EXTENSIONS.calls['post_push']
549 assert len(push_calls) == 1
559 assert len(push_calls) == 1
550 unused_last_call_args, last_call_kwargs = push_calls[0]
560 unused_last_call_args, last_call_kwargs = push_calls[0]
551 assert last_call_kwargs['action'] == 'push'
561 assert last_call_kwargs['action'] == 'push'
552 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
562 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
553
563
554 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
564 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
555 pull_request = pr_util.create_pull_request(mergeable=False)
565 pull_request = pr_util.create_pull_request(mergeable=False)
556 pull_request_id = pull_request.pull_request_id
566 pull_request_id = pull_request.pull_request_id
557 pull_request = PullRequest.get(pull_request_id)
567 pull_request = PullRequest.get(pull_request_id)
558
568
559 response = self.app.post(
569 response = self.app.post(
560 url(controller='pullrequests',
570 url(controller='pullrequests',
561 action='merge',
571 action='merge',
562 repo_name=pull_request.target_repo.scm_instance().name,
572 repo_name=pull_request.target_repo.scm_instance().name,
563 pull_request_id=str(pull_request.pull_request_id)),
573 pull_request_id=str(pull_request.pull_request_id)),
564 params={'csrf_token': csrf_token}).follow()
574 params={'csrf_token': csrf_token}).follow()
565
575
566 assert response.status_int == 200
576 assert response.status_int == 200
567 response.mustcontain(
577 response.mustcontain(
568 'Merge is not currently possible because of below failed checks.')
578 'Merge is not currently possible because of below failed checks.')
569 response.mustcontain('Server-side pull request merging is disabled.')
579 response.mustcontain('Server-side pull request merging is disabled.')
570
580
571 @pytest.mark.skip_backends('svn')
581 @pytest.mark.skip_backends('svn')
572 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
582 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
573 pull_request = pr_util.create_pull_request(mergeable=True)
583 pull_request = pr_util.create_pull_request(mergeable=True)
574 pull_request_id = pull_request.pull_request_id
584 pull_request_id = pull_request.pull_request_id
575 repo_name = pull_request.target_repo.scm_instance().name,
585 repo_name = pull_request.target_repo.scm_instance().name,
576
586
577 response = self.app.post(
587 response = self.app.post(
578 url(controller='pullrequests',
588 url(controller='pullrequests',
579 action='merge',
589 action='merge',
580 repo_name=str(repo_name[0]),
590 repo_name=str(repo_name[0]),
581 pull_request_id=str(pull_request_id)),
591 pull_request_id=str(pull_request_id)),
582 params={'csrf_token': csrf_token}).follow()
592 params={'csrf_token': csrf_token}).follow()
583
593
584 assert response.status_int == 200
594 assert response.status_int == 200
585
595
586 response.mustcontain(
596 response.mustcontain(
587 'Merge is not currently possible because of below failed checks.')
597 'Merge is not currently possible because of below failed checks.')
588 response.mustcontain('Pull request reviewer approval is pending.')
598 response.mustcontain('Pull request reviewer approval is pending.')
589
599
590 def test_update_source_revision(self, backend, csrf_token):
600 def test_update_source_revision(self, backend, csrf_token):
591 commits = [
601 commits = [
592 {'message': 'ancestor'},
602 {'message': 'ancestor'},
593 {'message': 'change'},
603 {'message': 'change'},
594 {'message': 'change-2'},
604 {'message': 'change-2'},
595 ]
605 ]
596 commit_ids = backend.create_master_repo(commits)
606 commit_ids = backend.create_master_repo(commits)
597 target = backend.create_repo(heads=['ancestor'])
607 target = backend.create_repo(heads=['ancestor'])
598 source = backend.create_repo(heads=['change'])
608 source = backend.create_repo(heads=['change'])
599
609
600 # create pr from a in source to A in target
610 # create pr from a in source to A in target
601 pull_request = PullRequest()
611 pull_request = PullRequest()
602 pull_request.source_repo = source
612 pull_request.source_repo = source
603 # TODO: johbo: Make sure that we write the source ref this way!
613 # TODO: johbo: Make sure that we write the source ref this way!
604 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
614 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
605 branch=backend.default_branch_name, commit_id=commit_ids['change'])
615 branch=backend.default_branch_name, commit_id=commit_ids['change'])
606 pull_request.target_repo = target
616 pull_request.target_repo = target
607
617
608 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
618 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
609 branch=backend.default_branch_name,
619 branch=backend.default_branch_name,
610 commit_id=commit_ids['ancestor'])
620 commit_id=commit_ids['ancestor'])
611 pull_request.revisions = [commit_ids['change']]
621 pull_request.revisions = [commit_ids['change']]
612 pull_request.title = u"Test"
622 pull_request.title = u"Test"
613 pull_request.description = u"Description"
623 pull_request.description = u"Description"
614 pull_request.author = UserModel().get_by_username(
624 pull_request.author = UserModel().get_by_username(
615 TEST_USER_ADMIN_LOGIN)
625 TEST_USER_ADMIN_LOGIN)
616 Session().add(pull_request)
626 Session().add(pull_request)
617 Session().commit()
627 Session().commit()
618 pull_request_id = pull_request.pull_request_id
628 pull_request_id = pull_request.pull_request_id
619
629
620 # source has ancestor - change - change-2
630 # source has ancestor - change - change-2
621 backend.pull_heads(source, heads=['change-2'])
631 backend.pull_heads(source, heads=['change-2'])
622
632
623 # update PR
633 # update PR
624 self.app.post(
634 self.app.post(
625 url(controller='pullrequests', action='update',
635 url(controller='pullrequests', action='update',
626 repo_name=target.repo_name,
636 repo_name=target.repo_name,
627 pull_request_id=str(pull_request_id)),
637 pull_request_id=str(pull_request_id)),
628 params={'update_commits': 'true', '_method': 'put',
638 params={'update_commits': 'true', '_method': 'put',
629 'csrf_token': csrf_token})
639 'csrf_token': csrf_token})
630
640
631 # check that we have now both revisions
641 # check that we have now both revisions
632 pull_request = PullRequest.get(pull_request_id)
642 pull_request = PullRequest.get(pull_request_id)
633 assert pull_request.revisions == [
643 assert pull_request.revisions == [
634 commit_ids['change-2'], commit_ids['change']]
644 commit_ids['change-2'], commit_ids['change']]
635
645
636 # TODO: johbo: this should be a test on its own
646 # TODO: johbo: this should be a test on its own
637 response = self.app.get(url(
647 response = self.app.get(url(
638 controller='pullrequests', action='index',
648 controller='pullrequests', action='index',
639 repo_name=target.repo_name))
649 repo_name=target.repo_name))
640 assert response.status_int == 200
650 assert response.status_int == 200
641 assert 'Pull request updated to' in response.body
651 assert 'Pull request updated to' in response.body
642 assert 'with 1 added, 0 removed commits.' in response.body
652 assert 'with 1 added, 0 removed commits.' in response.body
643
653
644 def test_update_target_revision(self, backend, csrf_token):
654 def test_update_target_revision(self, backend, csrf_token):
645 commits = [
655 commits = [
646 {'message': 'ancestor'},
656 {'message': 'ancestor'},
647 {'message': 'change'},
657 {'message': 'change'},
648 {'message': 'ancestor-new', 'parents': ['ancestor']},
658 {'message': 'ancestor-new', 'parents': ['ancestor']},
649 {'message': 'change-rebased'},
659 {'message': 'change-rebased'},
650 ]
660 ]
651 commit_ids = backend.create_master_repo(commits)
661 commit_ids = backend.create_master_repo(commits)
652 target = backend.create_repo(heads=['ancestor'])
662 target = backend.create_repo(heads=['ancestor'])
653 source = backend.create_repo(heads=['change'])
663 source = backend.create_repo(heads=['change'])
654
664
655 # create pr from a in source to A in target
665 # create pr from a in source to A in target
656 pull_request = PullRequest()
666 pull_request = PullRequest()
657 pull_request.source_repo = source
667 pull_request.source_repo = source
658 # TODO: johbo: Make sure that we write the source ref this way!
668 # TODO: johbo: Make sure that we write the source ref this way!
659 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
669 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
660 branch=backend.default_branch_name, commit_id=commit_ids['change'])
670 branch=backend.default_branch_name, commit_id=commit_ids['change'])
661 pull_request.target_repo = target
671 pull_request.target_repo = target
662 # TODO: johbo: Target ref should be branch based, since tip can jump
672 # TODO: johbo: Target ref should be branch based, since tip can jump
663 # from branch to branch
673 # from branch to branch
664 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
674 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
665 branch=backend.default_branch_name,
675 branch=backend.default_branch_name,
666 commit_id=commit_ids['ancestor'])
676 commit_id=commit_ids['ancestor'])
667 pull_request.revisions = [commit_ids['change']]
677 pull_request.revisions = [commit_ids['change']]
668 pull_request.title = u"Test"
678 pull_request.title = u"Test"
669 pull_request.description = u"Description"
679 pull_request.description = u"Description"
670 pull_request.author = UserModel().get_by_username(
680 pull_request.author = UserModel().get_by_username(
671 TEST_USER_ADMIN_LOGIN)
681 TEST_USER_ADMIN_LOGIN)
672 Session().add(pull_request)
682 Session().add(pull_request)
673 Session().commit()
683 Session().commit()
674 pull_request_id = pull_request.pull_request_id
684 pull_request_id = pull_request.pull_request_id
675
685
676 # target has ancestor - ancestor-new
686 # target has ancestor - ancestor-new
677 # source has ancestor - ancestor-new - change-rebased
687 # source has ancestor - ancestor-new - change-rebased
678 backend.pull_heads(target, heads=['ancestor-new'])
688 backend.pull_heads(target, heads=['ancestor-new'])
679 backend.pull_heads(source, heads=['change-rebased'])
689 backend.pull_heads(source, heads=['change-rebased'])
680
690
681 # update PR
691 # update PR
682 self.app.post(
692 self.app.post(
683 url(controller='pullrequests', action='update',
693 url(controller='pullrequests', action='update',
684 repo_name=target.repo_name,
694 repo_name=target.repo_name,
685 pull_request_id=str(pull_request_id)),
695 pull_request_id=str(pull_request_id)),
686 params={'update_commits': 'true', '_method': 'put',
696 params={'update_commits': 'true', '_method': 'put',
687 'csrf_token': csrf_token},
697 'csrf_token': csrf_token},
688 status=200)
698 status=200)
689
699
690 # check that we have now both revisions
700 # check that we have now both revisions
691 pull_request = PullRequest.get(pull_request_id)
701 pull_request = PullRequest.get(pull_request_id)
692 assert pull_request.revisions == [commit_ids['change-rebased']]
702 assert pull_request.revisions == [commit_ids['change-rebased']]
693 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
703 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
694 branch=backend.default_branch_name,
704 branch=backend.default_branch_name,
695 commit_id=commit_ids['ancestor-new'])
705 commit_id=commit_ids['ancestor-new'])
696
706
697 # TODO: johbo: This should be a test on its own
707 # TODO: johbo: This should be a test on its own
698 response = self.app.get(url(
708 response = self.app.get(url(
699 controller='pullrequests', action='index',
709 controller='pullrequests', action='index',
700 repo_name=target.repo_name))
710 repo_name=target.repo_name))
701 assert response.status_int == 200
711 assert response.status_int == 200
702 assert 'Pull request updated to' in response.body
712 assert 'Pull request updated to' in response.body
703 assert 'with 1 added, 1 removed commits.' in response.body
713 assert 'with 1 added, 1 removed commits.' in response.body
704
714
705 def test_update_of_ancestor_reference(self, backend, csrf_token):
715 def test_update_of_ancestor_reference(self, backend, csrf_token):
706 commits = [
716 commits = [
707 {'message': 'ancestor'},
717 {'message': 'ancestor'},
708 {'message': 'change'},
718 {'message': 'change'},
709 {'message': 'change-2'},
719 {'message': 'change-2'},
710 {'message': 'ancestor-new', 'parents': ['ancestor']},
720 {'message': 'ancestor-new', 'parents': ['ancestor']},
711 {'message': 'change-rebased'},
721 {'message': 'change-rebased'},
712 ]
722 ]
713 commit_ids = backend.create_master_repo(commits)
723 commit_ids = backend.create_master_repo(commits)
714 target = backend.create_repo(heads=['ancestor'])
724 target = backend.create_repo(heads=['ancestor'])
715 source = backend.create_repo(heads=['change'])
725 source = backend.create_repo(heads=['change'])
716
726
717 # create pr from a in source to A in target
727 # create pr from a in source to A in target
718 pull_request = PullRequest()
728 pull_request = PullRequest()
719 pull_request.source_repo = source
729 pull_request.source_repo = source
720 # TODO: johbo: Make sure that we write the source ref this way!
730 # TODO: johbo: Make sure that we write the source ref this way!
721 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
731 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
722 branch=backend.default_branch_name,
732 branch=backend.default_branch_name,
723 commit_id=commit_ids['change'])
733 commit_id=commit_ids['change'])
724 pull_request.target_repo = target
734 pull_request.target_repo = target
725 # TODO: johbo: Target ref should be branch based, since tip can jump
735 # TODO: johbo: Target ref should be branch based, since tip can jump
726 # from branch to branch
736 # from branch to branch
727 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
737 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
728 branch=backend.default_branch_name,
738 branch=backend.default_branch_name,
729 commit_id=commit_ids['ancestor'])
739 commit_id=commit_ids['ancestor'])
730 pull_request.revisions = [commit_ids['change']]
740 pull_request.revisions = [commit_ids['change']]
731 pull_request.title = u"Test"
741 pull_request.title = u"Test"
732 pull_request.description = u"Description"
742 pull_request.description = u"Description"
733 pull_request.author = UserModel().get_by_username(
743 pull_request.author = UserModel().get_by_username(
734 TEST_USER_ADMIN_LOGIN)
744 TEST_USER_ADMIN_LOGIN)
735 Session().add(pull_request)
745 Session().add(pull_request)
736 Session().commit()
746 Session().commit()
737 pull_request_id = pull_request.pull_request_id
747 pull_request_id = pull_request.pull_request_id
738
748
739 # target has ancestor - ancestor-new
749 # target has ancestor - ancestor-new
740 # source has ancestor - ancestor-new - change-rebased
750 # source has ancestor - ancestor-new - change-rebased
741 backend.pull_heads(target, heads=['ancestor-new'])
751 backend.pull_heads(target, heads=['ancestor-new'])
742 backend.pull_heads(source, heads=['change-rebased'])
752 backend.pull_heads(source, heads=['change-rebased'])
743
753
744 # update PR
754 # update PR
745 self.app.post(
755 self.app.post(
746 url(controller='pullrequests', action='update',
756 url(controller='pullrequests', action='update',
747 repo_name=target.repo_name,
757 repo_name=target.repo_name,
748 pull_request_id=str(pull_request_id)),
758 pull_request_id=str(pull_request_id)),
749 params={'update_commits': 'true', '_method': 'put',
759 params={'update_commits': 'true', '_method': 'put',
750 'csrf_token': csrf_token},
760 'csrf_token': csrf_token},
751 status=200)
761 status=200)
752
762
753 # Expect the target reference to be updated correctly
763 # Expect the target reference to be updated correctly
754 pull_request = PullRequest.get(pull_request_id)
764 pull_request = PullRequest.get(pull_request_id)
755 assert pull_request.revisions == [commit_ids['change-rebased']]
765 assert pull_request.revisions == [commit_ids['change-rebased']]
756 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
766 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
757 branch=backend.default_branch_name,
767 branch=backend.default_branch_name,
758 commit_id=commit_ids['ancestor-new'])
768 commit_id=commit_ids['ancestor-new'])
759 assert pull_request.target_ref == expected_target_ref
769 assert pull_request.target_ref == expected_target_ref
760
770
761 def test_remove_pull_request_branch(self, backend_git, csrf_token):
771 def test_remove_pull_request_branch(self, backend_git, csrf_token):
762 branch_name = 'development'
772 branch_name = 'development'
763 commits = [
773 commits = [
764 {'message': 'initial-commit'},
774 {'message': 'initial-commit'},
765 {'message': 'old-feature'},
775 {'message': 'old-feature'},
766 {'message': 'new-feature', 'branch': branch_name},
776 {'message': 'new-feature', 'branch': branch_name},
767 ]
777 ]
768 repo = backend_git.create_repo(commits)
778 repo = backend_git.create_repo(commits)
769 commit_ids = backend_git.commit_ids
779 commit_ids = backend_git.commit_ids
770
780
771 pull_request = PullRequest()
781 pull_request = PullRequest()
772 pull_request.source_repo = repo
782 pull_request.source_repo = repo
773 pull_request.target_repo = repo
783 pull_request.target_repo = repo
774 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
784 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
775 branch=branch_name, commit_id=commit_ids['new-feature'])
785 branch=branch_name, commit_id=commit_ids['new-feature'])
776 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
786 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
777 branch=backend_git.default_branch_name,
787 branch=backend_git.default_branch_name,
778 commit_id=commit_ids['old-feature'])
788 commit_id=commit_ids['old-feature'])
779 pull_request.revisions = [commit_ids['new-feature']]
789 pull_request.revisions = [commit_ids['new-feature']]
780 pull_request.title = u"Test"
790 pull_request.title = u"Test"
781 pull_request.description = u"Description"
791 pull_request.description = u"Description"
782 pull_request.author = UserModel().get_by_username(
792 pull_request.author = UserModel().get_by_username(
783 TEST_USER_ADMIN_LOGIN)
793 TEST_USER_ADMIN_LOGIN)
784 Session().add(pull_request)
794 Session().add(pull_request)
785 Session().commit()
795 Session().commit()
786
796
787 vcs = repo.scm_instance()
797 vcs = repo.scm_instance()
788 vcs.remove_ref('refs/heads/{}'.format(branch_name))
798 vcs.remove_ref('refs/heads/{}'.format(branch_name))
789
799
790 response = self.app.get(url(
800 response = self.app.get(url(
791 controller='pullrequests', action='show',
801 controller='pullrequests', action='show',
792 repo_name=repo.repo_name,
802 repo_name=repo.repo_name,
793 pull_request_id=str(pull_request.pull_request_id)))
803 pull_request_id=str(pull_request.pull_request_id)))
794
804
795 assert response.status_int == 200
805 assert response.status_int == 200
796 assert_response = AssertResponse(response)
806 assert_response = AssertResponse(response)
797 assert_response.element_contains(
807 assert_response.element_contains(
798 '#changeset_compare_view_content .alert strong',
808 '#changeset_compare_view_content .alert strong',
799 'Missing commits')
809 'Missing commits')
800 assert_response.element_contains(
810 assert_response.element_contains(
801 '#changeset_compare_view_content .alert',
811 '#changeset_compare_view_content .alert',
802 'This pull request cannot be displayed, because one or more'
812 'This pull request cannot be displayed, because one or more'
803 ' commits no longer exist in the source repository.')
813 ' commits no longer exist in the source repository.')
804
814
805 def test_strip_commits_from_pull_request(
815 def test_strip_commits_from_pull_request(
806 self, backend, pr_util, csrf_token):
816 self, backend, pr_util, csrf_token):
807 commits = [
817 commits = [
808 {'message': 'initial-commit'},
818 {'message': 'initial-commit'},
809 {'message': 'old-feature'},
819 {'message': 'old-feature'},
810 {'message': 'new-feature', 'parents': ['initial-commit']},
820 {'message': 'new-feature', 'parents': ['initial-commit']},
811 ]
821 ]
812 pull_request = pr_util.create_pull_request(
822 pull_request = pr_util.create_pull_request(
813 commits, target_head='initial-commit', source_head='new-feature',
823 commits, target_head='initial-commit', source_head='new-feature',
814 revisions=['new-feature'])
824 revisions=['new-feature'])
815
825
816 vcs = pr_util.source_repository.scm_instance()
826 vcs = pr_util.source_repository.scm_instance()
817 if backend.alias == 'git':
827 if backend.alias == 'git':
818 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
828 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
819 else:
829 else:
820 vcs.strip(pr_util.commit_ids['new-feature'])
830 vcs.strip(pr_util.commit_ids['new-feature'])
821
831
822 response = self.app.get(url(
832 response = self.app.get(url(
823 controller='pullrequests', action='show',
833 controller='pullrequests', action='show',
824 repo_name=pr_util.target_repository.repo_name,
834 repo_name=pr_util.target_repository.repo_name,
825 pull_request_id=str(pull_request.pull_request_id)))
835 pull_request_id=str(pull_request.pull_request_id)))
826
836
827 assert response.status_int == 200
837 assert response.status_int == 200
828 assert_response = AssertResponse(response)
838 assert_response = AssertResponse(response)
829 assert_response.element_contains(
839 assert_response.element_contains(
830 '#changeset_compare_view_content .alert strong',
840 '#changeset_compare_view_content .alert strong',
831 'Missing commits')
841 'Missing commits')
832 assert_response.element_contains(
842 assert_response.element_contains(
833 '#changeset_compare_view_content .alert',
843 '#changeset_compare_view_content .alert',
834 'This pull request cannot be displayed, because one or more'
844 'This pull request cannot be displayed, because one or more'
835 ' commits no longer exist in the source repository.')
845 ' commits no longer exist in the source repository.')
836 assert_response.element_contains(
846 assert_response.element_contains(
837 '#update_commits',
847 '#update_commits',
838 'Update commits')
848 'Update commits')
839
849
840 def test_strip_commits_and_update(
850 def test_strip_commits_and_update(
841 self, backend, pr_util, csrf_token):
851 self, backend, pr_util, csrf_token):
842 commits = [
852 commits = [
843 {'message': 'initial-commit'},
853 {'message': 'initial-commit'},
844 {'message': 'old-feature'},
854 {'message': 'old-feature'},
845 {'message': 'new-feature', 'parents': ['old-feature']},
855 {'message': 'new-feature', 'parents': ['old-feature']},
846 ]
856 ]
847 pull_request = pr_util.create_pull_request(
857 pull_request = pr_util.create_pull_request(
848 commits, target_head='old-feature', source_head='new-feature',
858 commits, target_head='old-feature', source_head='new-feature',
849 revisions=['new-feature'], mergeable=True)
859 revisions=['new-feature'], mergeable=True)
850
860
851 vcs = pr_util.source_repository.scm_instance()
861 vcs = pr_util.source_repository.scm_instance()
852 if backend.alias == 'git':
862 if backend.alias == 'git':
853 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
863 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
854 else:
864 else:
855 vcs.strip(pr_util.commit_ids['new-feature'])
865 vcs.strip(pr_util.commit_ids['new-feature'])
856
866
857 response = self.app.post(
867 response = self.app.post(
858 url(controller='pullrequests', action='update',
868 url(controller='pullrequests', action='update',
859 repo_name=pull_request.target_repo.repo_name,
869 repo_name=pull_request.target_repo.repo_name,
860 pull_request_id=str(pull_request.pull_request_id)),
870 pull_request_id=str(pull_request.pull_request_id)),
861 params={'update_commits': 'true', '_method': 'put',
871 params={'update_commits': 'true', '_method': 'put',
862 'csrf_token': csrf_token})
872 'csrf_token': csrf_token})
863
873
864 assert response.status_int == 200
874 assert response.status_int == 200
865 assert response.body == 'true'
875 assert response.body == 'true'
866
876
867 # Make sure that after update, it won't raise 500 errors
877 # Make sure that after update, it won't raise 500 errors
868 response = self.app.get(url(
878 response = self.app.get(url(
869 controller='pullrequests', action='show',
879 controller='pullrequests', action='show',
870 repo_name=pr_util.target_repository.repo_name,
880 repo_name=pr_util.target_repository.repo_name,
871 pull_request_id=str(pull_request.pull_request_id)))
881 pull_request_id=str(pull_request.pull_request_id)))
872
882
873 assert response.status_int == 200
883 assert response.status_int == 200
874 assert_response = AssertResponse(response)
884 assert_response = AssertResponse(response)
875 assert_response.element_contains(
885 assert_response.element_contains(
876 '#changeset_compare_view_content .alert strong',
886 '#changeset_compare_view_content .alert strong',
877 'Missing commits')
887 'Missing commits')
878
888
879 def test_branch_is_a_link(self, pr_util):
889 def test_branch_is_a_link(self, pr_util):
880 pull_request = pr_util.create_pull_request()
890 pull_request = pr_util.create_pull_request()
881 pull_request.source_ref = 'branch:origin:1234567890abcdef'
891 pull_request.source_ref = 'branch:origin:1234567890abcdef'
882 pull_request.target_ref = 'branch:target:abcdef1234567890'
892 pull_request.target_ref = 'branch:target:abcdef1234567890'
883 Session().add(pull_request)
893 Session().add(pull_request)
884 Session().commit()
894 Session().commit()
885
895
886 response = self.app.get(url(
896 response = self.app.get(url(
887 controller='pullrequests', action='show',
897 controller='pullrequests', action='show',
888 repo_name=pull_request.target_repo.scm_instance().name,
898 repo_name=pull_request.target_repo.scm_instance().name,
889 pull_request_id=str(pull_request.pull_request_id)))
899 pull_request_id=str(pull_request.pull_request_id)))
890 assert response.status_int == 200
900 assert response.status_int == 200
891 assert_response = AssertResponse(response)
901 assert_response = AssertResponse(response)
892
902
893 origin = assert_response.get_element('.pr-origininfo .tag')
903 origin = assert_response.get_element('.pr-origininfo .tag')
894 origin_children = origin.getchildren()
904 origin_children = origin.getchildren()
895 assert len(origin_children) == 1
905 assert len(origin_children) == 1
896 target = assert_response.get_element('.pr-targetinfo .tag')
906 target = assert_response.get_element('.pr-targetinfo .tag')
897 target_children = target.getchildren()
907 target_children = target.getchildren()
898 assert len(target_children) == 1
908 assert len(target_children) == 1
899
909
900 expected_origin_link = url(
910 expected_origin_link = url(
901 'changelog_home',
911 'changelog_home',
902 repo_name=pull_request.source_repo.scm_instance().name,
912 repo_name=pull_request.source_repo.scm_instance().name,
903 branch='origin')
913 branch='origin')
904 expected_target_link = url(
914 expected_target_link = url(
905 'changelog_home',
915 'changelog_home',
906 repo_name=pull_request.target_repo.scm_instance().name,
916 repo_name=pull_request.target_repo.scm_instance().name,
907 branch='target')
917 branch='target')
908 assert origin_children[0].attrib['href'] == expected_origin_link
918 assert origin_children[0].attrib['href'] == expected_origin_link
909 assert origin_children[0].text == 'branch: origin'
919 assert origin_children[0].text == 'branch: origin'
910 assert target_children[0].attrib['href'] == expected_target_link
920 assert target_children[0].attrib['href'] == expected_target_link
911 assert target_children[0].text == 'branch: target'
921 assert target_children[0].text == 'branch: target'
912
922
913 def test_bookmark_is_not_a_link(self, pr_util):
923 def test_bookmark_is_not_a_link(self, pr_util):
914 pull_request = pr_util.create_pull_request()
924 pull_request = pr_util.create_pull_request()
915 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
925 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
916 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
926 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
917 Session().add(pull_request)
927 Session().add(pull_request)
918 Session().commit()
928 Session().commit()
919
929
920 response = self.app.get(url(
930 response = self.app.get(url(
921 controller='pullrequests', action='show',
931 controller='pullrequests', action='show',
922 repo_name=pull_request.target_repo.scm_instance().name,
932 repo_name=pull_request.target_repo.scm_instance().name,
923 pull_request_id=str(pull_request.pull_request_id)))
933 pull_request_id=str(pull_request.pull_request_id)))
924 assert response.status_int == 200
934 assert response.status_int == 200
925 assert_response = AssertResponse(response)
935 assert_response = AssertResponse(response)
926
936
927 origin = assert_response.get_element('.pr-origininfo .tag')
937 origin = assert_response.get_element('.pr-origininfo .tag')
928 assert origin.text.strip() == 'bookmark: origin'
938 assert origin.text.strip() == 'bookmark: origin'
929 assert origin.getchildren() == []
939 assert origin.getchildren() == []
930
940
931 target = assert_response.get_element('.pr-targetinfo .tag')
941 target = assert_response.get_element('.pr-targetinfo .tag')
932 assert target.text.strip() == 'bookmark: target'
942 assert target.text.strip() == 'bookmark: target'
933 assert target.getchildren() == []
943 assert target.getchildren() == []
934
944
935 def test_tag_is_not_a_link(self, pr_util):
945 def test_tag_is_not_a_link(self, pr_util):
936 pull_request = pr_util.create_pull_request()
946 pull_request = pr_util.create_pull_request()
937 pull_request.source_ref = 'tag:origin:1234567890abcdef'
947 pull_request.source_ref = 'tag:origin:1234567890abcdef'
938 pull_request.target_ref = 'tag:target:abcdef1234567890'
948 pull_request.target_ref = 'tag:target:abcdef1234567890'
939 Session().add(pull_request)
949 Session().add(pull_request)
940 Session().commit()
950 Session().commit()
941
951
942 response = self.app.get(url(
952 response = self.app.get(url(
943 controller='pullrequests', action='show',
953 controller='pullrequests', action='show',
944 repo_name=pull_request.target_repo.scm_instance().name,
954 repo_name=pull_request.target_repo.scm_instance().name,
945 pull_request_id=str(pull_request.pull_request_id)))
955 pull_request_id=str(pull_request.pull_request_id)))
946 assert response.status_int == 200
956 assert response.status_int == 200
947 assert_response = AssertResponse(response)
957 assert_response = AssertResponse(response)
948
958
949 origin = assert_response.get_element('.pr-origininfo .tag')
959 origin = assert_response.get_element('.pr-origininfo .tag')
950 assert origin.text.strip() == 'tag: origin'
960 assert origin.text.strip() == 'tag: origin'
951 assert origin.getchildren() == []
961 assert origin.getchildren() == []
952
962
953 target = assert_response.get_element('.pr-targetinfo .tag')
963 target = assert_response.get_element('.pr-targetinfo .tag')
954 assert target.text.strip() == 'tag: target'
964 assert target.text.strip() == 'tag: target'
955 assert target.getchildren() == []
965 assert target.getchildren() == []
956
966
957 def test_description_is_escaped_on_index_page(self, backend, pr_util):
967
958 xss_description = "<script>alert('Hi!')</script>"
959 pull_request = pr_util.create_pull_request(description=xss_description)
960 response = self.app.get(url(
961 controller='pullrequests', action='show_all',
962 repo_name=pull_request.target_repo.repo_name))
963 response.mustcontain(
964 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
965
968
966 @pytest.mark.parametrize('mergeable', [True, False])
969 @pytest.mark.parametrize('mergeable', [True, False])
967 def test_shadow_repository_link(
970 def test_shadow_repository_link(
968 self, mergeable, pr_util, http_host_stub):
971 self, mergeable, pr_util, http_host_stub):
969 """
972 """
970 Check that the pull request summary page displays a link to the shadow
973 Check that the pull request summary page displays a link to the shadow
971 repository if the pull request is mergeable. If it is not mergeable
974 repository if the pull request is mergeable. If it is not mergeable
972 the link should not be displayed.
975 the link should not be displayed.
973 """
976 """
974 pull_request = pr_util.create_pull_request(
977 pull_request = pr_util.create_pull_request(
975 mergeable=mergeable, enable_notifications=False)
978 mergeable=mergeable, enable_notifications=False)
976 target_repo = pull_request.target_repo.scm_instance()
979 target_repo = pull_request.target_repo.scm_instance()
977 pr_id = pull_request.pull_request_id
980 pr_id = pull_request.pull_request_id
978 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
981 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
979 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
982 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
980
983
981 response = self.app.get(url(
984 response = self.app.get(url(
982 controller='pullrequests', action='show',
985 controller='pullrequests', action='show',
983 repo_name=target_repo.name,
986 repo_name=target_repo.name,
984 pull_request_id=str(pr_id)))
987 pull_request_id=str(pr_id)))
985
988
986 assertr = AssertResponse(response)
989 assertr = AssertResponse(response)
987 if mergeable:
990 if mergeable:
988 assertr.element_value_contains(
991 assertr.element_value_contains(
989 'div.pr-mergeinfo input', shadow_url)
992 'div.pr-mergeinfo input', shadow_url)
990 assertr.element_value_contains(
993 assertr.element_value_contains(
991 'div.pr-mergeinfo input', 'pr-merge')
994 'div.pr-mergeinfo input', 'pr-merge')
992 else:
995 else:
993 assertr.no_element_exists('div.pr-mergeinfo')
996 assertr.no_element_exists('div.pr-mergeinfo')
994
997
995
998
996 @pytest.mark.usefixtures('app')
999 @pytest.mark.usefixtures('app')
997 @pytest.mark.backends("git", "hg")
1000 @pytest.mark.backends("git", "hg")
998 class TestPullrequestsControllerDelete(object):
1001 class TestPullrequestsControllerDelete(object):
999 def test_pull_request_delete_button_permissions_admin(
1002 def test_pull_request_delete_button_permissions_admin(
1000 self, autologin_user, user_admin, pr_util):
1003 self, autologin_user, user_admin, pr_util):
1001 pull_request = pr_util.create_pull_request(
1004 pull_request = pr_util.create_pull_request(
1002 author=user_admin.username, enable_notifications=False)
1005 author=user_admin.username, enable_notifications=False)
1003
1006
1004 response = self.app.get(url(
1007 response = self.app.get(url(
1005 controller='pullrequests', action='show',
1008 controller='pullrequests', action='show',
1006 repo_name=pull_request.target_repo.scm_instance().name,
1009 repo_name=pull_request.target_repo.scm_instance().name,
1007 pull_request_id=str(pull_request.pull_request_id)))
1010 pull_request_id=str(pull_request.pull_request_id)))
1008
1011
1009 response.mustcontain('id="delete_pullrequest"')
1012 response.mustcontain('id="delete_pullrequest"')
1010 response.mustcontain('Confirm to delete this pull request')
1013 response.mustcontain('Confirm to delete this pull request')
1011
1014
1012 def test_pull_request_delete_button_permissions_owner(
1015 def test_pull_request_delete_button_permissions_owner(
1013 self, autologin_regular_user, user_regular, pr_util):
1016 self, autologin_regular_user, user_regular, pr_util):
1014 pull_request = pr_util.create_pull_request(
1017 pull_request = pr_util.create_pull_request(
1015 author=user_regular.username, enable_notifications=False)
1018 author=user_regular.username, enable_notifications=False)
1016
1019
1017 response = self.app.get(url(
1020 response = self.app.get(url(
1018 controller='pullrequests', action='show',
1021 controller='pullrequests', action='show',
1019 repo_name=pull_request.target_repo.scm_instance().name,
1022 repo_name=pull_request.target_repo.scm_instance().name,
1020 pull_request_id=str(pull_request.pull_request_id)))
1023 pull_request_id=str(pull_request.pull_request_id)))
1021
1024
1022 response.mustcontain('id="delete_pullrequest"')
1025 response.mustcontain('id="delete_pullrequest"')
1023 response.mustcontain('Confirm to delete this pull request')
1026 response.mustcontain('Confirm to delete this pull request')
1024
1027
1025 def test_pull_request_delete_button_permissions_forbidden(
1028 def test_pull_request_delete_button_permissions_forbidden(
1026 self, autologin_regular_user, user_regular, user_admin, pr_util):
1029 self, autologin_regular_user, user_regular, user_admin, pr_util):
1027 pull_request = pr_util.create_pull_request(
1030 pull_request = pr_util.create_pull_request(
1028 author=user_admin.username, enable_notifications=False)
1031 author=user_admin.username, enable_notifications=False)
1029
1032
1030 response = self.app.get(url(
1033 response = self.app.get(url(
1031 controller='pullrequests', action='show',
1034 controller='pullrequests', action='show',
1032 repo_name=pull_request.target_repo.scm_instance().name,
1035 repo_name=pull_request.target_repo.scm_instance().name,
1033 pull_request_id=str(pull_request.pull_request_id)))
1036 pull_request_id=str(pull_request.pull_request_id)))
1034 response.mustcontain(no=['id="delete_pullrequest"'])
1037 response.mustcontain(no=['id="delete_pullrequest"'])
1035 response.mustcontain(no=['Confirm to delete this pull request'])
1038 response.mustcontain(no=['Confirm to delete this pull request'])
1036
1039
1037 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1040 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1038 self, autologin_regular_user, user_regular, user_admin, pr_util,
1041 self, autologin_regular_user, user_regular, user_admin, pr_util,
1039 user_util):
1042 user_util):
1040
1043
1041 pull_request = pr_util.create_pull_request(
1044 pull_request = pr_util.create_pull_request(
1042 author=user_admin.username, enable_notifications=False)
1045 author=user_admin.username, enable_notifications=False)
1043
1046
1044 user_util.grant_user_permission_to_repo(
1047 user_util.grant_user_permission_to_repo(
1045 pull_request.target_repo, user_regular,
1048 pull_request.target_repo, user_regular,
1046 'repository.write')
1049 'repository.write')
1047
1050
1048 response = self.app.get(url(
1051 response = self.app.get(url(
1049 controller='pullrequests', action='show',
1052 controller='pullrequests', action='show',
1050 repo_name=pull_request.target_repo.scm_instance().name,
1053 repo_name=pull_request.target_repo.scm_instance().name,
1051 pull_request_id=str(pull_request.pull_request_id)))
1054 pull_request_id=str(pull_request.pull_request_id)))
1052
1055
1053 response.mustcontain('id="open_edit_pullrequest"')
1056 response.mustcontain('id="open_edit_pullrequest"')
1054 response.mustcontain('id="delete_pullrequest"')
1057 response.mustcontain('id="delete_pullrequest"')
1055 response.mustcontain(no=['Confirm to delete this pull request'])
1058 response.mustcontain(no=['Confirm to delete this pull request'])
1056
1059
1057
1060
1058 def assert_pull_request_status(pull_request, expected_status):
1061 def assert_pull_request_status(pull_request, expected_status):
1059 status = ChangesetStatusModel().calculated_review_status(
1062 status = ChangesetStatusModel().calculated_review_status(
1060 pull_request=pull_request)
1063 pull_request=pull_request)
1061 assert status == expected_status
1064 assert status == expected_status
1062
1065
1063
1066
1064 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1067 @pytest.mark.parametrize('action', ['index', 'create'])
1065 @pytest.mark.usefixtures("autologin_user")
1068 @pytest.mark.usefixtures("autologin_user")
1066 def test_redirects_to_repo_summary_for_svn_repositories(
1069 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1067 backend_svn, app, action):
1068 denied_actions = ['show_all', 'index', 'create']
1069 for action in denied_actions:
1070 response = app.get(url(
1070 response = app.get(url(
1071 controller='pullrequests', action=action,
1071 controller='pullrequests', action=action,
1072 repo_name=backend_svn.repo_name))
1072 repo_name=backend_svn.repo_name))
1073 assert response.status_int == 302
1073 assert response.status_int == 302
1074
1074
1075 # Not allowed, redirect to the summary
1075 # Not allowed, redirect to the summary
1076 redirected = response.follow()
1076 redirected = response.follow()
1077 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1077 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1078
1078
1079 # URL adds leading slash and path doesn't have it
1079 # URL adds leading slash and path doesn't have it
1080 assert redirected.req.path == summary_url
1080 assert redirected.req.path == summary_url
1081
1081
1082
1082
1083 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1083 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1084 # TODO: johbo: Global import not possible because models.forms blows up
1084 # TODO: johbo: Global import not possible because models.forms blows up
1085 from rhodecode.controllers.pullrequests import PullrequestsController
1085 from rhodecode.controllers.pullrequests import PullrequestsController
1086 controller = PullrequestsController()
1086 controller = PullrequestsController()
1087 patcher = mock.patch(
1087 patcher = mock.patch(
1088 'rhodecode.model.db.BaseModel.get', return_value=None)
1088 'rhodecode.model.db.BaseModel.get', return_value=None)
1089 with pytest.raises(HTTPNotFound), patcher:
1089 with pytest.raises(HTTPNotFound), patcher:
1090 controller._delete_comment(1)
1090 controller._delete_comment(1)
@@ -1,851 +1,851 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 class TestPullRequestModel:
44 class TestPullRequestModel:
45
45
46 @pytest.fixture
46 @pytest.fixture
47 def pull_request(self, request, backend, pr_util):
47 def pull_request(self, request, backend, pr_util):
48 """
48 """
49 A pull request combined with multiples patches.
49 A pull request combined with multiples patches.
50 """
50 """
51 BackendClass = get_backend(backend.alias)
51 BackendClass = get_backend(backend.alias)
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
53 self.workspace_remove_patcher = mock.patch.object(
53 self.workspace_remove_patcher = mock.patch.object(
54 BackendClass, 'cleanup_merge_workspace')
54 BackendClass, 'cleanup_merge_workspace')
55
55
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
58 self.comment_patcher = mock.patch(
58 self.comment_patcher = mock.patch(
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
60 self.comment_patcher.start()
60 self.comment_patcher.start()
61 self.notification_patcher = mock.patch(
61 self.notification_patcher = mock.patch(
62 'rhodecode.model.notification.NotificationModel.create')
62 'rhodecode.model.notification.NotificationModel.create')
63 self.notification_patcher.start()
63 self.notification_patcher.start()
64 self.helper_patcher = mock.patch(
64 self.helper_patcher = mock.patch(
65 'rhodecode.lib.helpers.url')
65 'rhodecode.lib.helpers.url')
66 self.helper_patcher.start()
66 self.helper_patcher.start()
67
67
68 self.hook_patcher = mock.patch.object(PullRequestModel,
68 self.hook_patcher = mock.patch.object(PullRequestModel,
69 '_trigger_pull_request_hook')
69 '_trigger_pull_request_hook')
70 self.hook_mock = self.hook_patcher.start()
70 self.hook_mock = self.hook_patcher.start()
71
71
72 self.invalidation_patcher = mock.patch(
72 self.invalidation_patcher = mock.patch(
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
74 self.invalidation_mock = self.invalidation_patcher.start()
74 self.invalidation_mock = self.invalidation_patcher.start()
75
75
76 self.pull_request = pr_util.create_pull_request(
76 self.pull_request = pr_util.create_pull_request(
77 mergeable=True, name_suffix=u'ąć')
77 mergeable=True, name_suffix=u'ąć')
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
81
81
82 @request.addfinalizer
82 @request.addfinalizer
83 def cleanup_pull_request():
83 def cleanup_pull_request():
84 calls = [mock.call(
84 calls = [mock.call(
85 self.pull_request, self.pull_request.author, 'create')]
85 self.pull_request, self.pull_request.author, 'create')]
86 self.hook_mock.assert_has_calls(calls)
86 self.hook_mock.assert_has_calls(calls)
87
87
88 self.workspace_remove_patcher.stop()
88 self.workspace_remove_patcher.stop()
89 self.merge_patcher.stop()
89 self.merge_patcher.stop()
90 self.comment_patcher.stop()
90 self.comment_patcher.stop()
91 self.notification_patcher.stop()
91 self.notification_patcher.stop()
92 self.helper_patcher.stop()
92 self.helper_patcher.stop()
93 self.hook_patcher.stop()
93 self.hook_patcher.stop()
94 self.invalidation_patcher.stop()
94 self.invalidation_patcher.stop()
95
95
96 return self.pull_request
96 return self.pull_request
97
97
98 def test_get_all(self, pull_request):
98 def test_get_all(self, pull_request):
99 prs = PullRequestModel().get_all(pull_request.target_repo)
99 prs = PullRequestModel().get_all(pull_request.target_repo)
100 assert isinstance(prs, list)
100 assert isinstance(prs, list)
101 assert len(prs) == 1
101 assert len(prs) == 1
102
102
103 def test_count_all(self, pull_request):
103 def test_count_all(self, pull_request):
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
105 assert pr_count == 1
105 assert pr_count == 1
106
106
107 def test_get_awaiting_review(self, pull_request):
107 def test_get_awaiting_review(self, pull_request):
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
109 assert isinstance(prs, list)
109 assert isinstance(prs, list)
110 assert len(prs) == 1
110 assert len(prs) == 1
111
111
112 def test_count_awaiting_review(self, pull_request):
112 def test_count_awaiting_review(self, pull_request):
113 pr_count = PullRequestModel().count_awaiting_review(
113 pr_count = PullRequestModel().count_awaiting_review(
114 pull_request.target_repo)
114 pull_request.target_repo)
115 assert pr_count == 1
115 assert pr_count == 1
116
116
117 def test_get_awaiting_my_review(self, pull_request):
117 def test_get_awaiting_my_review(self, pull_request):
118 PullRequestModel().update_reviewers(
118 PullRequestModel().update_reviewers(
119 pull_request, [(pull_request.author, ['author'])])
119 pull_request, [(pull_request.author, ['author'], False)])
120 prs = PullRequestModel().get_awaiting_my_review(
120 prs = PullRequestModel().get_awaiting_my_review(
121 pull_request.target_repo, user_id=pull_request.author.user_id)
121 pull_request.target_repo, user_id=pull_request.author.user_id)
122 assert isinstance(prs, list)
122 assert isinstance(prs, list)
123 assert len(prs) == 1
123 assert len(prs) == 1
124
124
125 def test_count_awaiting_my_review(self, pull_request):
125 def test_count_awaiting_my_review(self, pull_request):
126 PullRequestModel().update_reviewers(
126 PullRequestModel().update_reviewers(
127 pull_request, [(pull_request.author, ['author'])])
127 pull_request, [(pull_request.author, ['author'], False)])
128 pr_count = PullRequestModel().count_awaiting_my_review(
128 pr_count = PullRequestModel().count_awaiting_my_review(
129 pull_request.target_repo, user_id=pull_request.author.user_id)
129 pull_request.target_repo, user_id=pull_request.author.user_id)
130 assert pr_count == 1
130 assert pr_count == 1
131
131
132 def test_delete_calls_cleanup_merge(self, pull_request):
132 def test_delete_calls_cleanup_merge(self, pull_request):
133 PullRequestModel().delete(pull_request)
133 PullRequestModel().delete(pull_request)
134
134
135 self.workspace_remove_mock.assert_called_once_with(
135 self.workspace_remove_mock.assert_called_once_with(
136 self.workspace_id)
136 self.workspace_id)
137
137
138 def test_close_calls_cleanup_and_hook(self, pull_request):
138 def test_close_calls_cleanup_and_hook(self, pull_request):
139 PullRequestModel().close_pull_request(
139 PullRequestModel().close_pull_request(
140 pull_request, pull_request.author)
140 pull_request, pull_request.author)
141
141
142 self.workspace_remove_mock.assert_called_once_with(
142 self.workspace_remove_mock.assert_called_once_with(
143 self.workspace_id)
143 self.workspace_id)
144 self.hook_mock.assert_called_with(
144 self.hook_mock.assert_called_with(
145 self.pull_request, self.pull_request.author, 'close')
145 self.pull_request, self.pull_request.author, 'close')
146
146
147 def test_merge_status(self, pull_request):
147 def test_merge_status(self, pull_request):
148 self.merge_mock.return_value = MergeResponse(
148 self.merge_mock.return_value = MergeResponse(
149 True, False, None, MergeFailureReason.NONE)
149 True, False, None, MergeFailureReason.NONE)
150
150
151 assert pull_request._last_merge_source_rev is None
151 assert pull_request._last_merge_source_rev is None
152 assert pull_request._last_merge_target_rev is None
152 assert pull_request._last_merge_target_rev is None
153 assert pull_request._last_merge_status is None
153 assert pull_request._last_merge_status is None
154
154
155 status, msg = PullRequestModel().merge_status(pull_request)
155 status, msg = PullRequestModel().merge_status(pull_request)
156 assert status is True
156 assert status is True
157 assert msg.eval() == 'This pull request can be automatically merged.'
157 assert msg.eval() == 'This pull request can be automatically merged.'
158 self.merge_mock.assert_called_once_with(
158 self.merge_mock.assert_called_once_with(
159 pull_request.target_ref_parts,
159 pull_request.target_ref_parts,
160 pull_request.source_repo.scm_instance(),
160 pull_request.source_repo.scm_instance(),
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
162 use_rebase=False)
162 use_rebase=False)
163
163
164 assert pull_request._last_merge_source_rev == self.source_commit
164 assert pull_request._last_merge_source_rev == self.source_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
167
167
168 self.merge_mock.reset_mock()
168 self.merge_mock.reset_mock()
169 status, msg = PullRequestModel().merge_status(pull_request)
169 status, msg = PullRequestModel().merge_status(pull_request)
170 assert status is True
170 assert status is True
171 assert msg.eval() == 'This pull request can be automatically merged.'
171 assert msg.eval() == 'This pull request can be automatically merged.'
172 assert self.merge_mock.called is False
172 assert self.merge_mock.called is False
173
173
174 def test_merge_status_known_failure(self, pull_request):
174 def test_merge_status_known_failure(self, pull_request):
175 self.merge_mock.return_value = MergeResponse(
175 self.merge_mock.return_value = MergeResponse(
176 False, False, None, MergeFailureReason.MERGE_FAILED)
176 False, False, None, MergeFailureReason.MERGE_FAILED)
177
177
178 assert pull_request._last_merge_source_rev is None
178 assert pull_request._last_merge_source_rev is None
179 assert pull_request._last_merge_target_rev is None
179 assert pull_request._last_merge_target_rev is None
180 assert pull_request._last_merge_status is None
180 assert pull_request._last_merge_status is None
181
181
182 status, msg = PullRequestModel().merge_status(pull_request)
182 status, msg = PullRequestModel().merge_status(pull_request)
183 assert status is False
183 assert status is False
184 assert (
184 assert (
185 msg.eval() ==
185 msg.eval() ==
186 'This pull request cannot be merged because of merge conflicts.')
186 'This pull request cannot be merged because of merge conflicts.')
187 self.merge_mock.assert_called_once_with(
187 self.merge_mock.assert_called_once_with(
188 pull_request.target_ref_parts,
188 pull_request.target_ref_parts,
189 pull_request.source_repo.scm_instance(),
189 pull_request.source_repo.scm_instance(),
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
191 use_rebase=False)
191 use_rebase=False)
192
192
193 assert pull_request._last_merge_source_rev == self.source_commit
193 assert pull_request._last_merge_source_rev == self.source_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
195 assert (
195 assert (
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
197
197
198 self.merge_mock.reset_mock()
198 self.merge_mock.reset_mock()
199 status, msg = PullRequestModel().merge_status(pull_request)
199 status, msg = PullRequestModel().merge_status(pull_request)
200 assert status is False
200 assert status is False
201 assert (
201 assert (
202 msg.eval() ==
202 msg.eval() ==
203 'This pull request cannot be merged because of merge conflicts.')
203 'This pull request cannot be merged because of merge conflicts.')
204 assert self.merge_mock.called is False
204 assert self.merge_mock.called is False
205
205
206 def test_merge_status_unknown_failure(self, pull_request):
206 def test_merge_status_unknown_failure(self, pull_request):
207 self.merge_mock.return_value = MergeResponse(
207 self.merge_mock.return_value = MergeResponse(
208 False, False, None, MergeFailureReason.UNKNOWN)
208 False, False, None, MergeFailureReason.UNKNOWN)
209
209
210 assert pull_request._last_merge_source_rev is None
210 assert pull_request._last_merge_source_rev is None
211 assert pull_request._last_merge_target_rev is None
211 assert pull_request._last_merge_target_rev is None
212 assert pull_request._last_merge_status is None
212 assert pull_request._last_merge_status is None
213
213
214 status, msg = PullRequestModel().merge_status(pull_request)
214 status, msg = PullRequestModel().merge_status(pull_request)
215 assert status is False
215 assert status is False
216 assert msg.eval() == (
216 assert msg.eval() == (
217 'This pull request cannot be merged because of an unhandled'
217 'This pull request cannot be merged because of an unhandled'
218 ' exception.')
218 ' exception.')
219 self.merge_mock.assert_called_once_with(
219 self.merge_mock.assert_called_once_with(
220 pull_request.target_ref_parts,
220 pull_request.target_ref_parts,
221 pull_request.source_repo.scm_instance(),
221 pull_request.source_repo.scm_instance(),
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
223 use_rebase=False)
223 use_rebase=False)
224
224
225 assert pull_request._last_merge_source_rev is None
225 assert pull_request._last_merge_source_rev is None
226 assert pull_request._last_merge_target_rev is None
226 assert pull_request._last_merge_target_rev is None
227 assert pull_request._last_merge_status is None
227 assert pull_request._last_merge_status is None
228
228
229 self.merge_mock.reset_mock()
229 self.merge_mock.reset_mock()
230 status, msg = PullRequestModel().merge_status(pull_request)
230 status, msg = PullRequestModel().merge_status(pull_request)
231 assert status is False
231 assert status is False
232 assert msg.eval() == (
232 assert msg.eval() == (
233 'This pull request cannot be merged because of an unhandled'
233 'This pull request cannot be merged because of an unhandled'
234 ' exception.')
234 ' exception.')
235 assert self.merge_mock.called is True
235 assert self.merge_mock.called is True
236
236
237 def test_merge_status_when_target_is_locked(self, pull_request):
237 def test_merge_status_when_target_is_locked(self, pull_request):
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
239 status, msg = PullRequestModel().merge_status(pull_request)
239 status, msg = PullRequestModel().merge_status(pull_request)
240 assert status is False
240 assert status is False
241 assert msg.eval() == (
241 assert msg.eval() == (
242 'This pull request cannot be merged because the target repository'
242 'This pull request cannot be merged because the target repository'
243 ' is locked.')
243 ' is locked.')
244
244
245 def test_merge_status_requirements_check_target(self, pull_request):
245 def test_merge_status_requirements_check_target(self, pull_request):
246
246
247 def has_largefiles(self, repo):
247 def has_largefiles(self, repo):
248 return repo == pull_request.source_repo
248 return repo == pull_request.source_repo
249
249
250 patcher = mock.patch.object(
250 patcher = mock.patch.object(
251 PullRequestModel, '_has_largefiles', has_largefiles)
251 PullRequestModel, '_has_largefiles', has_largefiles)
252 with patcher:
252 with patcher:
253 status, msg = PullRequestModel().merge_status(pull_request)
253 status, msg = PullRequestModel().merge_status(pull_request)
254
254
255 assert status is False
255 assert status is False
256 assert msg == 'Target repository large files support is disabled.'
256 assert msg == 'Target repository large files support is disabled.'
257
257
258 def test_merge_status_requirements_check_source(self, pull_request):
258 def test_merge_status_requirements_check_source(self, pull_request):
259
259
260 def has_largefiles(self, repo):
260 def has_largefiles(self, repo):
261 return repo == pull_request.target_repo
261 return repo == pull_request.target_repo
262
262
263 patcher = mock.patch.object(
263 patcher = mock.patch.object(
264 PullRequestModel, '_has_largefiles', has_largefiles)
264 PullRequestModel, '_has_largefiles', has_largefiles)
265 with patcher:
265 with patcher:
266 status, msg = PullRequestModel().merge_status(pull_request)
266 status, msg = PullRequestModel().merge_status(pull_request)
267
267
268 assert status is False
268 assert status is False
269 assert msg == 'Source repository large files support is disabled.'
269 assert msg == 'Source repository large files support is disabled.'
270
270
271 def test_merge(self, pull_request, merge_extras):
271 def test_merge(self, pull_request, merge_extras):
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
273 merge_ref = Reference(
273 merge_ref = Reference(
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
275 self.merge_mock.return_value = MergeResponse(
275 self.merge_mock.return_value = MergeResponse(
276 True, True, merge_ref, MergeFailureReason.NONE)
276 True, True, merge_ref, MergeFailureReason.NONE)
277
277
278 merge_extras['repository'] = pull_request.target_repo.repo_name
278 merge_extras['repository'] = pull_request.target_repo.repo_name
279 PullRequestModel().merge(
279 PullRequestModel().merge(
280 pull_request, pull_request.author, extras=merge_extras)
280 pull_request, pull_request.author, extras=merge_extras)
281
281
282 message = (
282 message = (
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
284 u'\n\n {pr_title}'.format(
284 u'\n\n {pr_title}'.format(
285 pr_id=pull_request.pull_request_id,
285 pr_id=pull_request.pull_request_id,
286 source_repo=safe_unicode(
286 source_repo=safe_unicode(
287 pull_request.source_repo.scm_instance().name),
287 pull_request.source_repo.scm_instance().name),
288 source_ref_name=pull_request.source_ref_parts.name,
288 source_ref_name=pull_request.source_ref_parts.name,
289 pr_title=safe_unicode(pull_request.title)
289 pr_title=safe_unicode(pull_request.title)
290 )
290 )
291 )
291 )
292 self.merge_mock.assert_called_once_with(
292 self.merge_mock.assert_called_once_with(
293 pull_request.target_ref_parts,
293 pull_request.target_ref_parts,
294 pull_request.source_repo.scm_instance(),
294 pull_request.source_repo.scm_instance(),
295 pull_request.source_ref_parts, self.workspace_id,
295 pull_request.source_ref_parts, self.workspace_id,
296 user_name=user.username, user_email=user.email, message=message,
296 user_name=user.username, user_email=user.email, message=message,
297 use_rebase=False
297 use_rebase=False
298 )
298 )
299 self.invalidation_mock.assert_called_once_with(
299 self.invalidation_mock.assert_called_once_with(
300 pull_request.target_repo.repo_name)
300 pull_request.target_repo.repo_name)
301
301
302 self.hook_mock.assert_called_with(
302 self.hook_mock.assert_called_with(
303 self.pull_request, self.pull_request.author, 'merge')
303 self.pull_request, self.pull_request.author, 'merge')
304
304
305 pull_request = PullRequest.get(pull_request.pull_request_id)
305 pull_request = PullRequest.get(pull_request.pull_request_id)
306 assert (
306 assert (
307 pull_request.merge_rev ==
307 pull_request.merge_rev ==
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
309
309
310 def test_merge_failed(self, pull_request, merge_extras):
310 def test_merge_failed(self, pull_request, merge_extras):
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
312 merge_ref = Reference(
312 merge_ref = Reference(
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314 self.merge_mock.return_value = MergeResponse(
314 self.merge_mock.return_value = MergeResponse(
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
316
316
317 merge_extras['repository'] = pull_request.target_repo.repo_name
317 merge_extras['repository'] = pull_request.target_repo.repo_name
318 PullRequestModel().merge(
318 PullRequestModel().merge(
319 pull_request, pull_request.author, extras=merge_extras)
319 pull_request, pull_request.author, extras=merge_extras)
320
320
321 message = (
321 message = (
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
323 u'\n\n {pr_title}'.format(
323 u'\n\n {pr_title}'.format(
324 pr_id=pull_request.pull_request_id,
324 pr_id=pull_request.pull_request_id,
325 source_repo=safe_unicode(
325 source_repo=safe_unicode(
326 pull_request.source_repo.scm_instance().name),
326 pull_request.source_repo.scm_instance().name),
327 source_ref_name=pull_request.source_ref_parts.name,
327 source_ref_name=pull_request.source_ref_parts.name,
328 pr_title=safe_unicode(pull_request.title)
328 pr_title=safe_unicode(pull_request.title)
329 )
329 )
330 )
330 )
331 self.merge_mock.assert_called_once_with(
331 self.merge_mock.assert_called_once_with(
332 pull_request.target_ref_parts,
332 pull_request.target_ref_parts,
333 pull_request.source_repo.scm_instance(),
333 pull_request.source_repo.scm_instance(),
334 pull_request.source_ref_parts, self.workspace_id,
334 pull_request.source_ref_parts, self.workspace_id,
335 user_name=user.username, user_email=user.email, message=message,
335 user_name=user.username, user_email=user.email, message=message,
336 use_rebase=False
336 use_rebase=False
337 )
337 )
338
338
339 pull_request = PullRequest.get(pull_request.pull_request_id)
339 pull_request = PullRequest.get(pull_request.pull_request_id)
340 assert self.invalidation_mock.called is False
340 assert self.invalidation_mock.called is False
341 assert pull_request.merge_rev is None
341 assert pull_request.merge_rev is None
342
342
343 def test_get_commit_ids(self, pull_request):
343 def test_get_commit_ids(self, pull_request):
344 # The PR has been not merget yet, so expect an exception
344 # The PR has been not merget yet, so expect an exception
345 with pytest.raises(ValueError):
345 with pytest.raises(ValueError):
346 PullRequestModel()._get_commit_ids(pull_request)
346 PullRequestModel()._get_commit_ids(pull_request)
347
347
348 # Merge revision is in the revisions list
348 # Merge revision is in the revisions list
349 pull_request.merge_rev = pull_request.revisions[0]
349 pull_request.merge_rev = pull_request.revisions[0]
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
351 assert commit_ids == pull_request.revisions
351 assert commit_ids == pull_request.revisions
352
352
353 # Merge revision is not in the revisions list
353 # Merge revision is not in the revisions list
354 pull_request.merge_rev = 'f000' * 10
354 pull_request.merge_rev = 'f000' * 10
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
357
357
358 def test_get_diff_from_pr_version(self, pull_request):
358 def test_get_diff_from_pr_version(self, pull_request):
359 source_repo = pull_request.source_repo
359 source_repo = pull_request.source_repo
360 source_ref_id = pull_request.source_ref_parts.commit_id
360 source_ref_id = pull_request.source_ref_parts.commit_id
361 target_ref_id = pull_request.target_ref_parts.commit_id
361 target_ref_id = pull_request.target_ref_parts.commit_id
362 diff = PullRequestModel()._get_diff_from_pr_or_version(
362 diff = PullRequestModel()._get_diff_from_pr_or_version(
363 source_repo, source_ref_id, target_ref_id, context=6)
363 source_repo, source_ref_id, target_ref_id, context=6)
364 assert 'file_1' in diff.raw
364 assert 'file_1' in diff.raw
365
365
366 def test_generate_title_returns_unicode(self):
366 def test_generate_title_returns_unicode(self):
367 title = PullRequestModel().generate_pullrequest_title(
367 title = PullRequestModel().generate_pullrequest_title(
368 source='source-dummy',
368 source='source-dummy',
369 source_ref='source-ref-dummy',
369 source_ref='source-ref-dummy',
370 target='target-dummy',
370 target='target-dummy',
371 )
371 )
372 assert type(title) == unicode
372 assert type(title) == unicode
373
373
374
374
375 class TestIntegrationMerge(object):
375 class TestIntegrationMerge(object):
376 @pytest.mark.parametrize('extra_config', (
376 @pytest.mark.parametrize('extra_config', (
377 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
377 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
378 ))
378 ))
379 def test_merge_triggers_push_hooks(
379 def test_merge_triggers_push_hooks(
380 self, pr_util, user_admin, capture_rcextensions, merge_extras,
380 self, pr_util, user_admin, capture_rcextensions, merge_extras,
381 extra_config):
381 extra_config):
382 pull_request = pr_util.create_pull_request(
382 pull_request = pr_util.create_pull_request(
383 approved=True, mergeable=True)
383 approved=True, mergeable=True)
384 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
384 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
385 merge_extras['repository'] = pull_request.target_repo.repo_name
385 merge_extras['repository'] = pull_request.target_repo.repo_name
386 Session().commit()
386 Session().commit()
387
387
388 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
388 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
389 merge_state = PullRequestModel().merge(
389 merge_state = PullRequestModel().merge(
390 pull_request, user_admin, extras=merge_extras)
390 pull_request, user_admin, extras=merge_extras)
391
391
392 assert merge_state.executed
392 assert merge_state.executed
393 assert 'pre_push' in capture_rcextensions
393 assert 'pre_push' in capture_rcextensions
394 assert 'post_push' in capture_rcextensions
394 assert 'post_push' in capture_rcextensions
395
395
396 def test_merge_can_be_rejected_by_pre_push_hook(
396 def test_merge_can_be_rejected_by_pre_push_hook(
397 self, pr_util, user_admin, capture_rcextensions, merge_extras):
397 self, pr_util, user_admin, capture_rcextensions, merge_extras):
398 pull_request = pr_util.create_pull_request(
398 pull_request = pr_util.create_pull_request(
399 approved=True, mergeable=True)
399 approved=True, mergeable=True)
400 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
400 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
401 merge_extras['repository'] = pull_request.target_repo.repo_name
401 merge_extras['repository'] = pull_request.target_repo.repo_name
402 Session().commit()
402 Session().commit()
403
403
404 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
404 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
405 pre_pull.side_effect = RepositoryError("Disallow push!")
405 pre_pull.side_effect = RepositoryError("Disallow push!")
406 merge_status = PullRequestModel().merge(
406 merge_status = PullRequestModel().merge(
407 pull_request, user_admin, extras=merge_extras)
407 pull_request, user_admin, extras=merge_extras)
408
408
409 assert not merge_status.executed
409 assert not merge_status.executed
410 assert 'pre_push' not in capture_rcextensions
410 assert 'pre_push' not in capture_rcextensions
411 assert 'post_push' not in capture_rcextensions
411 assert 'post_push' not in capture_rcextensions
412
412
413 def test_merge_fails_if_target_is_locked(
413 def test_merge_fails_if_target_is_locked(
414 self, pr_util, user_regular, merge_extras):
414 self, pr_util, user_regular, merge_extras):
415 pull_request = pr_util.create_pull_request(
415 pull_request = pr_util.create_pull_request(
416 approved=True, mergeable=True)
416 approved=True, mergeable=True)
417 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
417 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
418 pull_request.target_repo.locked = locked_by
418 pull_request.target_repo.locked = locked_by
419 # TODO: johbo: Check if this can work based on the database, currently
419 # TODO: johbo: Check if this can work based on the database, currently
420 # all data is pre-computed, that's why just updating the DB is not
420 # all data is pre-computed, that's why just updating the DB is not
421 # enough.
421 # enough.
422 merge_extras['locked_by'] = locked_by
422 merge_extras['locked_by'] = locked_by
423 merge_extras['repository'] = pull_request.target_repo.repo_name
423 merge_extras['repository'] = pull_request.target_repo.repo_name
424 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
424 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
425 Session().commit()
425 Session().commit()
426 merge_status = PullRequestModel().merge(
426 merge_status = PullRequestModel().merge(
427 pull_request, user_regular, extras=merge_extras)
427 pull_request, user_regular, extras=merge_extras)
428 assert not merge_status.executed
428 assert not merge_status.executed
429
429
430
430
431 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
431 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
432 (False, 1, 0),
432 (False, 1, 0),
433 (True, 0, 1),
433 (True, 0, 1),
434 ])
434 ])
435 def test_outdated_comments(
435 def test_outdated_comments(
436 pr_util, use_outdated, inlines_count, outdated_count):
436 pr_util, use_outdated, inlines_count, outdated_count):
437 pull_request = pr_util.create_pull_request()
437 pull_request = pr_util.create_pull_request()
438 pr_util.create_inline_comment(file_path='not_in_updated_diff')
438 pr_util.create_inline_comment(file_path='not_in_updated_diff')
439
439
440 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
440 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
441 pr_util.add_one_commit()
441 pr_util.add_one_commit()
442 assert_inline_comments(
442 assert_inline_comments(
443 pull_request, visible=inlines_count, outdated=outdated_count)
443 pull_request, visible=inlines_count, outdated=outdated_count)
444 outdated_comment_mock.assert_called_with(pull_request)
444 outdated_comment_mock.assert_called_with(pull_request)
445
445
446
446
447 @pytest.fixture
447 @pytest.fixture
448 def merge_extras(user_regular):
448 def merge_extras(user_regular):
449 """
449 """
450 Context for the vcs operation when running a merge.
450 Context for the vcs operation when running a merge.
451 """
451 """
452 extras = {
452 extras = {
453 'ip': '127.0.0.1',
453 'ip': '127.0.0.1',
454 'username': user_regular.username,
454 'username': user_regular.username,
455 'action': 'push',
455 'action': 'push',
456 'repository': 'fake_target_repo_name',
456 'repository': 'fake_target_repo_name',
457 'scm': 'git',
457 'scm': 'git',
458 'config': 'fake_config_ini_path',
458 'config': 'fake_config_ini_path',
459 'make_lock': None,
459 'make_lock': None,
460 'locked_by': [None, None, None],
460 'locked_by': [None, None, None],
461 'server_url': 'http://test.example.com:5000',
461 'server_url': 'http://test.example.com:5000',
462 'hooks': ['push', 'pull'],
462 'hooks': ['push', 'pull'],
463 'is_shadow_repo': False,
463 'is_shadow_repo': False,
464 }
464 }
465 return extras
465 return extras
466
466
467
467
468 class TestUpdateCommentHandling(object):
468 class TestUpdateCommentHandling(object):
469
469
470 @pytest.fixture(autouse=True, scope='class')
470 @pytest.fixture(autouse=True, scope='class')
471 def enable_outdated_comments(self, request, pylonsapp):
471 def enable_outdated_comments(self, request, pylonsapp):
472 config_patch = mock.patch.dict(
472 config_patch = mock.patch.dict(
473 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
473 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
474 config_patch.start()
474 config_patch.start()
475
475
476 @request.addfinalizer
476 @request.addfinalizer
477 def cleanup():
477 def cleanup():
478 config_patch.stop()
478 config_patch.stop()
479
479
480 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
480 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
481 commits = [
481 commits = [
482 {'message': 'a'},
482 {'message': 'a'},
483 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
483 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
484 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
484 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
485 ]
485 ]
486 pull_request = pr_util.create_pull_request(
486 pull_request = pr_util.create_pull_request(
487 commits=commits, target_head='a', source_head='b', revisions=['b'])
487 commits=commits, target_head='a', source_head='b', revisions=['b'])
488 pr_util.create_inline_comment(file_path='file_b')
488 pr_util.create_inline_comment(file_path='file_b')
489 pr_util.add_one_commit(head='c')
489 pr_util.add_one_commit(head='c')
490
490
491 assert_inline_comments(pull_request, visible=1, outdated=0)
491 assert_inline_comments(pull_request, visible=1, outdated=0)
492
492
493 def test_comment_stays_unflagged_on_change_above(self, pr_util):
493 def test_comment_stays_unflagged_on_change_above(self, pr_util):
494 original_content = ''.join(
494 original_content = ''.join(
495 ['line {}\n'.format(x) for x in range(1, 11)])
495 ['line {}\n'.format(x) for x in range(1, 11)])
496 updated_content = 'new_line_at_top\n' + original_content
496 updated_content = 'new_line_at_top\n' + original_content
497 commits = [
497 commits = [
498 {'message': 'a'},
498 {'message': 'a'},
499 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
499 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
500 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
500 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
501 ]
501 ]
502 pull_request = pr_util.create_pull_request(
502 pull_request = pr_util.create_pull_request(
503 commits=commits, target_head='a', source_head='b', revisions=['b'])
503 commits=commits, target_head='a', source_head='b', revisions=['b'])
504
504
505 with outdated_comments_patcher():
505 with outdated_comments_patcher():
506 comment = pr_util.create_inline_comment(
506 comment = pr_util.create_inline_comment(
507 line_no=u'n8', file_path='file_b')
507 line_no=u'n8', file_path='file_b')
508 pr_util.add_one_commit(head='c')
508 pr_util.add_one_commit(head='c')
509
509
510 assert_inline_comments(pull_request, visible=1, outdated=0)
510 assert_inline_comments(pull_request, visible=1, outdated=0)
511 assert comment.line_no == u'n9'
511 assert comment.line_no == u'n9'
512
512
513 def test_comment_stays_unflagged_on_change_below(self, pr_util):
513 def test_comment_stays_unflagged_on_change_below(self, pr_util):
514 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
514 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
515 updated_content = original_content + 'new_line_at_end\n'
515 updated_content = original_content + 'new_line_at_end\n'
516 commits = [
516 commits = [
517 {'message': 'a'},
517 {'message': 'a'},
518 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
518 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
519 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
519 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
520 ]
520 ]
521 pull_request = pr_util.create_pull_request(
521 pull_request = pr_util.create_pull_request(
522 commits=commits, target_head='a', source_head='b', revisions=['b'])
522 commits=commits, target_head='a', source_head='b', revisions=['b'])
523 pr_util.create_inline_comment(file_path='file_b')
523 pr_util.create_inline_comment(file_path='file_b')
524 pr_util.add_one_commit(head='c')
524 pr_util.add_one_commit(head='c')
525
525
526 assert_inline_comments(pull_request, visible=1, outdated=0)
526 assert_inline_comments(pull_request, visible=1, outdated=0)
527
527
528 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
528 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
529 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
529 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
530 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
530 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
531 change_lines = list(base_lines)
531 change_lines = list(base_lines)
532 change_lines.insert(6, 'line 6a added\n')
532 change_lines.insert(6, 'line 6a added\n')
533
533
534 # Changes on the last line of sight
534 # Changes on the last line of sight
535 update_lines = list(change_lines)
535 update_lines = list(change_lines)
536 update_lines[0] = 'line 1 changed\n'
536 update_lines[0] = 'line 1 changed\n'
537 update_lines[-1] = 'line 12 changed\n'
537 update_lines[-1] = 'line 12 changed\n'
538
538
539 def file_b(lines):
539 def file_b(lines):
540 return FileNode('file_b', ''.join(lines))
540 return FileNode('file_b', ''.join(lines))
541
541
542 commits = [
542 commits = [
543 {'message': 'a', 'added': [file_b(base_lines)]},
543 {'message': 'a', 'added': [file_b(base_lines)]},
544 {'message': 'b', 'changed': [file_b(change_lines)]},
544 {'message': 'b', 'changed': [file_b(change_lines)]},
545 {'message': 'c', 'changed': [file_b(update_lines)]},
545 {'message': 'c', 'changed': [file_b(update_lines)]},
546 ]
546 ]
547
547
548 pull_request = pr_util.create_pull_request(
548 pull_request = pr_util.create_pull_request(
549 commits=commits, target_head='a', source_head='b', revisions=['b'])
549 commits=commits, target_head='a', source_head='b', revisions=['b'])
550 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
550 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
551
551
552 with outdated_comments_patcher():
552 with outdated_comments_patcher():
553 pr_util.add_one_commit(head='c')
553 pr_util.add_one_commit(head='c')
554 assert_inline_comments(pull_request, visible=0, outdated=1)
554 assert_inline_comments(pull_request, visible=0, outdated=1)
555
555
556 @pytest.mark.parametrize("change, content", [
556 @pytest.mark.parametrize("change, content", [
557 ('changed', 'changed\n'),
557 ('changed', 'changed\n'),
558 ('removed', ''),
558 ('removed', ''),
559 ], ids=['changed', 'removed'])
559 ], ids=['changed', 'removed'])
560 def test_comment_flagged_on_change(self, pr_util, change, content):
560 def test_comment_flagged_on_change(self, pr_util, change, content):
561 commits = [
561 commits = [
562 {'message': 'a'},
562 {'message': 'a'},
563 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
563 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
564 {'message': 'c', change: [FileNode('file_b', content)]},
564 {'message': 'c', change: [FileNode('file_b', content)]},
565 ]
565 ]
566 pull_request = pr_util.create_pull_request(
566 pull_request = pr_util.create_pull_request(
567 commits=commits, target_head='a', source_head='b', revisions=['b'])
567 commits=commits, target_head='a', source_head='b', revisions=['b'])
568 pr_util.create_inline_comment(file_path='file_b')
568 pr_util.create_inline_comment(file_path='file_b')
569
569
570 with outdated_comments_patcher():
570 with outdated_comments_patcher():
571 pr_util.add_one_commit(head='c')
571 pr_util.add_one_commit(head='c')
572 assert_inline_comments(pull_request, visible=0, outdated=1)
572 assert_inline_comments(pull_request, visible=0, outdated=1)
573
573
574
574
575 class TestUpdateChangedFiles(object):
575 class TestUpdateChangedFiles(object):
576
576
577 def test_no_changes_on_unchanged_diff(self, pr_util):
577 def test_no_changes_on_unchanged_diff(self, pr_util):
578 commits = [
578 commits = [
579 {'message': 'a'},
579 {'message': 'a'},
580 {'message': 'b',
580 {'message': 'b',
581 'added': [FileNode('file_b', 'test_content b\n')]},
581 'added': [FileNode('file_b', 'test_content b\n')]},
582 {'message': 'c',
582 {'message': 'c',
583 'added': [FileNode('file_c', 'test_content c\n')]},
583 'added': [FileNode('file_c', 'test_content c\n')]},
584 ]
584 ]
585 # open a PR from a to b, adding file_b
585 # open a PR from a to b, adding file_b
586 pull_request = pr_util.create_pull_request(
586 pull_request = pr_util.create_pull_request(
587 commits=commits, target_head='a', source_head='b', revisions=['b'],
587 commits=commits, target_head='a', source_head='b', revisions=['b'],
588 name_suffix='per-file-review')
588 name_suffix='per-file-review')
589
589
590 # modify PR adding new file file_c
590 # modify PR adding new file file_c
591 pr_util.add_one_commit(head='c')
591 pr_util.add_one_commit(head='c')
592
592
593 assert_pr_file_changes(
593 assert_pr_file_changes(
594 pull_request,
594 pull_request,
595 added=['file_c'],
595 added=['file_c'],
596 modified=[],
596 modified=[],
597 removed=[])
597 removed=[])
598
598
599 def test_modify_and_undo_modification_diff(self, pr_util):
599 def test_modify_and_undo_modification_diff(self, pr_util):
600 commits = [
600 commits = [
601 {'message': 'a'},
601 {'message': 'a'},
602 {'message': 'b',
602 {'message': 'b',
603 'added': [FileNode('file_b', 'test_content b\n')]},
603 'added': [FileNode('file_b', 'test_content b\n')]},
604 {'message': 'c',
604 {'message': 'c',
605 'changed': [FileNode('file_b', 'test_content b modified\n')]},
605 'changed': [FileNode('file_b', 'test_content b modified\n')]},
606 {'message': 'd',
606 {'message': 'd',
607 'changed': [FileNode('file_b', 'test_content b\n')]},
607 'changed': [FileNode('file_b', 'test_content b\n')]},
608 ]
608 ]
609 # open a PR from a to b, adding file_b
609 # open a PR from a to b, adding file_b
610 pull_request = pr_util.create_pull_request(
610 pull_request = pr_util.create_pull_request(
611 commits=commits, target_head='a', source_head='b', revisions=['b'],
611 commits=commits, target_head='a', source_head='b', revisions=['b'],
612 name_suffix='per-file-review')
612 name_suffix='per-file-review')
613
613
614 # modify PR modifying file file_b
614 # modify PR modifying file file_b
615 pr_util.add_one_commit(head='c')
615 pr_util.add_one_commit(head='c')
616
616
617 assert_pr_file_changes(
617 assert_pr_file_changes(
618 pull_request,
618 pull_request,
619 added=[],
619 added=[],
620 modified=['file_b'],
620 modified=['file_b'],
621 removed=[])
621 removed=[])
622
622
623 # move the head again to d, which rollbacks change,
623 # move the head again to d, which rollbacks change,
624 # meaning we should indicate no changes
624 # meaning we should indicate no changes
625 pr_util.add_one_commit(head='d')
625 pr_util.add_one_commit(head='d')
626
626
627 assert_pr_file_changes(
627 assert_pr_file_changes(
628 pull_request,
628 pull_request,
629 added=[],
629 added=[],
630 modified=[],
630 modified=[],
631 removed=[])
631 removed=[])
632
632
633 def test_updated_all_files_in_pr(self, pr_util):
633 def test_updated_all_files_in_pr(self, pr_util):
634 commits = [
634 commits = [
635 {'message': 'a'},
635 {'message': 'a'},
636 {'message': 'b', 'added': [
636 {'message': 'b', 'added': [
637 FileNode('file_a', 'test_content a\n'),
637 FileNode('file_a', 'test_content a\n'),
638 FileNode('file_b', 'test_content b\n'),
638 FileNode('file_b', 'test_content b\n'),
639 FileNode('file_c', 'test_content c\n')]},
639 FileNode('file_c', 'test_content c\n')]},
640 {'message': 'c', 'changed': [
640 {'message': 'c', 'changed': [
641 FileNode('file_a', 'test_content a changed\n'),
641 FileNode('file_a', 'test_content a changed\n'),
642 FileNode('file_b', 'test_content b changed\n'),
642 FileNode('file_b', 'test_content b changed\n'),
643 FileNode('file_c', 'test_content c changed\n')]},
643 FileNode('file_c', 'test_content c changed\n')]},
644 ]
644 ]
645 # open a PR from a to b, changing 3 files
645 # open a PR from a to b, changing 3 files
646 pull_request = pr_util.create_pull_request(
646 pull_request = pr_util.create_pull_request(
647 commits=commits, target_head='a', source_head='b', revisions=['b'],
647 commits=commits, target_head='a', source_head='b', revisions=['b'],
648 name_suffix='per-file-review')
648 name_suffix='per-file-review')
649
649
650 pr_util.add_one_commit(head='c')
650 pr_util.add_one_commit(head='c')
651
651
652 assert_pr_file_changes(
652 assert_pr_file_changes(
653 pull_request,
653 pull_request,
654 added=[],
654 added=[],
655 modified=['file_a', 'file_b', 'file_c'],
655 modified=['file_a', 'file_b', 'file_c'],
656 removed=[])
656 removed=[])
657
657
658 def test_updated_and_removed_all_files_in_pr(self, pr_util):
658 def test_updated_and_removed_all_files_in_pr(self, pr_util):
659 commits = [
659 commits = [
660 {'message': 'a'},
660 {'message': 'a'},
661 {'message': 'b', 'added': [
661 {'message': 'b', 'added': [
662 FileNode('file_a', 'test_content a\n'),
662 FileNode('file_a', 'test_content a\n'),
663 FileNode('file_b', 'test_content b\n'),
663 FileNode('file_b', 'test_content b\n'),
664 FileNode('file_c', 'test_content c\n')]},
664 FileNode('file_c', 'test_content c\n')]},
665 {'message': 'c', 'removed': [
665 {'message': 'c', 'removed': [
666 FileNode('file_a', 'test_content a changed\n'),
666 FileNode('file_a', 'test_content a changed\n'),
667 FileNode('file_b', 'test_content b changed\n'),
667 FileNode('file_b', 'test_content b changed\n'),
668 FileNode('file_c', 'test_content c changed\n')]},
668 FileNode('file_c', 'test_content c changed\n')]},
669 ]
669 ]
670 # open a PR from a to b, removing 3 files
670 # open a PR from a to b, removing 3 files
671 pull_request = pr_util.create_pull_request(
671 pull_request = pr_util.create_pull_request(
672 commits=commits, target_head='a', source_head='b', revisions=['b'],
672 commits=commits, target_head='a', source_head='b', revisions=['b'],
673 name_suffix='per-file-review')
673 name_suffix='per-file-review')
674
674
675 pr_util.add_one_commit(head='c')
675 pr_util.add_one_commit(head='c')
676
676
677 assert_pr_file_changes(
677 assert_pr_file_changes(
678 pull_request,
678 pull_request,
679 added=[],
679 added=[],
680 modified=[],
680 modified=[],
681 removed=['file_a', 'file_b', 'file_c'])
681 removed=['file_a', 'file_b', 'file_c'])
682
682
683
683
684 def test_update_writes_snapshot_into_pull_request_version(pr_util):
684 def test_update_writes_snapshot_into_pull_request_version(pr_util):
685 model = PullRequestModel()
685 model = PullRequestModel()
686 pull_request = pr_util.create_pull_request()
686 pull_request = pr_util.create_pull_request()
687 pr_util.update_source_repository()
687 pr_util.update_source_repository()
688
688
689 model.update_commits(pull_request)
689 model.update_commits(pull_request)
690
690
691 # Expect that it has a version entry now
691 # Expect that it has a version entry now
692 assert len(model.get_versions(pull_request)) == 1
692 assert len(model.get_versions(pull_request)) == 1
693
693
694
694
695 def test_update_skips_new_version_if_unchanged(pr_util):
695 def test_update_skips_new_version_if_unchanged(pr_util):
696 pull_request = pr_util.create_pull_request()
696 pull_request = pr_util.create_pull_request()
697 model = PullRequestModel()
697 model = PullRequestModel()
698 model.update_commits(pull_request)
698 model.update_commits(pull_request)
699
699
700 # Expect that it still has no versions
700 # Expect that it still has no versions
701 assert len(model.get_versions(pull_request)) == 0
701 assert len(model.get_versions(pull_request)) == 0
702
702
703
703
704 def test_update_assigns_comments_to_the_new_version(pr_util):
704 def test_update_assigns_comments_to_the_new_version(pr_util):
705 model = PullRequestModel()
705 model = PullRequestModel()
706 pull_request = pr_util.create_pull_request()
706 pull_request = pr_util.create_pull_request()
707 comment = pr_util.create_comment()
707 comment = pr_util.create_comment()
708 pr_util.update_source_repository()
708 pr_util.update_source_repository()
709
709
710 model.update_commits(pull_request)
710 model.update_commits(pull_request)
711
711
712 # Expect that the comment is linked to the pr version now
712 # Expect that the comment is linked to the pr version now
713 assert comment.pull_request_version == model.get_versions(pull_request)[0]
713 assert comment.pull_request_version == model.get_versions(pull_request)[0]
714
714
715
715
716 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
716 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
717 model = PullRequestModel()
717 model = PullRequestModel()
718 pull_request = pr_util.create_pull_request()
718 pull_request = pr_util.create_pull_request()
719 pr_util.update_source_repository()
719 pr_util.update_source_repository()
720 pr_util.update_source_repository()
720 pr_util.update_source_repository()
721
721
722 model.update_commits(pull_request)
722 model.update_commits(pull_request)
723
723
724 # Expect to find a new comment about the change
724 # Expect to find a new comment about the change
725 expected_message = textwrap.dedent(
725 expected_message = textwrap.dedent(
726 """\
726 """\
727 Pull request updated. Auto status change to |under_review|
727 Pull request updated. Auto status change to |under_review|
728
728
729 .. role:: added
729 .. role:: added
730 .. role:: removed
730 .. role:: removed
731 .. parsed-literal::
731 .. parsed-literal::
732
732
733 Changed commits:
733 Changed commits:
734 * :added:`1 added`
734 * :added:`1 added`
735 * :removed:`0 removed`
735 * :removed:`0 removed`
736
736
737 Changed files:
737 Changed files:
738 * `A file_2 <#a_c--92ed3b5f07b4>`_
738 * `A file_2 <#a_c--92ed3b5f07b4>`_
739
739
740 .. |under_review| replace:: *"Under Review"*"""
740 .. |under_review| replace:: *"Under Review"*"""
741 )
741 )
742 pull_request_comments = sorted(
742 pull_request_comments = sorted(
743 pull_request.comments, key=lambda c: c.modified_at)
743 pull_request.comments, key=lambda c: c.modified_at)
744 update_comment = pull_request_comments[-1]
744 update_comment = pull_request_comments[-1]
745 assert update_comment.text == expected_message
745 assert update_comment.text == expected_message
746
746
747
747
748 def test_create_version_from_snapshot_updates_attributes(pr_util):
748 def test_create_version_from_snapshot_updates_attributes(pr_util):
749 pull_request = pr_util.create_pull_request()
749 pull_request = pr_util.create_pull_request()
750
750
751 # Avoiding default values
751 # Avoiding default values
752 pull_request.status = PullRequest.STATUS_CLOSED
752 pull_request.status = PullRequest.STATUS_CLOSED
753 pull_request._last_merge_source_rev = "0" * 40
753 pull_request._last_merge_source_rev = "0" * 40
754 pull_request._last_merge_target_rev = "1" * 40
754 pull_request._last_merge_target_rev = "1" * 40
755 pull_request._last_merge_status = 1
755 pull_request._last_merge_status = 1
756 pull_request.merge_rev = "2" * 40
756 pull_request.merge_rev = "2" * 40
757
757
758 # Remember automatic values
758 # Remember automatic values
759 created_on = pull_request.created_on
759 created_on = pull_request.created_on
760 updated_on = pull_request.updated_on
760 updated_on = pull_request.updated_on
761
761
762 # Create a new version of the pull request
762 # Create a new version of the pull request
763 version = PullRequestModel()._create_version_from_snapshot(pull_request)
763 version = PullRequestModel()._create_version_from_snapshot(pull_request)
764
764
765 # Check attributes
765 # Check attributes
766 assert version.title == pr_util.create_parameters['title']
766 assert version.title == pr_util.create_parameters['title']
767 assert version.description == pr_util.create_parameters['description']
767 assert version.description == pr_util.create_parameters['description']
768 assert version.status == PullRequest.STATUS_CLOSED
768 assert version.status == PullRequest.STATUS_CLOSED
769
769
770 # versions get updated created_on
770 # versions get updated created_on
771 assert version.created_on != created_on
771 assert version.created_on != created_on
772
772
773 assert version.updated_on == updated_on
773 assert version.updated_on == updated_on
774 assert version.user_id == pull_request.user_id
774 assert version.user_id == pull_request.user_id
775 assert version.revisions == pr_util.create_parameters['revisions']
775 assert version.revisions == pr_util.create_parameters['revisions']
776 assert version.source_repo == pr_util.source_repository
776 assert version.source_repo == pr_util.source_repository
777 assert version.source_ref == pr_util.create_parameters['source_ref']
777 assert version.source_ref == pr_util.create_parameters['source_ref']
778 assert version.target_repo == pr_util.target_repository
778 assert version.target_repo == pr_util.target_repository
779 assert version.target_ref == pr_util.create_parameters['target_ref']
779 assert version.target_ref == pr_util.create_parameters['target_ref']
780 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
780 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
781 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
781 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
782 assert version._last_merge_status == pull_request._last_merge_status
782 assert version._last_merge_status == pull_request._last_merge_status
783 assert version.merge_rev == pull_request.merge_rev
783 assert version.merge_rev == pull_request.merge_rev
784 assert version.pull_request == pull_request
784 assert version.pull_request == pull_request
785
785
786
786
787 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
787 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
788 version1 = pr_util.create_version_of_pull_request()
788 version1 = pr_util.create_version_of_pull_request()
789 comment_linked = pr_util.create_comment(linked_to=version1)
789 comment_linked = pr_util.create_comment(linked_to=version1)
790 comment_unlinked = pr_util.create_comment()
790 comment_unlinked = pr_util.create_comment()
791 version2 = pr_util.create_version_of_pull_request()
791 version2 = pr_util.create_version_of_pull_request()
792
792
793 PullRequestModel()._link_comments_to_version(version2)
793 PullRequestModel()._link_comments_to_version(version2)
794
794
795 # Expect that only the new comment is linked to version2
795 # Expect that only the new comment is linked to version2
796 assert (
796 assert (
797 comment_unlinked.pull_request_version_id ==
797 comment_unlinked.pull_request_version_id ==
798 version2.pull_request_version_id)
798 version2.pull_request_version_id)
799 assert (
799 assert (
800 comment_linked.pull_request_version_id ==
800 comment_linked.pull_request_version_id ==
801 version1.pull_request_version_id)
801 version1.pull_request_version_id)
802 assert (
802 assert (
803 comment_unlinked.pull_request_version_id !=
803 comment_unlinked.pull_request_version_id !=
804 comment_linked.pull_request_version_id)
804 comment_linked.pull_request_version_id)
805
805
806
806
807 def test_calculate_commits():
807 def test_calculate_commits():
808 old_ids = [1, 2, 3]
808 old_ids = [1, 2, 3]
809 new_ids = [1, 3, 4, 5]
809 new_ids = [1, 3, 4, 5]
810 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
810 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
811 assert change.added == [4, 5]
811 assert change.added == [4, 5]
812 assert change.common == [1, 3]
812 assert change.common == [1, 3]
813 assert change.removed == [2]
813 assert change.removed == [2]
814 assert change.total == [1, 3, 4, 5]
814 assert change.total == [1, 3, 4, 5]
815
815
816
816
817 def assert_inline_comments(pull_request, visible=None, outdated=None):
817 def assert_inline_comments(pull_request, visible=None, outdated=None):
818 if visible is not None:
818 if visible is not None:
819 inline_comments = CommentsModel().get_inline_comments(
819 inline_comments = CommentsModel().get_inline_comments(
820 pull_request.target_repo.repo_id, pull_request=pull_request)
820 pull_request.target_repo.repo_id, pull_request=pull_request)
821 inline_cnt = CommentsModel().get_inline_comments_count(
821 inline_cnt = CommentsModel().get_inline_comments_count(
822 inline_comments)
822 inline_comments)
823 assert inline_cnt == visible
823 assert inline_cnt == visible
824 if outdated is not None:
824 if outdated is not None:
825 outdated_comments = CommentsModel().get_outdated_comments(
825 outdated_comments = CommentsModel().get_outdated_comments(
826 pull_request.target_repo.repo_id, pull_request)
826 pull_request.target_repo.repo_id, pull_request)
827 assert len(outdated_comments) == outdated
827 assert len(outdated_comments) == outdated
828
828
829
829
830 def assert_pr_file_changes(
830 def assert_pr_file_changes(
831 pull_request, added=None, modified=None, removed=None):
831 pull_request, added=None, modified=None, removed=None):
832 pr_versions = PullRequestModel().get_versions(pull_request)
832 pr_versions = PullRequestModel().get_versions(pull_request)
833 # always use first version, ie original PR to calculate changes
833 # always use first version, ie original PR to calculate changes
834 pull_request_version = pr_versions[0]
834 pull_request_version = pr_versions[0]
835 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
835 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
836 pull_request, pull_request_version)
836 pull_request, pull_request_version)
837 file_changes = PullRequestModel()._calculate_file_changes(
837 file_changes = PullRequestModel()._calculate_file_changes(
838 old_diff_data, new_diff_data)
838 old_diff_data, new_diff_data)
839
839
840 assert added == file_changes.added, \
840 assert added == file_changes.added, \
841 'expected added:%s vs value:%s' % (added, file_changes.added)
841 'expected added:%s vs value:%s' % (added, file_changes.added)
842 assert modified == file_changes.modified, \
842 assert modified == file_changes.modified, \
843 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
843 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
844 assert removed == file_changes.removed, \
844 assert removed == file_changes.removed, \
845 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
845 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
846
846
847
847
848 def outdated_comments_patcher(use_outdated=True):
848 def outdated_comments_patcher(use_outdated=True):
849 return mock.patch.object(
849 return mock.patch.object(
850 CommentsModel, 'use_outdated_comments',
850 CommentsModel, 'use_outdated_comments',
851 return_value=use_outdated)
851 return_value=use_outdated)
@@ -1,1814 +1,1813 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess32
29 import subprocess32
30 import time
30 import time
31 import uuid
31 import uuid
32 import dateutil.tz
32 import dateutil.tz
33
33
34 import mock
34 import mock
35 import pyramid.testing
35 import pyramid.testing
36 import pytest
36 import pytest
37 import colander
37 import colander
38 import requests
38 import requests
39
39
40 import rhodecode
40 import rhodecode
41 from rhodecode.lib.utils2 import AttributeDict
41 from rhodecode.lib.utils2 import AttributeDict
42 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.changeset_status import ChangesetStatusModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
45 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
46 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
46 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.user import UserModel
51 from rhodecode.model.user import UserModel
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.user_group import UserGroupModel
54 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.model.integration import IntegrationModel
55 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations import integration_type_registry
56 from rhodecode.integrations.types.base import IntegrationTypeBase
56 from rhodecode.integrations.types.base import IntegrationTypeBase
57 from rhodecode.lib.utils import repo2db_mapper
57 from rhodecode.lib.utils import repo2db_mapper
58 from rhodecode.lib.vcs import create_vcsserver_proxy
58 from rhodecode.lib.vcs import create_vcsserver_proxy
59 from rhodecode.lib.vcs.backends import get_backend
59 from rhodecode.lib.vcs.backends import get_backend
60 from rhodecode.lib.vcs.nodes import FileNode
60 from rhodecode.lib.vcs.nodes import FileNode
61 from rhodecode.tests import (
61 from rhodecode.tests import (
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
64 TEST_USER_REGULAR_PASS)
64 TEST_USER_REGULAR_PASS)
65 from rhodecode.tests.utils import CustomTestApp
65 from rhodecode.tests.utils import CustomTestApp
66 from rhodecode.tests.fixture import Fixture
66 from rhodecode.tests.fixture import Fixture
67
67
68
68
69 def _split_comma(value):
69 def _split_comma(value):
70 return value.split(',')
70 return value.split(',')
71
71
72
72
73 def pytest_addoption(parser):
73 def pytest_addoption(parser):
74 parser.addoption(
74 parser.addoption(
75 '--keep-tmp-path', action='store_true',
75 '--keep-tmp-path', action='store_true',
76 help="Keep the test temporary directories")
76 help="Keep the test temporary directories")
77 parser.addoption(
77 parser.addoption(
78 '--backends', action='store', type=_split_comma,
78 '--backends', action='store', type=_split_comma,
79 default=['git', 'hg', 'svn'],
79 default=['git', 'hg', 'svn'],
80 help="Select which backends to test for backend specific tests.")
80 help="Select which backends to test for backend specific tests.")
81 parser.addoption(
81 parser.addoption(
82 '--dbs', action='store', type=_split_comma,
82 '--dbs', action='store', type=_split_comma,
83 default=['sqlite'],
83 default=['sqlite'],
84 help="Select which database to test for database specific tests. "
84 help="Select which database to test for database specific tests. "
85 "Possible options are sqlite,postgres,mysql")
85 "Possible options are sqlite,postgres,mysql")
86 parser.addoption(
86 parser.addoption(
87 '--appenlight', '--ae', action='store_true',
87 '--appenlight', '--ae', action='store_true',
88 help="Track statistics in appenlight.")
88 help="Track statistics in appenlight.")
89 parser.addoption(
89 parser.addoption(
90 '--appenlight-api-key', '--ae-key',
90 '--appenlight-api-key', '--ae-key',
91 help="API key for Appenlight.")
91 help="API key for Appenlight.")
92 parser.addoption(
92 parser.addoption(
93 '--appenlight-url', '--ae-url',
93 '--appenlight-url', '--ae-url',
94 default="https://ae.rhodecode.com",
94 default="https://ae.rhodecode.com",
95 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
95 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
96 parser.addoption(
96 parser.addoption(
97 '--sqlite-connection-string', action='store',
97 '--sqlite-connection-string', action='store',
98 default='', help="Connection string for the dbs tests with SQLite")
98 default='', help="Connection string for the dbs tests with SQLite")
99 parser.addoption(
99 parser.addoption(
100 '--postgres-connection-string', action='store',
100 '--postgres-connection-string', action='store',
101 default='', help="Connection string for the dbs tests with Postgres")
101 default='', help="Connection string for the dbs tests with Postgres")
102 parser.addoption(
102 parser.addoption(
103 '--mysql-connection-string', action='store',
103 '--mysql-connection-string', action='store',
104 default='', help="Connection string for the dbs tests with MySQL")
104 default='', help="Connection string for the dbs tests with MySQL")
105 parser.addoption(
105 parser.addoption(
106 '--repeat', type=int, default=100,
106 '--repeat', type=int, default=100,
107 help="Number of repetitions in performance tests.")
107 help="Number of repetitions in performance tests.")
108
108
109
109
110 def pytest_configure(config):
110 def pytest_configure(config):
111 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
111 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
112 from rhodecode.config import patches
112 from rhodecode.config import patches
113 patches.kombu_1_5_1_python_2_7_11()
113 patches.kombu_1_5_1_python_2_7_11()
114
114
115
115
116 def pytest_collection_modifyitems(session, config, items):
116 def pytest_collection_modifyitems(session, config, items):
117 # nottest marked, compare nose, used for transition from nose to pytest
117 # nottest marked, compare nose, used for transition from nose to pytest
118 remaining = [
118 remaining = [
119 i for i in items if getattr(i.obj, '__test__', True)]
119 i for i in items if getattr(i.obj, '__test__', True)]
120 items[:] = remaining
120 items[:] = remaining
121
121
122
122
123 def pytest_generate_tests(metafunc):
123 def pytest_generate_tests(metafunc):
124 # Support test generation based on --backend parameter
124 # Support test generation based on --backend parameter
125 if 'backend_alias' in metafunc.fixturenames:
125 if 'backend_alias' in metafunc.fixturenames:
126 backends = get_backends_from_metafunc(metafunc)
126 backends = get_backends_from_metafunc(metafunc)
127 scope = None
127 scope = None
128 if not backends:
128 if not backends:
129 pytest.skip("Not enabled for any of selected backends")
129 pytest.skip("Not enabled for any of selected backends")
130 metafunc.parametrize('backend_alias', backends, scope=scope)
130 metafunc.parametrize('backend_alias', backends, scope=scope)
131 elif hasattr(metafunc.function, 'backends'):
131 elif hasattr(metafunc.function, 'backends'):
132 backends = get_backends_from_metafunc(metafunc)
132 backends = get_backends_from_metafunc(metafunc)
133 if not backends:
133 if not backends:
134 pytest.skip("Not enabled for any of selected backends")
134 pytest.skip("Not enabled for any of selected backends")
135
135
136
136
137 def get_backends_from_metafunc(metafunc):
137 def get_backends_from_metafunc(metafunc):
138 requested_backends = set(metafunc.config.getoption('--backends'))
138 requested_backends = set(metafunc.config.getoption('--backends'))
139 if hasattr(metafunc.function, 'backends'):
139 if hasattr(metafunc.function, 'backends'):
140 # Supported backends by this test function, created from
140 # Supported backends by this test function, created from
141 # pytest.mark.backends
141 # pytest.mark.backends
142 backends = metafunc.function.backends.args
142 backends = metafunc.function.backends.args
143 elif hasattr(metafunc.cls, 'backend_alias'):
143 elif hasattr(metafunc.cls, 'backend_alias'):
144 # Support class attribute "backend_alias", this is mainly
144 # Support class attribute "backend_alias", this is mainly
145 # for legacy reasons for tests not yet using pytest.mark.backends
145 # for legacy reasons for tests not yet using pytest.mark.backends
146 backends = [metafunc.cls.backend_alias]
146 backends = [metafunc.cls.backend_alias]
147 else:
147 else:
148 backends = metafunc.config.getoption('--backends')
148 backends = metafunc.config.getoption('--backends')
149 return requested_backends.intersection(backends)
149 return requested_backends.intersection(backends)
150
150
151
151
152 @pytest.fixture(scope='session', autouse=True)
152 @pytest.fixture(scope='session', autouse=True)
153 def activate_example_rcextensions(request):
153 def activate_example_rcextensions(request):
154 """
154 """
155 Patch in an example rcextensions module which verifies passed in kwargs.
155 Patch in an example rcextensions module which verifies passed in kwargs.
156 """
156 """
157 from rhodecode.tests.other import example_rcextensions
157 from rhodecode.tests.other import example_rcextensions
158
158
159 old_extensions = rhodecode.EXTENSIONS
159 old_extensions = rhodecode.EXTENSIONS
160 rhodecode.EXTENSIONS = example_rcextensions
160 rhodecode.EXTENSIONS = example_rcextensions
161
161
162 @request.addfinalizer
162 @request.addfinalizer
163 def cleanup():
163 def cleanup():
164 rhodecode.EXTENSIONS = old_extensions
164 rhodecode.EXTENSIONS = old_extensions
165
165
166
166
167 @pytest.fixture
167 @pytest.fixture
168 def capture_rcextensions():
168 def capture_rcextensions():
169 """
169 """
170 Returns the recorded calls to entry points in rcextensions.
170 Returns the recorded calls to entry points in rcextensions.
171 """
171 """
172 calls = rhodecode.EXTENSIONS.calls
172 calls = rhodecode.EXTENSIONS.calls
173 calls.clear()
173 calls.clear()
174 # Note: At this moment, it is still the empty dict, but that will
174 # Note: At this moment, it is still the empty dict, but that will
175 # be filled during the test run and since it is a reference this
175 # be filled during the test run and since it is a reference this
176 # is enough to make it work.
176 # is enough to make it work.
177 return calls
177 return calls
178
178
179
179
180 @pytest.fixture(scope='session')
180 @pytest.fixture(scope='session')
181 def http_environ_session():
181 def http_environ_session():
182 """
182 """
183 Allow to use "http_environ" in session scope.
183 Allow to use "http_environ" in session scope.
184 """
184 """
185 return http_environ(
185 return http_environ(
186 http_host_stub=http_host_stub())
186 http_host_stub=http_host_stub())
187
187
188
188
189 @pytest.fixture
189 @pytest.fixture
190 def http_host_stub():
190 def http_host_stub():
191 """
191 """
192 Value of HTTP_HOST in the test run.
192 Value of HTTP_HOST in the test run.
193 """
193 """
194 return 'test.example.com:80'
194 return 'test.example.com:80'
195
195
196
196
197 @pytest.fixture
197 @pytest.fixture
198 def http_environ(http_host_stub):
198 def http_environ(http_host_stub):
199 """
199 """
200 HTTP extra environ keys.
200 HTTP extra environ keys.
201
201
202 User by the test application and as well for setting up the pylons
202 User by the test application and as well for setting up the pylons
203 environment. In the case of the fixture "app" it should be possible
203 environment. In the case of the fixture "app" it should be possible
204 to override this for a specific test case.
204 to override this for a specific test case.
205 """
205 """
206 return {
206 return {
207 'SERVER_NAME': http_host_stub.split(':')[0],
207 'SERVER_NAME': http_host_stub.split(':')[0],
208 'SERVER_PORT': http_host_stub.split(':')[1],
208 'SERVER_PORT': http_host_stub.split(':')[1],
209 'HTTP_HOST': http_host_stub,
209 'HTTP_HOST': http_host_stub,
210 'HTTP_USER_AGENT': 'rc-test-agent',
210 'HTTP_USER_AGENT': 'rc-test-agent',
211 'REQUEST_METHOD': 'GET'
211 'REQUEST_METHOD': 'GET'
212 }
212 }
213
213
214
214
215 @pytest.fixture(scope='function')
215 @pytest.fixture(scope='function')
216 def app(request, pylonsapp, http_environ):
216 def app(request, pylonsapp, http_environ):
217 app = CustomTestApp(
217 app = CustomTestApp(
218 pylonsapp,
218 pylonsapp,
219 extra_environ=http_environ)
219 extra_environ=http_environ)
220 if request.cls:
220 if request.cls:
221 request.cls.app = app
221 request.cls.app = app
222 return app
222 return app
223
223
224
224
225 @pytest.fixture(scope='session')
225 @pytest.fixture(scope='session')
226 def app_settings(pylonsapp, pylons_config):
226 def app_settings(pylonsapp, pylons_config):
227 """
227 """
228 Settings dictionary used to create the app.
228 Settings dictionary used to create the app.
229
229
230 Parses the ini file and passes the result through the sanitize and apply
230 Parses the ini file and passes the result through the sanitize and apply
231 defaults mechanism in `rhodecode.config.middleware`.
231 defaults mechanism in `rhodecode.config.middleware`.
232 """
232 """
233 from paste.deploy.loadwsgi import loadcontext, APP
233 from paste.deploy.loadwsgi import loadcontext, APP
234 from rhodecode.config.middleware import (
234 from rhodecode.config.middleware import (
235 sanitize_settings_and_apply_defaults)
235 sanitize_settings_and_apply_defaults)
236 context = loadcontext(APP, 'config:' + pylons_config)
236 context = loadcontext(APP, 'config:' + pylons_config)
237 settings = sanitize_settings_and_apply_defaults(context.config())
237 settings = sanitize_settings_and_apply_defaults(context.config())
238 return settings
238 return settings
239
239
240
240
241 @pytest.fixture(scope='session')
241 @pytest.fixture(scope='session')
242 def db(app_settings):
242 def db(app_settings):
243 """
243 """
244 Initializes the database connection.
244 Initializes the database connection.
245
245
246 It uses the same settings which are used to create the ``pylonsapp`` or
246 It uses the same settings which are used to create the ``pylonsapp`` or
247 ``app`` fixtures.
247 ``app`` fixtures.
248 """
248 """
249 from rhodecode.config.utils import initialize_database
249 from rhodecode.config.utils import initialize_database
250 initialize_database(app_settings)
250 initialize_database(app_settings)
251
251
252
252
253 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
253 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
254
254
255
255
256 def _autologin_user(app, *args):
256 def _autologin_user(app, *args):
257 session = login_user_session(app, *args)
257 session = login_user_session(app, *args)
258 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
258 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
259 return LoginData(csrf_token, session['rhodecode_user'])
259 return LoginData(csrf_token, session['rhodecode_user'])
260
260
261
261
262 @pytest.fixture
262 @pytest.fixture
263 def autologin_user(app):
263 def autologin_user(app):
264 """
264 """
265 Utility fixture which makes sure that the admin user is logged in
265 Utility fixture which makes sure that the admin user is logged in
266 """
266 """
267 return _autologin_user(app)
267 return _autologin_user(app)
268
268
269
269
270 @pytest.fixture
270 @pytest.fixture
271 def autologin_regular_user(app):
271 def autologin_regular_user(app):
272 """
272 """
273 Utility fixture which makes sure that the regular user is logged in
273 Utility fixture which makes sure that the regular user is logged in
274 """
274 """
275 return _autologin_user(
275 return _autologin_user(
276 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
276 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
277
277
278
278
279 @pytest.fixture(scope='function')
279 @pytest.fixture(scope='function')
280 def csrf_token(request, autologin_user):
280 def csrf_token(request, autologin_user):
281 return autologin_user.csrf_token
281 return autologin_user.csrf_token
282
282
283
283
284 @pytest.fixture(scope='function')
284 @pytest.fixture(scope='function')
285 def xhr_header(request):
285 def xhr_header(request):
286 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
286 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
287
287
288
288
289 @pytest.fixture
289 @pytest.fixture
290 def real_crypto_backend(monkeypatch):
290 def real_crypto_backend(monkeypatch):
291 """
291 """
292 Switch the production crypto backend on for this test.
292 Switch the production crypto backend on for this test.
293
293
294 During the test run the crypto backend is replaced with a faster
294 During the test run the crypto backend is replaced with a faster
295 implementation based on the MD5 algorithm.
295 implementation based on the MD5 algorithm.
296 """
296 """
297 monkeypatch.setattr(rhodecode, 'is_test', False)
297 monkeypatch.setattr(rhodecode, 'is_test', False)
298
298
299
299
300 @pytest.fixture(scope='class')
300 @pytest.fixture(scope='class')
301 def index_location(request, pylonsapp):
301 def index_location(request, pylonsapp):
302 index_location = pylonsapp.config['app_conf']['search.location']
302 index_location = pylonsapp.config['app_conf']['search.location']
303 if request.cls:
303 if request.cls:
304 request.cls.index_location = index_location
304 request.cls.index_location = index_location
305 return index_location
305 return index_location
306
306
307
307
308 @pytest.fixture(scope='session', autouse=True)
308 @pytest.fixture(scope='session', autouse=True)
309 def tests_tmp_path(request):
309 def tests_tmp_path(request):
310 """
310 """
311 Create temporary directory to be used during the test session.
311 Create temporary directory to be used during the test session.
312 """
312 """
313 if not os.path.exists(TESTS_TMP_PATH):
313 if not os.path.exists(TESTS_TMP_PATH):
314 os.makedirs(TESTS_TMP_PATH)
314 os.makedirs(TESTS_TMP_PATH)
315
315
316 if not request.config.getoption('--keep-tmp-path'):
316 if not request.config.getoption('--keep-tmp-path'):
317 @request.addfinalizer
317 @request.addfinalizer
318 def remove_tmp_path():
318 def remove_tmp_path():
319 shutil.rmtree(TESTS_TMP_PATH)
319 shutil.rmtree(TESTS_TMP_PATH)
320
320
321 return TESTS_TMP_PATH
321 return TESTS_TMP_PATH
322
322
323
323
324 @pytest.fixture
324 @pytest.fixture
325 def test_repo_group(request):
325 def test_repo_group(request):
326 """
326 """
327 Create a temporary repository group, and destroy it after
327 Create a temporary repository group, and destroy it after
328 usage automatically
328 usage automatically
329 """
329 """
330 fixture = Fixture()
330 fixture = Fixture()
331 repogroupid = 'test_repo_group_%s' % int(time.time())
331 repogroupid = 'test_repo_group_%s' % int(time.time())
332 repo_group = fixture.create_repo_group(repogroupid)
332 repo_group = fixture.create_repo_group(repogroupid)
333
333
334 def _cleanup():
334 def _cleanup():
335 fixture.destroy_repo_group(repogroupid)
335 fixture.destroy_repo_group(repogroupid)
336
336
337 request.addfinalizer(_cleanup)
337 request.addfinalizer(_cleanup)
338 return repo_group
338 return repo_group
339
339
340
340
341 @pytest.fixture
341 @pytest.fixture
342 def test_user_group(request):
342 def test_user_group(request):
343 """
343 """
344 Create a temporary user group, and destroy it after
344 Create a temporary user group, and destroy it after
345 usage automatically
345 usage automatically
346 """
346 """
347 fixture = Fixture()
347 fixture = Fixture()
348 usergroupid = 'test_user_group_%s' % int(time.time())
348 usergroupid = 'test_user_group_%s' % int(time.time())
349 user_group = fixture.create_user_group(usergroupid)
349 user_group = fixture.create_user_group(usergroupid)
350
350
351 def _cleanup():
351 def _cleanup():
352 fixture.destroy_user_group(user_group)
352 fixture.destroy_user_group(user_group)
353
353
354 request.addfinalizer(_cleanup)
354 request.addfinalizer(_cleanup)
355 return user_group
355 return user_group
356
356
357
357
358 @pytest.fixture(scope='session')
358 @pytest.fixture(scope='session')
359 def test_repo(request):
359 def test_repo(request):
360 container = TestRepoContainer()
360 container = TestRepoContainer()
361 request.addfinalizer(container._cleanup)
361 request.addfinalizer(container._cleanup)
362 return container
362 return container
363
363
364
364
365 class TestRepoContainer(object):
365 class TestRepoContainer(object):
366 """
366 """
367 Container for test repositories which are used read only.
367 Container for test repositories which are used read only.
368
368
369 Repositories will be created on demand and re-used during the lifetime
369 Repositories will be created on demand and re-used during the lifetime
370 of this object.
370 of this object.
371
371
372 Usage to get the svn test repository "minimal"::
372 Usage to get the svn test repository "minimal"::
373
373
374 test_repo = TestContainer()
374 test_repo = TestContainer()
375 repo = test_repo('minimal', 'svn')
375 repo = test_repo('minimal', 'svn')
376
376
377 """
377 """
378
378
379 dump_extractors = {
379 dump_extractors = {
380 'git': utils.extract_git_repo_from_dump,
380 'git': utils.extract_git_repo_from_dump,
381 'hg': utils.extract_hg_repo_from_dump,
381 'hg': utils.extract_hg_repo_from_dump,
382 'svn': utils.extract_svn_repo_from_dump,
382 'svn': utils.extract_svn_repo_from_dump,
383 }
383 }
384
384
385 def __init__(self):
385 def __init__(self):
386 self._cleanup_repos = []
386 self._cleanup_repos = []
387 self._fixture = Fixture()
387 self._fixture = Fixture()
388 self._repos = {}
388 self._repos = {}
389
389
390 def __call__(self, dump_name, backend_alias, config=None):
390 def __call__(self, dump_name, backend_alias, config=None):
391 key = (dump_name, backend_alias)
391 key = (dump_name, backend_alias)
392 if key not in self._repos:
392 if key not in self._repos:
393 repo = self._create_repo(dump_name, backend_alias, config)
393 repo = self._create_repo(dump_name, backend_alias, config)
394 self._repos[key] = repo.repo_id
394 self._repos[key] = repo.repo_id
395 return Repository.get(self._repos[key])
395 return Repository.get(self._repos[key])
396
396
397 def _create_repo(self, dump_name, backend_alias, config):
397 def _create_repo(self, dump_name, backend_alias, config):
398 repo_name = '%s-%s' % (backend_alias, dump_name)
398 repo_name = '%s-%s' % (backend_alias, dump_name)
399 backend_class = get_backend(backend_alias)
399 backend_class = get_backend(backend_alias)
400 dump_extractor = self.dump_extractors[backend_alias]
400 dump_extractor = self.dump_extractors[backend_alias]
401 repo_path = dump_extractor(dump_name, repo_name)
401 repo_path = dump_extractor(dump_name, repo_name)
402
402
403 vcs_repo = backend_class(repo_path, config=config)
403 vcs_repo = backend_class(repo_path, config=config)
404 repo2db_mapper({repo_name: vcs_repo})
404 repo2db_mapper({repo_name: vcs_repo})
405
405
406 repo = RepoModel().get_by_repo_name(repo_name)
406 repo = RepoModel().get_by_repo_name(repo_name)
407 self._cleanup_repos.append(repo_name)
407 self._cleanup_repos.append(repo_name)
408 return repo
408 return repo
409
409
410 def _cleanup(self):
410 def _cleanup(self):
411 for repo_name in reversed(self._cleanup_repos):
411 for repo_name in reversed(self._cleanup_repos):
412 self._fixture.destroy_repo(repo_name)
412 self._fixture.destroy_repo(repo_name)
413
413
414
414
415 @pytest.fixture
415 @pytest.fixture
416 def backend(request, backend_alias, pylonsapp, test_repo):
416 def backend(request, backend_alias, pylonsapp, test_repo):
417 """
417 """
418 Parametrized fixture which represents a single backend implementation.
418 Parametrized fixture which represents a single backend implementation.
419
419
420 It respects the option `--backends` to focus the test run on specific
420 It respects the option `--backends` to focus the test run on specific
421 backend implementations.
421 backend implementations.
422
422
423 It also supports `pytest.mark.xfail_backends` to mark tests as failing
423 It also supports `pytest.mark.xfail_backends` to mark tests as failing
424 for specific backends. This is intended as a utility for incremental
424 for specific backends. This is intended as a utility for incremental
425 development of a new backend implementation.
425 development of a new backend implementation.
426 """
426 """
427 if backend_alias not in request.config.getoption('--backends'):
427 if backend_alias not in request.config.getoption('--backends'):
428 pytest.skip("Backend %s not selected." % (backend_alias, ))
428 pytest.skip("Backend %s not selected." % (backend_alias, ))
429
429
430 utils.check_xfail_backends(request.node, backend_alias)
430 utils.check_xfail_backends(request.node, backend_alias)
431 utils.check_skip_backends(request.node, backend_alias)
431 utils.check_skip_backends(request.node, backend_alias)
432
432
433 repo_name = 'vcs_test_%s' % (backend_alias, )
433 repo_name = 'vcs_test_%s' % (backend_alias, )
434 backend = Backend(
434 backend = Backend(
435 alias=backend_alias,
435 alias=backend_alias,
436 repo_name=repo_name,
436 repo_name=repo_name,
437 test_name=request.node.name,
437 test_name=request.node.name,
438 test_repo_container=test_repo)
438 test_repo_container=test_repo)
439 request.addfinalizer(backend.cleanup)
439 request.addfinalizer(backend.cleanup)
440 return backend
440 return backend
441
441
442
442
443 @pytest.fixture
443 @pytest.fixture
444 def backend_git(request, pylonsapp, test_repo):
444 def backend_git(request, pylonsapp, test_repo):
445 return backend(request, 'git', pylonsapp, test_repo)
445 return backend(request, 'git', pylonsapp, test_repo)
446
446
447
447
448 @pytest.fixture
448 @pytest.fixture
449 def backend_hg(request, pylonsapp, test_repo):
449 def backend_hg(request, pylonsapp, test_repo):
450 return backend(request, 'hg', pylonsapp, test_repo)
450 return backend(request, 'hg', pylonsapp, test_repo)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def backend_svn(request, pylonsapp, test_repo):
454 def backend_svn(request, pylonsapp, test_repo):
455 return backend(request, 'svn', pylonsapp, test_repo)
455 return backend(request, 'svn', pylonsapp, test_repo)
456
456
457
457
458 @pytest.fixture
458 @pytest.fixture
459 def backend_random(backend_git):
459 def backend_random(backend_git):
460 """
460 """
461 Use this to express that your tests need "a backend.
461 Use this to express that your tests need "a backend.
462
462
463 A few of our tests need a backend, so that we can run the code. This
463 A few of our tests need a backend, so that we can run the code. This
464 fixture is intended to be used for such cases. It will pick one of the
464 fixture is intended to be used for such cases. It will pick one of the
465 backends and run the tests.
465 backends and run the tests.
466
466
467 The fixture `backend` would run the test multiple times for each
467 The fixture `backend` would run the test multiple times for each
468 available backend which is a pure waste of time if the test is
468 available backend which is a pure waste of time if the test is
469 independent of the backend type.
469 independent of the backend type.
470 """
470 """
471 # TODO: johbo: Change this to pick a random backend
471 # TODO: johbo: Change this to pick a random backend
472 return backend_git
472 return backend_git
473
473
474
474
475 @pytest.fixture
475 @pytest.fixture
476 def backend_stub(backend_git):
476 def backend_stub(backend_git):
477 """
477 """
478 Use this to express that your tests need a backend stub
478 Use this to express that your tests need a backend stub
479
479
480 TODO: mikhail: Implement a real stub logic instead of returning
480 TODO: mikhail: Implement a real stub logic instead of returning
481 a git backend
481 a git backend
482 """
482 """
483 return backend_git
483 return backend_git
484
484
485
485
486 @pytest.fixture
486 @pytest.fixture
487 def repo_stub(backend_stub):
487 def repo_stub(backend_stub):
488 """
488 """
489 Use this to express that your tests need a repository stub
489 Use this to express that your tests need a repository stub
490 """
490 """
491 return backend_stub.create_repo()
491 return backend_stub.create_repo()
492
492
493
493
494 class Backend(object):
494 class Backend(object):
495 """
495 """
496 Represents the test configuration for one supported backend
496 Represents the test configuration for one supported backend
497
497
498 Provides easy access to different test repositories based on
498 Provides easy access to different test repositories based on
499 `__getitem__`. Such repositories will only be created once per test
499 `__getitem__`. Such repositories will only be created once per test
500 session.
500 session.
501 """
501 """
502
502
503 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
503 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
504 _master_repo = None
504 _master_repo = None
505 _commit_ids = {}
505 _commit_ids = {}
506
506
507 def __init__(self, alias, repo_name, test_name, test_repo_container):
507 def __init__(self, alias, repo_name, test_name, test_repo_container):
508 self.alias = alias
508 self.alias = alias
509 self.repo_name = repo_name
509 self.repo_name = repo_name
510 self._cleanup_repos = []
510 self._cleanup_repos = []
511 self._test_name = test_name
511 self._test_name = test_name
512 self._test_repo_container = test_repo_container
512 self._test_repo_container = test_repo_container
513 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
513 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
514 # Fixture will survive in the end.
514 # Fixture will survive in the end.
515 self._fixture = Fixture()
515 self._fixture = Fixture()
516
516
517 def __getitem__(self, key):
517 def __getitem__(self, key):
518 return self._test_repo_container(key, self.alias)
518 return self._test_repo_container(key, self.alias)
519
519
520 def create_test_repo(self, key, config=None):
520 def create_test_repo(self, key, config=None):
521 return self._test_repo_container(key, self.alias, config)
521 return self._test_repo_container(key, self.alias, config)
522
522
523 @property
523 @property
524 def repo(self):
524 def repo(self):
525 """
525 """
526 Returns the "current" repository. This is the vcs_test repo or the
526 Returns the "current" repository. This is the vcs_test repo or the
527 last repo which has been created with `create_repo`.
527 last repo which has been created with `create_repo`.
528 """
528 """
529 from rhodecode.model.db import Repository
529 from rhodecode.model.db import Repository
530 return Repository.get_by_repo_name(self.repo_name)
530 return Repository.get_by_repo_name(self.repo_name)
531
531
532 @property
532 @property
533 def default_branch_name(self):
533 def default_branch_name(self):
534 VcsRepository = get_backend(self.alias)
534 VcsRepository = get_backend(self.alias)
535 return VcsRepository.DEFAULT_BRANCH_NAME
535 return VcsRepository.DEFAULT_BRANCH_NAME
536
536
537 @property
537 @property
538 def default_head_id(self):
538 def default_head_id(self):
539 """
539 """
540 Returns the default head id of the underlying backend.
540 Returns the default head id of the underlying backend.
541
541
542 This will be the default branch name in case the backend does have a
542 This will be the default branch name in case the backend does have a
543 default branch. In the other cases it will point to a valid head
543 default branch. In the other cases it will point to a valid head
544 which can serve as the base to create a new commit on top of it.
544 which can serve as the base to create a new commit on top of it.
545 """
545 """
546 vcsrepo = self.repo.scm_instance()
546 vcsrepo = self.repo.scm_instance()
547 head_id = (
547 head_id = (
548 vcsrepo.DEFAULT_BRANCH_NAME or
548 vcsrepo.DEFAULT_BRANCH_NAME or
549 vcsrepo.commit_ids[-1])
549 vcsrepo.commit_ids[-1])
550 return head_id
550 return head_id
551
551
552 @property
552 @property
553 def commit_ids(self):
553 def commit_ids(self):
554 """
554 """
555 Returns the list of commits for the last created repository
555 Returns the list of commits for the last created repository
556 """
556 """
557 return self._commit_ids
557 return self._commit_ids
558
558
559 def create_master_repo(self, commits):
559 def create_master_repo(self, commits):
560 """
560 """
561 Create a repository and remember it as a template.
561 Create a repository and remember it as a template.
562
562
563 This allows to easily create derived repositories to construct
563 This allows to easily create derived repositories to construct
564 more complex scenarios for diff, compare and pull requests.
564 more complex scenarios for diff, compare and pull requests.
565
565
566 Returns a commit map which maps from commit message to raw_id.
566 Returns a commit map which maps from commit message to raw_id.
567 """
567 """
568 self._master_repo = self.create_repo(commits=commits)
568 self._master_repo = self.create_repo(commits=commits)
569 return self._commit_ids
569 return self._commit_ids
570
570
571 def create_repo(
571 def create_repo(
572 self, commits=None, number_of_commits=0, heads=None,
572 self, commits=None, number_of_commits=0, heads=None,
573 name_suffix=u'', **kwargs):
573 name_suffix=u'', **kwargs):
574 """
574 """
575 Create a repository and record it for later cleanup.
575 Create a repository and record it for later cleanup.
576
576
577 :param commits: Optional. A sequence of dict instances.
577 :param commits: Optional. A sequence of dict instances.
578 Will add a commit per entry to the new repository.
578 Will add a commit per entry to the new repository.
579 :param number_of_commits: Optional. If set to a number, this number of
579 :param number_of_commits: Optional. If set to a number, this number of
580 commits will be added to the new repository.
580 commits will be added to the new repository.
581 :param heads: Optional. Can be set to a sequence of of commit
581 :param heads: Optional. Can be set to a sequence of of commit
582 names which shall be pulled in from the master repository.
582 names which shall be pulled in from the master repository.
583
583
584 """
584 """
585 self.repo_name = self._next_repo_name() + name_suffix
585 self.repo_name = self._next_repo_name() + name_suffix
586 repo = self._fixture.create_repo(
586 repo = self._fixture.create_repo(
587 self.repo_name, repo_type=self.alias, **kwargs)
587 self.repo_name, repo_type=self.alias, **kwargs)
588 self._cleanup_repos.append(repo.repo_name)
588 self._cleanup_repos.append(repo.repo_name)
589
589
590 commits = commits or [
590 commits = commits or [
591 {'message': 'Commit %s of %s' % (x, self.repo_name)}
591 {'message': 'Commit %s of %s' % (x, self.repo_name)}
592 for x in xrange(number_of_commits)]
592 for x in xrange(number_of_commits)]
593 self._add_commits_to_repo(repo.scm_instance(), commits)
593 self._add_commits_to_repo(repo.scm_instance(), commits)
594 if heads:
594 if heads:
595 self.pull_heads(repo, heads)
595 self.pull_heads(repo, heads)
596
596
597 return repo
597 return repo
598
598
599 def pull_heads(self, repo, heads):
599 def pull_heads(self, repo, heads):
600 """
600 """
601 Make sure that repo contains all commits mentioned in `heads`
601 Make sure that repo contains all commits mentioned in `heads`
602 """
602 """
603 vcsmaster = self._master_repo.scm_instance()
603 vcsmaster = self._master_repo.scm_instance()
604 vcsrepo = repo.scm_instance()
604 vcsrepo = repo.scm_instance()
605 vcsrepo.config.clear_section('hooks')
605 vcsrepo.config.clear_section('hooks')
606 commit_ids = [self._commit_ids[h] for h in heads]
606 commit_ids = [self._commit_ids[h] for h in heads]
607 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
607 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
608
608
609 def create_fork(self):
609 def create_fork(self):
610 repo_to_fork = self.repo_name
610 repo_to_fork = self.repo_name
611 self.repo_name = self._next_repo_name()
611 self.repo_name = self._next_repo_name()
612 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
612 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
613 self._cleanup_repos.append(self.repo_name)
613 self._cleanup_repos.append(self.repo_name)
614 return repo
614 return repo
615
615
616 def new_repo_name(self, suffix=u''):
616 def new_repo_name(self, suffix=u''):
617 self.repo_name = self._next_repo_name() + suffix
617 self.repo_name = self._next_repo_name() + suffix
618 self._cleanup_repos.append(self.repo_name)
618 self._cleanup_repos.append(self.repo_name)
619 return self.repo_name
619 return self.repo_name
620
620
621 def _next_repo_name(self):
621 def _next_repo_name(self):
622 return u"%s_%s" % (
622 return u"%s_%s" % (
623 self.invalid_repo_name.sub(u'_', self._test_name),
623 self.invalid_repo_name.sub(u'_', self._test_name),
624 len(self._cleanup_repos))
624 len(self._cleanup_repos))
625
625
626 def ensure_file(self, filename, content='Test content\n'):
626 def ensure_file(self, filename, content='Test content\n'):
627 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
627 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
628 commits = [
628 commits = [
629 {'added': [
629 {'added': [
630 FileNode(filename, content=content),
630 FileNode(filename, content=content),
631 ]},
631 ]},
632 ]
632 ]
633 self._add_commits_to_repo(self.repo.scm_instance(), commits)
633 self._add_commits_to_repo(self.repo.scm_instance(), commits)
634
634
635 def enable_downloads(self):
635 def enable_downloads(self):
636 repo = self.repo
636 repo = self.repo
637 repo.enable_downloads = True
637 repo.enable_downloads = True
638 Session().add(repo)
638 Session().add(repo)
639 Session().commit()
639 Session().commit()
640
640
641 def cleanup(self):
641 def cleanup(self):
642 for repo_name in reversed(self._cleanup_repos):
642 for repo_name in reversed(self._cleanup_repos):
643 self._fixture.destroy_repo(repo_name)
643 self._fixture.destroy_repo(repo_name)
644
644
645 def _add_commits_to_repo(self, repo, commits):
645 def _add_commits_to_repo(self, repo, commits):
646 commit_ids = _add_commits_to_repo(repo, commits)
646 commit_ids = _add_commits_to_repo(repo, commits)
647 if not commit_ids:
647 if not commit_ids:
648 return
648 return
649 self._commit_ids = commit_ids
649 self._commit_ids = commit_ids
650
650
651 # Creating refs for Git to allow fetching them from remote repository
651 # Creating refs for Git to allow fetching them from remote repository
652 if self.alias == 'git':
652 if self.alias == 'git':
653 refs = {}
653 refs = {}
654 for message in self._commit_ids:
654 for message in self._commit_ids:
655 # TODO: mikhail: do more special chars replacements
655 # TODO: mikhail: do more special chars replacements
656 ref_name = 'refs/test-refs/{}'.format(
656 ref_name = 'refs/test-refs/{}'.format(
657 message.replace(' ', ''))
657 message.replace(' ', ''))
658 refs[ref_name] = self._commit_ids[message]
658 refs[ref_name] = self._commit_ids[message]
659 self._create_refs(repo, refs)
659 self._create_refs(repo, refs)
660
660
661 def _create_refs(self, repo, refs):
661 def _create_refs(self, repo, refs):
662 for ref_name in refs:
662 for ref_name in refs:
663 repo.set_refs(ref_name, refs[ref_name])
663 repo.set_refs(ref_name, refs[ref_name])
664
664
665
665
666 @pytest.fixture
666 @pytest.fixture
667 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
667 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
668 """
668 """
669 Parametrized fixture which represents a single vcs backend implementation.
669 Parametrized fixture which represents a single vcs backend implementation.
670
670
671 See the fixture `backend` for more details. This one implements the same
671 See the fixture `backend` for more details. This one implements the same
672 concept, but on vcs level. So it does not provide model instances etc.
672 concept, but on vcs level. So it does not provide model instances etc.
673
673
674 Parameters are generated dynamically, see :func:`pytest_generate_tests`
674 Parameters are generated dynamically, see :func:`pytest_generate_tests`
675 for how this works.
675 for how this works.
676 """
676 """
677 if backend_alias not in request.config.getoption('--backends'):
677 if backend_alias not in request.config.getoption('--backends'):
678 pytest.skip("Backend %s not selected." % (backend_alias, ))
678 pytest.skip("Backend %s not selected." % (backend_alias, ))
679
679
680 utils.check_xfail_backends(request.node, backend_alias)
680 utils.check_xfail_backends(request.node, backend_alias)
681 utils.check_skip_backends(request.node, backend_alias)
681 utils.check_skip_backends(request.node, backend_alias)
682
682
683 repo_name = 'vcs_test_%s' % (backend_alias, )
683 repo_name = 'vcs_test_%s' % (backend_alias, )
684 repo_path = os.path.join(tests_tmp_path, repo_name)
684 repo_path = os.path.join(tests_tmp_path, repo_name)
685 backend = VcsBackend(
685 backend = VcsBackend(
686 alias=backend_alias,
686 alias=backend_alias,
687 repo_path=repo_path,
687 repo_path=repo_path,
688 test_name=request.node.name,
688 test_name=request.node.name,
689 test_repo_container=test_repo)
689 test_repo_container=test_repo)
690 request.addfinalizer(backend.cleanup)
690 request.addfinalizer(backend.cleanup)
691 return backend
691 return backend
692
692
693
693
694 @pytest.fixture
694 @pytest.fixture
695 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
695 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
696 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
696 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
697
697
698
698
699 @pytest.fixture
699 @pytest.fixture
700 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
700 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
701 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
701 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
702
702
703
703
704 @pytest.fixture
704 @pytest.fixture
705 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
705 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
706 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
706 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
707
707
708
708
709 @pytest.fixture
709 @pytest.fixture
710 def vcsbackend_random(vcsbackend_git):
710 def vcsbackend_random(vcsbackend_git):
711 """
711 """
712 Use this to express that your tests need "a vcsbackend".
712 Use this to express that your tests need "a vcsbackend".
713
713
714 The fixture `vcsbackend` would run the test multiple times for each
714 The fixture `vcsbackend` would run the test multiple times for each
715 available vcs backend which is a pure waste of time if the test is
715 available vcs backend which is a pure waste of time if the test is
716 independent of the vcs backend type.
716 independent of the vcs backend type.
717 """
717 """
718 # TODO: johbo: Change this to pick a random backend
718 # TODO: johbo: Change this to pick a random backend
719 return vcsbackend_git
719 return vcsbackend_git
720
720
721
721
722 @pytest.fixture
722 @pytest.fixture
723 def vcsbackend_stub(vcsbackend_git):
723 def vcsbackend_stub(vcsbackend_git):
724 """
724 """
725 Use this to express that your test just needs a stub of a vcsbackend.
725 Use this to express that your test just needs a stub of a vcsbackend.
726
726
727 Plan is to eventually implement an in-memory stub to speed tests up.
727 Plan is to eventually implement an in-memory stub to speed tests up.
728 """
728 """
729 return vcsbackend_git
729 return vcsbackend_git
730
730
731
731
732 class VcsBackend(object):
732 class VcsBackend(object):
733 """
733 """
734 Represents the test configuration for one supported vcs backend.
734 Represents the test configuration for one supported vcs backend.
735 """
735 """
736
736
737 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
737 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
738
738
739 def __init__(self, alias, repo_path, test_name, test_repo_container):
739 def __init__(self, alias, repo_path, test_name, test_repo_container):
740 self.alias = alias
740 self.alias = alias
741 self._repo_path = repo_path
741 self._repo_path = repo_path
742 self._cleanup_repos = []
742 self._cleanup_repos = []
743 self._test_name = test_name
743 self._test_name = test_name
744 self._test_repo_container = test_repo_container
744 self._test_repo_container = test_repo_container
745
745
746 def __getitem__(self, key):
746 def __getitem__(self, key):
747 return self._test_repo_container(key, self.alias).scm_instance()
747 return self._test_repo_container(key, self.alias).scm_instance()
748
748
749 @property
749 @property
750 def repo(self):
750 def repo(self):
751 """
751 """
752 Returns the "current" repository. This is the vcs_test repo of the last
752 Returns the "current" repository. This is the vcs_test repo of the last
753 repo which has been created.
753 repo which has been created.
754 """
754 """
755 Repository = get_backend(self.alias)
755 Repository = get_backend(self.alias)
756 return Repository(self._repo_path)
756 return Repository(self._repo_path)
757
757
758 @property
758 @property
759 def backend(self):
759 def backend(self):
760 """
760 """
761 Returns the backend implementation class.
761 Returns the backend implementation class.
762 """
762 """
763 return get_backend(self.alias)
763 return get_backend(self.alias)
764
764
765 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
765 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
766 repo_name = self._next_repo_name()
766 repo_name = self._next_repo_name()
767 self._repo_path = get_new_dir(repo_name)
767 self._repo_path = get_new_dir(repo_name)
768 repo_class = get_backend(self.alias)
768 repo_class = get_backend(self.alias)
769 src_url = None
769 src_url = None
770 if _clone_repo:
770 if _clone_repo:
771 src_url = _clone_repo.path
771 src_url = _clone_repo.path
772 repo = repo_class(self._repo_path, create=True, src_url=src_url)
772 repo = repo_class(self._repo_path, create=True, src_url=src_url)
773 self._cleanup_repos.append(repo)
773 self._cleanup_repos.append(repo)
774
774
775 commits = commits or [
775 commits = commits or [
776 {'message': 'Commit %s of %s' % (x, repo_name)}
776 {'message': 'Commit %s of %s' % (x, repo_name)}
777 for x in xrange(number_of_commits)]
777 for x in xrange(number_of_commits)]
778 _add_commits_to_repo(repo, commits)
778 _add_commits_to_repo(repo, commits)
779 return repo
779 return repo
780
780
781 def clone_repo(self, repo):
781 def clone_repo(self, repo):
782 return self.create_repo(_clone_repo=repo)
782 return self.create_repo(_clone_repo=repo)
783
783
784 def cleanup(self):
784 def cleanup(self):
785 for repo in self._cleanup_repos:
785 for repo in self._cleanup_repos:
786 shutil.rmtree(repo.path)
786 shutil.rmtree(repo.path)
787
787
788 def new_repo_path(self):
788 def new_repo_path(self):
789 repo_name = self._next_repo_name()
789 repo_name = self._next_repo_name()
790 self._repo_path = get_new_dir(repo_name)
790 self._repo_path = get_new_dir(repo_name)
791 return self._repo_path
791 return self._repo_path
792
792
793 def _next_repo_name(self):
793 def _next_repo_name(self):
794 return "%s_%s" % (
794 return "%s_%s" % (
795 self.invalid_repo_name.sub('_', self._test_name),
795 self.invalid_repo_name.sub('_', self._test_name),
796 len(self._cleanup_repos))
796 len(self._cleanup_repos))
797
797
798 def add_file(self, repo, filename, content='Test content\n'):
798 def add_file(self, repo, filename, content='Test content\n'):
799 imc = repo.in_memory_commit
799 imc = repo.in_memory_commit
800 imc.add(FileNode(filename, content=content))
800 imc.add(FileNode(filename, content=content))
801 imc.commit(
801 imc.commit(
802 message=u'Automatic commit from vcsbackend fixture',
802 message=u'Automatic commit from vcsbackend fixture',
803 author=u'Automatic')
803 author=u'Automatic')
804
804
805 def ensure_file(self, filename, content='Test content\n'):
805 def ensure_file(self, filename, content='Test content\n'):
806 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
806 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
807 self.add_file(self.repo, filename, content)
807 self.add_file(self.repo, filename, content)
808
808
809
809
810 def _add_commits_to_repo(vcs_repo, commits):
810 def _add_commits_to_repo(vcs_repo, commits):
811 commit_ids = {}
811 commit_ids = {}
812 if not commits:
812 if not commits:
813 return commit_ids
813 return commit_ids
814
814
815 imc = vcs_repo.in_memory_commit
815 imc = vcs_repo.in_memory_commit
816 commit = None
816 commit = None
817
817
818 for idx, commit in enumerate(commits):
818 for idx, commit in enumerate(commits):
819 message = unicode(commit.get('message', 'Commit %s' % idx))
819 message = unicode(commit.get('message', 'Commit %s' % idx))
820
820
821 for node in commit.get('added', []):
821 for node in commit.get('added', []):
822 imc.add(FileNode(node.path, content=node.content))
822 imc.add(FileNode(node.path, content=node.content))
823 for node in commit.get('changed', []):
823 for node in commit.get('changed', []):
824 imc.change(FileNode(node.path, content=node.content))
824 imc.change(FileNode(node.path, content=node.content))
825 for node in commit.get('removed', []):
825 for node in commit.get('removed', []):
826 imc.remove(FileNode(node.path))
826 imc.remove(FileNode(node.path))
827
827
828 parents = [
828 parents = [
829 vcs_repo.get_commit(commit_id=commit_ids[p])
829 vcs_repo.get_commit(commit_id=commit_ids[p])
830 for p in commit.get('parents', [])]
830 for p in commit.get('parents', [])]
831
831
832 operations = ('added', 'changed', 'removed')
832 operations = ('added', 'changed', 'removed')
833 if not any((commit.get(o) for o in operations)):
833 if not any((commit.get(o) for o in operations)):
834 imc.add(FileNode('file_%s' % idx, content=message))
834 imc.add(FileNode('file_%s' % idx, content=message))
835
835
836 commit = imc.commit(
836 commit = imc.commit(
837 message=message,
837 message=message,
838 author=unicode(commit.get('author', 'Automatic')),
838 author=unicode(commit.get('author', 'Automatic')),
839 date=commit.get('date'),
839 date=commit.get('date'),
840 branch=commit.get('branch'),
840 branch=commit.get('branch'),
841 parents=parents)
841 parents=parents)
842
842
843 commit_ids[commit.message] = commit.raw_id
843 commit_ids[commit.message] = commit.raw_id
844
844
845 return commit_ids
845 return commit_ids
846
846
847
847
848 @pytest.fixture
848 @pytest.fixture
849 def reposerver(request):
849 def reposerver(request):
850 """
850 """
851 Allows to serve a backend repository
851 Allows to serve a backend repository
852 """
852 """
853
853
854 repo_server = RepoServer()
854 repo_server = RepoServer()
855 request.addfinalizer(repo_server.cleanup)
855 request.addfinalizer(repo_server.cleanup)
856 return repo_server
856 return repo_server
857
857
858
858
859 class RepoServer(object):
859 class RepoServer(object):
860 """
860 """
861 Utility to serve a local repository for the duration of a test case.
861 Utility to serve a local repository for the duration of a test case.
862
862
863 Supports only Subversion so far.
863 Supports only Subversion so far.
864 """
864 """
865
865
866 url = None
866 url = None
867
867
868 def __init__(self):
868 def __init__(self):
869 self._cleanup_servers = []
869 self._cleanup_servers = []
870
870
871 def serve(self, vcsrepo):
871 def serve(self, vcsrepo):
872 if vcsrepo.alias != 'svn':
872 if vcsrepo.alias != 'svn':
873 raise TypeError("Backend %s not supported" % vcsrepo.alias)
873 raise TypeError("Backend %s not supported" % vcsrepo.alias)
874
874
875 proc = subprocess32.Popen(
875 proc = subprocess32.Popen(
876 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
876 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
877 '--root', vcsrepo.path])
877 '--root', vcsrepo.path])
878 self._cleanup_servers.append(proc)
878 self._cleanup_servers.append(proc)
879 self.url = 'svn://localhost'
879 self.url = 'svn://localhost'
880
880
881 def cleanup(self):
881 def cleanup(self):
882 for proc in self._cleanup_servers:
882 for proc in self._cleanup_servers:
883 proc.terminate()
883 proc.terminate()
884
884
885
885
886 @pytest.fixture
886 @pytest.fixture
887 def pr_util(backend, request):
887 def pr_util(backend, request):
888 """
888 """
889 Utility for tests of models and for functional tests around pull requests.
889 Utility for tests of models and for functional tests around pull requests.
890
890
891 It gives an instance of :class:`PRTestUtility` which provides various
891 It gives an instance of :class:`PRTestUtility` which provides various
892 utility methods around one pull request.
892 utility methods around one pull request.
893
893
894 This fixture uses `backend` and inherits its parameterization.
894 This fixture uses `backend` and inherits its parameterization.
895 """
895 """
896
896
897 util = PRTestUtility(backend)
897 util = PRTestUtility(backend)
898
898
899 @request.addfinalizer
899 @request.addfinalizer
900 def cleanup():
900 def cleanup():
901 util.cleanup()
901 util.cleanup()
902
902
903 return util
903 return util
904
904
905
905
906 class PRTestUtility(object):
906 class PRTestUtility(object):
907
907
908 pull_request = None
908 pull_request = None
909 pull_request_id = None
909 pull_request_id = None
910 mergeable_patcher = None
910 mergeable_patcher = None
911 mergeable_mock = None
911 mergeable_mock = None
912 notification_patcher = None
912 notification_patcher = None
913
913
914 def __init__(self, backend):
914 def __init__(self, backend):
915 self.backend = backend
915 self.backend = backend
916
916
917 def create_pull_request(
917 def create_pull_request(
918 self, commits=None, target_head=None, source_head=None,
918 self, commits=None, target_head=None, source_head=None,
919 revisions=None, approved=False, author=None, mergeable=False,
919 revisions=None, approved=False, author=None, mergeable=False,
920 enable_notifications=True, name_suffix=u'', reviewers=None,
920 enable_notifications=True, name_suffix=u'', reviewers=None,
921 title=u"Test", description=u"Description"):
921 title=u"Test", description=u"Description"):
922 self.set_mergeable(mergeable)
922 self.set_mergeable(mergeable)
923 if not enable_notifications:
923 if not enable_notifications:
924 # mock notification side effect
924 # mock notification side effect
925 self.notification_patcher = mock.patch(
925 self.notification_patcher = mock.patch(
926 'rhodecode.model.notification.NotificationModel.create')
926 'rhodecode.model.notification.NotificationModel.create')
927 self.notification_patcher.start()
927 self.notification_patcher.start()
928
928
929 if not self.pull_request:
929 if not self.pull_request:
930 if not commits:
930 if not commits:
931 commits = [
931 commits = [
932 {'message': 'c1'},
932 {'message': 'c1'},
933 {'message': 'c2'},
933 {'message': 'c2'},
934 {'message': 'c3'},
934 {'message': 'c3'},
935 ]
935 ]
936 target_head = 'c1'
936 target_head = 'c1'
937 source_head = 'c2'
937 source_head = 'c2'
938 revisions = ['c2']
938 revisions = ['c2']
939
939
940 self.commit_ids = self.backend.create_master_repo(commits)
940 self.commit_ids = self.backend.create_master_repo(commits)
941 self.target_repository = self.backend.create_repo(
941 self.target_repository = self.backend.create_repo(
942 heads=[target_head], name_suffix=name_suffix)
942 heads=[target_head], name_suffix=name_suffix)
943 self.source_repository = self.backend.create_repo(
943 self.source_repository = self.backend.create_repo(
944 heads=[source_head], name_suffix=name_suffix)
944 heads=[source_head], name_suffix=name_suffix)
945 self.author = author or UserModel().get_by_username(
945 self.author = author or UserModel().get_by_username(
946 TEST_USER_ADMIN_LOGIN)
946 TEST_USER_ADMIN_LOGIN)
947
947
948 model = PullRequestModel()
948 model = PullRequestModel()
949 self.create_parameters = {
949 self.create_parameters = {
950 'created_by': self.author,
950 'created_by': self.author,
951 'source_repo': self.source_repository.repo_name,
951 'source_repo': self.source_repository.repo_name,
952 'source_ref': self._default_branch_reference(source_head),
952 'source_ref': self._default_branch_reference(source_head),
953 'target_repo': self.target_repository.repo_name,
953 'target_repo': self.target_repository.repo_name,
954 'target_ref': self._default_branch_reference(target_head),
954 'target_ref': self._default_branch_reference(target_head),
955 'revisions': [self.commit_ids[r] for r in revisions],
955 'revisions': [self.commit_ids[r] for r in revisions],
956 'reviewers': reviewers or self._get_reviewers(),
956 'reviewers': reviewers or self._get_reviewers(),
957 'title': title,
957 'title': title,
958 'description': description,
958 'description': description,
959 }
959 }
960 self.pull_request = model.create(**self.create_parameters)
960 self.pull_request = model.create(**self.create_parameters)
961 assert model.get_versions(self.pull_request) == []
961 assert model.get_versions(self.pull_request) == []
962
962
963 self.pull_request_id = self.pull_request.pull_request_id
963 self.pull_request_id = self.pull_request.pull_request_id
964
964
965 if approved:
965 if approved:
966 self.approve()
966 self.approve()
967
967
968 Session().add(self.pull_request)
968 Session().add(self.pull_request)
969 Session().commit()
969 Session().commit()
970
970
971 return self.pull_request
971 return self.pull_request
972
972
973 def approve(self):
973 def approve(self):
974 self.create_status_votes(
974 self.create_status_votes(
975 ChangesetStatus.STATUS_APPROVED,
975 ChangesetStatus.STATUS_APPROVED,
976 *self.pull_request.reviewers)
976 *self.pull_request.reviewers)
977
977
978 def close(self):
978 def close(self):
979 PullRequestModel().close_pull_request(self.pull_request, self.author)
979 PullRequestModel().close_pull_request(self.pull_request, self.author)
980
980
981 def _default_branch_reference(self, commit_message):
981 def _default_branch_reference(self, commit_message):
982 reference = '%s:%s:%s' % (
982 reference = '%s:%s:%s' % (
983 'branch',
983 'branch',
984 self.backend.default_branch_name,
984 self.backend.default_branch_name,
985 self.commit_ids[commit_message])
985 self.commit_ids[commit_message])
986 return reference
986 return reference
987
987
988 def _get_reviewers(self):
988 def _get_reviewers(self):
989 model = UserModel()
990 return [
989 return [
991 model.get_by_username(TEST_USER_REGULAR_LOGIN),
990 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
992 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
991 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
993 ]
992 ]
994
993
995 def update_source_repository(self, head=None):
994 def update_source_repository(self, head=None):
996 heads = [head or 'c3']
995 heads = [head or 'c3']
997 self.backend.pull_heads(self.source_repository, heads=heads)
996 self.backend.pull_heads(self.source_repository, heads=heads)
998
997
999 def add_one_commit(self, head=None):
998 def add_one_commit(self, head=None):
1000 self.update_source_repository(head=head)
999 self.update_source_repository(head=head)
1001 old_commit_ids = set(self.pull_request.revisions)
1000 old_commit_ids = set(self.pull_request.revisions)
1002 PullRequestModel().update_commits(self.pull_request)
1001 PullRequestModel().update_commits(self.pull_request)
1003 commit_ids = set(self.pull_request.revisions)
1002 commit_ids = set(self.pull_request.revisions)
1004 new_commit_ids = commit_ids - old_commit_ids
1003 new_commit_ids = commit_ids - old_commit_ids
1005 assert len(new_commit_ids) == 1
1004 assert len(new_commit_ids) == 1
1006 return new_commit_ids.pop()
1005 return new_commit_ids.pop()
1007
1006
1008 def remove_one_commit(self):
1007 def remove_one_commit(self):
1009 assert len(self.pull_request.revisions) == 2
1008 assert len(self.pull_request.revisions) == 2
1010 source_vcs = self.source_repository.scm_instance()
1009 source_vcs = self.source_repository.scm_instance()
1011 removed_commit_id = source_vcs.commit_ids[-1]
1010 removed_commit_id = source_vcs.commit_ids[-1]
1012
1011
1013 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1012 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1014 # remove the if once that's sorted out.
1013 # remove the if once that's sorted out.
1015 if self.backend.alias == "git":
1014 if self.backend.alias == "git":
1016 kwargs = {'branch_name': self.backend.default_branch_name}
1015 kwargs = {'branch_name': self.backend.default_branch_name}
1017 else:
1016 else:
1018 kwargs = {}
1017 kwargs = {}
1019 source_vcs.strip(removed_commit_id, **kwargs)
1018 source_vcs.strip(removed_commit_id, **kwargs)
1020
1019
1021 PullRequestModel().update_commits(self.pull_request)
1020 PullRequestModel().update_commits(self.pull_request)
1022 assert len(self.pull_request.revisions) == 1
1021 assert len(self.pull_request.revisions) == 1
1023 return removed_commit_id
1022 return removed_commit_id
1024
1023
1025 def create_comment(self, linked_to=None):
1024 def create_comment(self, linked_to=None):
1026 comment = CommentsModel().create(
1025 comment = CommentsModel().create(
1027 text=u"Test comment",
1026 text=u"Test comment",
1028 repo=self.target_repository.repo_name,
1027 repo=self.target_repository.repo_name,
1029 user=self.author,
1028 user=self.author,
1030 pull_request=self.pull_request)
1029 pull_request=self.pull_request)
1031 assert comment.pull_request_version_id is None
1030 assert comment.pull_request_version_id is None
1032
1031
1033 if linked_to:
1032 if linked_to:
1034 PullRequestModel()._link_comments_to_version(linked_to)
1033 PullRequestModel()._link_comments_to_version(linked_to)
1035
1034
1036 return comment
1035 return comment
1037
1036
1038 def create_inline_comment(
1037 def create_inline_comment(
1039 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1038 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1040 comment = CommentsModel().create(
1039 comment = CommentsModel().create(
1041 text=u"Test comment",
1040 text=u"Test comment",
1042 repo=self.target_repository.repo_name,
1041 repo=self.target_repository.repo_name,
1043 user=self.author,
1042 user=self.author,
1044 line_no=line_no,
1043 line_no=line_no,
1045 f_path=file_path,
1044 f_path=file_path,
1046 pull_request=self.pull_request)
1045 pull_request=self.pull_request)
1047 assert comment.pull_request_version_id is None
1046 assert comment.pull_request_version_id is None
1048
1047
1049 if linked_to:
1048 if linked_to:
1050 PullRequestModel()._link_comments_to_version(linked_to)
1049 PullRequestModel()._link_comments_to_version(linked_to)
1051
1050
1052 return comment
1051 return comment
1053
1052
1054 def create_version_of_pull_request(self):
1053 def create_version_of_pull_request(self):
1055 pull_request = self.create_pull_request()
1054 pull_request = self.create_pull_request()
1056 version = PullRequestModel()._create_version_from_snapshot(
1055 version = PullRequestModel()._create_version_from_snapshot(
1057 pull_request)
1056 pull_request)
1058 return version
1057 return version
1059
1058
1060 def create_status_votes(self, status, *reviewers):
1059 def create_status_votes(self, status, *reviewers):
1061 for reviewer in reviewers:
1060 for reviewer in reviewers:
1062 ChangesetStatusModel().set_status(
1061 ChangesetStatusModel().set_status(
1063 repo=self.pull_request.target_repo,
1062 repo=self.pull_request.target_repo,
1064 status=status,
1063 status=status,
1065 user=reviewer.user_id,
1064 user=reviewer.user_id,
1066 pull_request=self.pull_request)
1065 pull_request=self.pull_request)
1067
1066
1068 def set_mergeable(self, value):
1067 def set_mergeable(self, value):
1069 if not self.mergeable_patcher:
1068 if not self.mergeable_patcher:
1070 self.mergeable_patcher = mock.patch.object(
1069 self.mergeable_patcher = mock.patch.object(
1071 VcsSettingsModel, 'get_general_settings')
1070 VcsSettingsModel, 'get_general_settings')
1072 self.mergeable_mock = self.mergeable_patcher.start()
1071 self.mergeable_mock = self.mergeable_patcher.start()
1073 self.mergeable_mock.return_value = {
1072 self.mergeable_mock.return_value = {
1074 'rhodecode_pr_merge_enabled': value}
1073 'rhodecode_pr_merge_enabled': value}
1075
1074
1076 def cleanup(self):
1075 def cleanup(self):
1077 # In case the source repository is already cleaned up, the pull
1076 # In case the source repository is already cleaned up, the pull
1078 # request will already be deleted.
1077 # request will already be deleted.
1079 pull_request = PullRequest().get(self.pull_request_id)
1078 pull_request = PullRequest().get(self.pull_request_id)
1080 if pull_request:
1079 if pull_request:
1081 PullRequestModel().delete(pull_request)
1080 PullRequestModel().delete(pull_request)
1082 Session().commit()
1081 Session().commit()
1083
1082
1084 if self.notification_patcher:
1083 if self.notification_patcher:
1085 self.notification_patcher.stop()
1084 self.notification_patcher.stop()
1086
1085
1087 if self.mergeable_patcher:
1086 if self.mergeable_patcher:
1088 self.mergeable_patcher.stop()
1087 self.mergeable_patcher.stop()
1089
1088
1090
1089
1091 @pytest.fixture
1090 @pytest.fixture
1092 def user_admin(pylonsapp):
1091 def user_admin(pylonsapp):
1093 """
1092 """
1094 Provides the default admin test user as an instance of `db.User`.
1093 Provides the default admin test user as an instance of `db.User`.
1095 """
1094 """
1096 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1095 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1097 return user
1096 return user
1098
1097
1099
1098
1100 @pytest.fixture
1099 @pytest.fixture
1101 def user_regular(pylonsapp):
1100 def user_regular(pylonsapp):
1102 """
1101 """
1103 Provides the default regular test user as an instance of `db.User`.
1102 Provides the default regular test user as an instance of `db.User`.
1104 """
1103 """
1105 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1104 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1106 return user
1105 return user
1107
1106
1108
1107
1109 @pytest.fixture
1108 @pytest.fixture
1110 def user_util(request, pylonsapp):
1109 def user_util(request, pylonsapp):
1111 """
1110 """
1112 Provides a wired instance of `UserUtility` with integrated cleanup.
1111 Provides a wired instance of `UserUtility` with integrated cleanup.
1113 """
1112 """
1114 utility = UserUtility(test_name=request.node.name)
1113 utility = UserUtility(test_name=request.node.name)
1115 request.addfinalizer(utility.cleanup)
1114 request.addfinalizer(utility.cleanup)
1116 return utility
1115 return utility
1117
1116
1118
1117
1119 # TODO: johbo: Split this up into utilities per domain or something similar
1118 # TODO: johbo: Split this up into utilities per domain or something similar
1120 class UserUtility(object):
1119 class UserUtility(object):
1121
1120
1122 def __init__(self, test_name="test"):
1121 def __init__(self, test_name="test"):
1123 self._test_name = self._sanitize_name(test_name)
1122 self._test_name = self._sanitize_name(test_name)
1124 self.fixture = Fixture()
1123 self.fixture = Fixture()
1125 self.repo_group_ids = []
1124 self.repo_group_ids = []
1126 self.repos_ids = []
1125 self.repos_ids = []
1127 self.user_ids = []
1126 self.user_ids = []
1128 self.user_group_ids = []
1127 self.user_group_ids = []
1129 self.user_repo_permission_ids = []
1128 self.user_repo_permission_ids = []
1130 self.user_group_repo_permission_ids = []
1129 self.user_group_repo_permission_ids = []
1131 self.user_repo_group_permission_ids = []
1130 self.user_repo_group_permission_ids = []
1132 self.user_group_repo_group_permission_ids = []
1131 self.user_group_repo_group_permission_ids = []
1133 self.user_user_group_permission_ids = []
1132 self.user_user_group_permission_ids = []
1134 self.user_group_user_group_permission_ids = []
1133 self.user_group_user_group_permission_ids = []
1135 self.user_permissions = []
1134 self.user_permissions = []
1136
1135
1137 def _sanitize_name(self, name):
1136 def _sanitize_name(self, name):
1138 for char in ['[', ']']:
1137 for char in ['[', ']']:
1139 name = name.replace(char, '_')
1138 name = name.replace(char, '_')
1140 return name
1139 return name
1141
1140
1142 def create_repo_group(
1141 def create_repo_group(
1143 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1142 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1144 group_name = "{prefix}_repogroup_{count}".format(
1143 group_name = "{prefix}_repogroup_{count}".format(
1145 prefix=self._test_name,
1144 prefix=self._test_name,
1146 count=len(self.repo_group_ids))
1145 count=len(self.repo_group_ids))
1147 repo_group = self.fixture.create_repo_group(
1146 repo_group = self.fixture.create_repo_group(
1148 group_name, cur_user=owner)
1147 group_name, cur_user=owner)
1149 if auto_cleanup:
1148 if auto_cleanup:
1150 self.repo_group_ids.append(repo_group.group_id)
1149 self.repo_group_ids.append(repo_group.group_id)
1151 return repo_group
1150 return repo_group
1152
1151
1153 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1152 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None,
1154 auto_cleanup=True, repo_type='hg'):
1153 auto_cleanup=True, repo_type='hg'):
1155 repo_name = "{prefix}_repository_{count}".format(
1154 repo_name = "{prefix}_repository_{count}".format(
1156 prefix=self._test_name,
1155 prefix=self._test_name,
1157 count=len(self.repos_ids))
1156 count=len(self.repos_ids))
1158
1157
1159 repository = self.fixture.create_repo(
1158 repository = self.fixture.create_repo(
1160 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1159 repo_name, cur_user=owner, repo_group=parent, repo_type=repo_type)
1161 if auto_cleanup:
1160 if auto_cleanup:
1162 self.repos_ids.append(repository.repo_id)
1161 self.repos_ids.append(repository.repo_id)
1163 return repository
1162 return repository
1164
1163
1165 def create_user(self, auto_cleanup=True, **kwargs):
1164 def create_user(self, auto_cleanup=True, **kwargs):
1166 user_name = "{prefix}_user_{count}".format(
1165 user_name = "{prefix}_user_{count}".format(
1167 prefix=self._test_name,
1166 prefix=self._test_name,
1168 count=len(self.user_ids))
1167 count=len(self.user_ids))
1169 user = self.fixture.create_user(user_name, **kwargs)
1168 user = self.fixture.create_user(user_name, **kwargs)
1170 if auto_cleanup:
1169 if auto_cleanup:
1171 self.user_ids.append(user.user_id)
1170 self.user_ids.append(user.user_id)
1172 return user
1171 return user
1173
1172
1174 def create_user_with_group(self):
1173 def create_user_with_group(self):
1175 user = self.create_user()
1174 user = self.create_user()
1176 user_group = self.create_user_group(members=[user])
1175 user_group = self.create_user_group(members=[user])
1177 return user, user_group
1176 return user, user_group
1178
1177
1179 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1178 def create_user_group(self, owner=TEST_USER_ADMIN_LOGIN, members=None,
1180 auto_cleanup=True, **kwargs):
1179 auto_cleanup=True, **kwargs):
1181 group_name = "{prefix}_usergroup_{count}".format(
1180 group_name = "{prefix}_usergroup_{count}".format(
1182 prefix=self._test_name,
1181 prefix=self._test_name,
1183 count=len(self.user_group_ids))
1182 count=len(self.user_group_ids))
1184 user_group = self.fixture.create_user_group(
1183 user_group = self.fixture.create_user_group(
1185 group_name, cur_user=owner, **kwargs)
1184 group_name, cur_user=owner, **kwargs)
1186
1185
1187 if auto_cleanup:
1186 if auto_cleanup:
1188 self.user_group_ids.append(user_group.users_group_id)
1187 self.user_group_ids.append(user_group.users_group_id)
1189 if members:
1188 if members:
1190 for user in members:
1189 for user in members:
1191 UserGroupModel().add_user_to_group(user_group, user)
1190 UserGroupModel().add_user_to_group(user_group, user)
1192 return user_group
1191 return user_group
1193
1192
1194 def grant_user_permission(self, user_name, permission_name):
1193 def grant_user_permission(self, user_name, permission_name):
1195 self._inherit_default_user_permissions(user_name, False)
1194 self._inherit_default_user_permissions(user_name, False)
1196 self.user_permissions.append((user_name, permission_name))
1195 self.user_permissions.append((user_name, permission_name))
1197
1196
1198 def grant_user_permission_to_repo_group(
1197 def grant_user_permission_to_repo_group(
1199 self, repo_group, user, permission_name):
1198 self, repo_group, user, permission_name):
1200 permission = RepoGroupModel().grant_user_permission(
1199 permission = RepoGroupModel().grant_user_permission(
1201 repo_group, user, permission_name)
1200 repo_group, user, permission_name)
1202 self.user_repo_group_permission_ids.append(
1201 self.user_repo_group_permission_ids.append(
1203 (repo_group.group_id, user.user_id))
1202 (repo_group.group_id, user.user_id))
1204 return permission
1203 return permission
1205
1204
1206 def grant_user_group_permission_to_repo_group(
1205 def grant_user_group_permission_to_repo_group(
1207 self, repo_group, user_group, permission_name):
1206 self, repo_group, user_group, permission_name):
1208 permission = RepoGroupModel().grant_user_group_permission(
1207 permission = RepoGroupModel().grant_user_group_permission(
1209 repo_group, user_group, permission_name)
1208 repo_group, user_group, permission_name)
1210 self.user_group_repo_group_permission_ids.append(
1209 self.user_group_repo_group_permission_ids.append(
1211 (repo_group.group_id, user_group.users_group_id))
1210 (repo_group.group_id, user_group.users_group_id))
1212 return permission
1211 return permission
1213
1212
1214 def grant_user_permission_to_repo(
1213 def grant_user_permission_to_repo(
1215 self, repo, user, permission_name):
1214 self, repo, user, permission_name):
1216 permission = RepoModel().grant_user_permission(
1215 permission = RepoModel().grant_user_permission(
1217 repo, user, permission_name)
1216 repo, user, permission_name)
1218 self.user_repo_permission_ids.append(
1217 self.user_repo_permission_ids.append(
1219 (repo.repo_id, user.user_id))
1218 (repo.repo_id, user.user_id))
1220 return permission
1219 return permission
1221
1220
1222 def grant_user_group_permission_to_repo(
1221 def grant_user_group_permission_to_repo(
1223 self, repo, user_group, permission_name):
1222 self, repo, user_group, permission_name):
1224 permission = RepoModel().grant_user_group_permission(
1223 permission = RepoModel().grant_user_group_permission(
1225 repo, user_group, permission_name)
1224 repo, user_group, permission_name)
1226 self.user_group_repo_permission_ids.append(
1225 self.user_group_repo_permission_ids.append(
1227 (repo.repo_id, user_group.users_group_id))
1226 (repo.repo_id, user_group.users_group_id))
1228 return permission
1227 return permission
1229
1228
1230 def grant_user_permission_to_user_group(
1229 def grant_user_permission_to_user_group(
1231 self, target_user_group, user, permission_name):
1230 self, target_user_group, user, permission_name):
1232 permission = UserGroupModel().grant_user_permission(
1231 permission = UserGroupModel().grant_user_permission(
1233 target_user_group, user, permission_name)
1232 target_user_group, user, permission_name)
1234 self.user_user_group_permission_ids.append(
1233 self.user_user_group_permission_ids.append(
1235 (target_user_group.users_group_id, user.user_id))
1234 (target_user_group.users_group_id, user.user_id))
1236 return permission
1235 return permission
1237
1236
1238 def grant_user_group_permission_to_user_group(
1237 def grant_user_group_permission_to_user_group(
1239 self, target_user_group, user_group, permission_name):
1238 self, target_user_group, user_group, permission_name):
1240 permission = UserGroupModel().grant_user_group_permission(
1239 permission = UserGroupModel().grant_user_group_permission(
1241 target_user_group, user_group, permission_name)
1240 target_user_group, user_group, permission_name)
1242 self.user_group_user_group_permission_ids.append(
1241 self.user_group_user_group_permission_ids.append(
1243 (target_user_group.users_group_id, user_group.users_group_id))
1242 (target_user_group.users_group_id, user_group.users_group_id))
1244 return permission
1243 return permission
1245
1244
1246 def revoke_user_permission(self, user_name, permission_name):
1245 def revoke_user_permission(self, user_name, permission_name):
1247 self._inherit_default_user_permissions(user_name, True)
1246 self._inherit_default_user_permissions(user_name, True)
1248 UserModel().revoke_perm(user_name, permission_name)
1247 UserModel().revoke_perm(user_name, permission_name)
1249
1248
1250 def _inherit_default_user_permissions(self, user_name, value):
1249 def _inherit_default_user_permissions(self, user_name, value):
1251 user = UserModel().get_by_username(user_name)
1250 user = UserModel().get_by_username(user_name)
1252 user.inherit_default_permissions = value
1251 user.inherit_default_permissions = value
1253 Session().add(user)
1252 Session().add(user)
1254 Session().commit()
1253 Session().commit()
1255
1254
1256 def cleanup(self):
1255 def cleanup(self):
1257 self._cleanup_permissions()
1256 self._cleanup_permissions()
1258 self._cleanup_repos()
1257 self._cleanup_repos()
1259 self._cleanup_repo_groups()
1258 self._cleanup_repo_groups()
1260 self._cleanup_user_groups()
1259 self._cleanup_user_groups()
1261 self._cleanup_users()
1260 self._cleanup_users()
1262
1261
1263 def _cleanup_permissions(self):
1262 def _cleanup_permissions(self):
1264 if self.user_permissions:
1263 if self.user_permissions:
1265 for user_name, permission_name in self.user_permissions:
1264 for user_name, permission_name in self.user_permissions:
1266 self.revoke_user_permission(user_name, permission_name)
1265 self.revoke_user_permission(user_name, permission_name)
1267
1266
1268 for permission in self.user_repo_permission_ids:
1267 for permission in self.user_repo_permission_ids:
1269 RepoModel().revoke_user_permission(*permission)
1268 RepoModel().revoke_user_permission(*permission)
1270
1269
1271 for permission in self.user_group_repo_permission_ids:
1270 for permission in self.user_group_repo_permission_ids:
1272 RepoModel().revoke_user_group_permission(*permission)
1271 RepoModel().revoke_user_group_permission(*permission)
1273
1272
1274 for permission in self.user_repo_group_permission_ids:
1273 for permission in self.user_repo_group_permission_ids:
1275 RepoGroupModel().revoke_user_permission(*permission)
1274 RepoGroupModel().revoke_user_permission(*permission)
1276
1275
1277 for permission in self.user_group_repo_group_permission_ids:
1276 for permission in self.user_group_repo_group_permission_ids:
1278 RepoGroupModel().revoke_user_group_permission(*permission)
1277 RepoGroupModel().revoke_user_group_permission(*permission)
1279
1278
1280 for permission in self.user_user_group_permission_ids:
1279 for permission in self.user_user_group_permission_ids:
1281 UserGroupModel().revoke_user_permission(*permission)
1280 UserGroupModel().revoke_user_permission(*permission)
1282
1281
1283 for permission in self.user_group_user_group_permission_ids:
1282 for permission in self.user_group_user_group_permission_ids:
1284 UserGroupModel().revoke_user_group_permission(*permission)
1283 UserGroupModel().revoke_user_group_permission(*permission)
1285
1284
1286 def _cleanup_repo_groups(self):
1285 def _cleanup_repo_groups(self):
1287 def _repo_group_compare(first_group_id, second_group_id):
1286 def _repo_group_compare(first_group_id, second_group_id):
1288 """
1287 """
1289 Gives higher priority to the groups with the most complex paths
1288 Gives higher priority to the groups with the most complex paths
1290 """
1289 """
1291 first_group = RepoGroup.get(first_group_id)
1290 first_group = RepoGroup.get(first_group_id)
1292 second_group = RepoGroup.get(second_group_id)
1291 second_group = RepoGroup.get(second_group_id)
1293 first_group_parts = (
1292 first_group_parts = (
1294 len(first_group.group_name.split('/')) if first_group else 0)
1293 len(first_group.group_name.split('/')) if first_group else 0)
1295 second_group_parts = (
1294 second_group_parts = (
1296 len(second_group.group_name.split('/')) if second_group else 0)
1295 len(second_group.group_name.split('/')) if second_group else 0)
1297 return cmp(second_group_parts, first_group_parts)
1296 return cmp(second_group_parts, first_group_parts)
1298
1297
1299 sorted_repo_group_ids = sorted(
1298 sorted_repo_group_ids = sorted(
1300 self.repo_group_ids, cmp=_repo_group_compare)
1299 self.repo_group_ids, cmp=_repo_group_compare)
1301 for repo_group_id in sorted_repo_group_ids:
1300 for repo_group_id in sorted_repo_group_ids:
1302 self.fixture.destroy_repo_group(repo_group_id)
1301 self.fixture.destroy_repo_group(repo_group_id)
1303
1302
1304 def _cleanup_repos(self):
1303 def _cleanup_repos(self):
1305 sorted_repos_ids = sorted(self.repos_ids)
1304 sorted_repos_ids = sorted(self.repos_ids)
1306 for repo_id in sorted_repos_ids:
1305 for repo_id in sorted_repos_ids:
1307 self.fixture.destroy_repo(repo_id)
1306 self.fixture.destroy_repo(repo_id)
1308
1307
1309 def _cleanup_user_groups(self):
1308 def _cleanup_user_groups(self):
1310 def _user_group_compare(first_group_id, second_group_id):
1309 def _user_group_compare(first_group_id, second_group_id):
1311 """
1310 """
1312 Gives higher priority to the groups with the most complex paths
1311 Gives higher priority to the groups with the most complex paths
1313 """
1312 """
1314 first_group = UserGroup.get(first_group_id)
1313 first_group = UserGroup.get(first_group_id)
1315 second_group = UserGroup.get(second_group_id)
1314 second_group = UserGroup.get(second_group_id)
1316 first_group_parts = (
1315 first_group_parts = (
1317 len(first_group.users_group_name.split('/'))
1316 len(first_group.users_group_name.split('/'))
1318 if first_group else 0)
1317 if first_group else 0)
1319 second_group_parts = (
1318 second_group_parts = (
1320 len(second_group.users_group_name.split('/'))
1319 len(second_group.users_group_name.split('/'))
1321 if second_group else 0)
1320 if second_group else 0)
1322 return cmp(second_group_parts, first_group_parts)
1321 return cmp(second_group_parts, first_group_parts)
1323
1322
1324 sorted_user_group_ids = sorted(
1323 sorted_user_group_ids = sorted(
1325 self.user_group_ids, cmp=_user_group_compare)
1324 self.user_group_ids, cmp=_user_group_compare)
1326 for user_group_id in sorted_user_group_ids:
1325 for user_group_id in sorted_user_group_ids:
1327 self.fixture.destroy_user_group(user_group_id)
1326 self.fixture.destroy_user_group(user_group_id)
1328
1327
1329 def _cleanup_users(self):
1328 def _cleanup_users(self):
1330 for user_id in self.user_ids:
1329 for user_id in self.user_ids:
1331 self.fixture.destroy_user(user_id)
1330 self.fixture.destroy_user(user_id)
1332
1331
1333
1332
1334 # TODO: Think about moving this into a pytest-pyro package and make it a
1333 # TODO: Think about moving this into a pytest-pyro package and make it a
1335 # pytest plugin
1334 # pytest plugin
1336 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1335 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1337 def pytest_runtest_makereport(item, call):
1336 def pytest_runtest_makereport(item, call):
1338 """
1337 """
1339 Adding the remote traceback if the exception has this information.
1338 Adding the remote traceback if the exception has this information.
1340
1339
1341 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1340 VCSServer attaches this information as the attribute `_vcs_server_traceback`
1342 to the exception instance.
1341 to the exception instance.
1343 """
1342 """
1344 outcome = yield
1343 outcome = yield
1345 report = outcome.get_result()
1344 report = outcome.get_result()
1346 if call.excinfo:
1345 if call.excinfo:
1347 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1346 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1348
1347
1349
1348
1350 def _add_vcsserver_remote_traceback(report, exc):
1349 def _add_vcsserver_remote_traceback(report, exc):
1351 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1350 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1352
1351
1353 if vcsserver_traceback:
1352 if vcsserver_traceback:
1354 section = 'VCSServer remote traceback ' + report.when
1353 section = 'VCSServer remote traceback ' + report.when
1355 report.sections.append((section, vcsserver_traceback))
1354 report.sections.append((section, vcsserver_traceback))
1356
1355
1357
1356
1358 @pytest.fixture(scope='session')
1357 @pytest.fixture(scope='session')
1359 def testrun():
1358 def testrun():
1360 return {
1359 return {
1361 'uuid': uuid.uuid4(),
1360 'uuid': uuid.uuid4(),
1362 'start': datetime.datetime.utcnow().isoformat(),
1361 'start': datetime.datetime.utcnow().isoformat(),
1363 'timestamp': int(time.time()),
1362 'timestamp': int(time.time()),
1364 }
1363 }
1365
1364
1366
1365
1367 @pytest.fixture(autouse=True)
1366 @pytest.fixture(autouse=True)
1368 def collect_appenlight_stats(request, testrun):
1367 def collect_appenlight_stats(request, testrun):
1369 """
1368 """
1370 This fixture reports memory consumtion of single tests.
1369 This fixture reports memory consumtion of single tests.
1371
1370
1372 It gathers data based on `psutil` and sends them to Appenlight. The option
1371 It gathers data based on `psutil` and sends them to Appenlight. The option
1373 ``--ae`` has te be used to enable this fixture and the API key for your
1372 ``--ae`` has te be used to enable this fixture and the API key for your
1374 application has to be provided in ``--ae-key``.
1373 application has to be provided in ``--ae-key``.
1375 """
1374 """
1376 try:
1375 try:
1377 # cygwin cannot have yet psutil support.
1376 # cygwin cannot have yet psutil support.
1378 import psutil
1377 import psutil
1379 except ImportError:
1378 except ImportError:
1380 return
1379 return
1381
1380
1382 if not request.config.getoption('--appenlight'):
1381 if not request.config.getoption('--appenlight'):
1383 return
1382 return
1384 else:
1383 else:
1385 # Only request the pylonsapp fixture if appenlight tracking is
1384 # Only request the pylonsapp fixture if appenlight tracking is
1386 # enabled. This will speed up a test run of unit tests by 2 to 3
1385 # enabled. This will speed up a test run of unit tests by 2 to 3
1387 # seconds if appenlight is not enabled.
1386 # seconds if appenlight is not enabled.
1388 pylonsapp = request.getfuncargvalue("pylonsapp")
1387 pylonsapp = request.getfuncargvalue("pylonsapp")
1389 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1388 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1390 client = AppenlightClient(
1389 client = AppenlightClient(
1391 url=url,
1390 url=url,
1392 api_key=request.config.getoption('--appenlight-api-key'),
1391 api_key=request.config.getoption('--appenlight-api-key'),
1393 namespace=request.node.nodeid,
1392 namespace=request.node.nodeid,
1394 request=str(testrun['uuid']),
1393 request=str(testrun['uuid']),
1395 testrun=testrun)
1394 testrun=testrun)
1396
1395
1397 client.collect({
1396 client.collect({
1398 'message': "Starting",
1397 'message': "Starting",
1399 })
1398 })
1400
1399
1401 server_and_port = pylonsapp.config['vcs.server']
1400 server_and_port = pylonsapp.config['vcs.server']
1402 protocol = pylonsapp.config['vcs.server.protocol']
1401 protocol = pylonsapp.config['vcs.server.protocol']
1403 server = create_vcsserver_proxy(server_and_port, protocol)
1402 server = create_vcsserver_proxy(server_and_port, protocol)
1404 with server:
1403 with server:
1405 vcs_pid = server.get_pid()
1404 vcs_pid = server.get_pid()
1406 server.run_gc()
1405 server.run_gc()
1407 vcs_process = psutil.Process(vcs_pid)
1406 vcs_process = psutil.Process(vcs_pid)
1408 mem = vcs_process.memory_info()
1407 mem = vcs_process.memory_info()
1409 client.tag_before('vcsserver.rss', mem.rss)
1408 client.tag_before('vcsserver.rss', mem.rss)
1410 client.tag_before('vcsserver.vms', mem.vms)
1409 client.tag_before('vcsserver.vms', mem.vms)
1411
1410
1412 test_process = psutil.Process()
1411 test_process = psutil.Process()
1413 mem = test_process.memory_info()
1412 mem = test_process.memory_info()
1414 client.tag_before('test.rss', mem.rss)
1413 client.tag_before('test.rss', mem.rss)
1415 client.tag_before('test.vms', mem.vms)
1414 client.tag_before('test.vms', mem.vms)
1416
1415
1417 client.tag_before('time', time.time())
1416 client.tag_before('time', time.time())
1418
1417
1419 @request.addfinalizer
1418 @request.addfinalizer
1420 def send_stats():
1419 def send_stats():
1421 client.tag_after('time', time.time())
1420 client.tag_after('time', time.time())
1422 with server:
1421 with server:
1423 gc_stats = server.run_gc()
1422 gc_stats = server.run_gc()
1424 for tag, value in gc_stats.items():
1423 for tag, value in gc_stats.items():
1425 client.tag_after(tag, value)
1424 client.tag_after(tag, value)
1426 mem = vcs_process.memory_info()
1425 mem = vcs_process.memory_info()
1427 client.tag_after('vcsserver.rss', mem.rss)
1426 client.tag_after('vcsserver.rss', mem.rss)
1428 client.tag_after('vcsserver.vms', mem.vms)
1427 client.tag_after('vcsserver.vms', mem.vms)
1429
1428
1430 mem = test_process.memory_info()
1429 mem = test_process.memory_info()
1431 client.tag_after('test.rss', mem.rss)
1430 client.tag_after('test.rss', mem.rss)
1432 client.tag_after('test.vms', mem.vms)
1431 client.tag_after('test.vms', mem.vms)
1433
1432
1434 client.collect({
1433 client.collect({
1435 'message': "Finished",
1434 'message': "Finished",
1436 })
1435 })
1437 client.send_stats()
1436 client.send_stats()
1438
1437
1439 return client
1438 return client
1440
1439
1441
1440
1442 class AppenlightClient():
1441 class AppenlightClient():
1443
1442
1444 url_template = '{url}?protocol_version=0.5'
1443 url_template = '{url}?protocol_version=0.5'
1445
1444
1446 def __init__(
1445 def __init__(
1447 self, url, api_key, add_server=True, add_timestamp=True,
1446 self, url, api_key, add_server=True, add_timestamp=True,
1448 namespace=None, request=None, testrun=None):
1447 namespace=None, request=None, testrun=None):
1449 self.url = self.url_template.format(url=url)
1448 self.url = self.url_template.format(url=url)
1450 self.api_key = api_key
1449 self.api_key = api_key
1451 self.add_server = add_server
1450 self.add_server = add_server
1452 self.add_timestamp = add_timestamp
1451 self.add_timestamp = add_timestamp
1453 self.namespace = namespace
1452 self.namespace = namespace
1454 self.request = request
1453 self.request = request
1455 self.server = socket.getfqdn(socket.gethostname())
1454 self.server = socket.getfqdn(socket.gethostname())
1456 self.tags_before = {}
1455 self.tags_before = {}
1457 self.tags_after = {}
1456 self.tags_after = {}
1458 self.stats = []
1457 self.stats = []
1459 self.testrun = testrun or {}
1458 self.testrun = testrun or {}
1460
1459
1461 def tag_before(self, tag, value):
1460 def tag_before(self, tag, value):
1462 self.tags_before[tag] = value
1461 self.tags_before[tag] = value
1463
1462
1464 def tag_after(self, tag, value):
1463 def tag_after(self, tag, value):
1465 self.tags_after[tag] = value
1464 self.tags_after[tag] = value
1466
1465
1467 def collect(self, data):
1466 def collect(self, data):
1468 if self.add_server:
1467 if self.add_server:
1469 data.setdefault('server', self.server)
1468 data.setdefault('server', self.server)
1470 if self.add_timestamp:
1469 if self.add_timestamp:
1471 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1470 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1472 if self.namespace:
1471 if self.namespace:
1473 data.setdefault('namespace', self.namespace)
1472 data.setdefault('namespace', self.namespace)
1474 if self.request:
1473 if self.request:
1475 data.setdefault('request', self.request)
1474 data.setdefault('request', self.request)
1476 self.stats.append(data)
1475 self.stats.append(data)
1477
1476
1478 def send_stats(self):
1477 def send_stats(self):
1479 tags = [
1478 tags = [
1480 ('testrun', self.request),
1479 ('testrun', self.request),
1481 ('testrun.start', self.testrun['start']),
1480 ('testrun.start', self.testrun['start']),
1482 ('testrun.timestamp', self.testrun['timestamp']),
1481 ('testrun.timestamp', self.testrun['timestamp']),
1483 ('test', self.namespace),
1482 ('test', self.namespace),
1484 ]
1483 ]
1485 for key, value in self.tags_before.items():
1484 for key, value in self.tags_before.items():
1486 tags.append((key + '.before', value))
1485 tags.append((key + '.before', value))
1487 try:
1486 try:
1488 delta = self.tags_after[key] - value
1487 delta = self.tags_after[key] - value
1489 tags.append((key + '.delta', delta))
1488 tags.append((key + '.delta', delta))
1490 except Exception:
1489 except Exception:
1491 pass
1490 pass
1492 for key, value in self.tags_after.items():
1491 for key, value in self.tags_after.items():
1493 tags.append((key + '.after', value))
1492 tags.append((key + '.after', value))
1494 self.collect({
1493 self.collect({
1495 'message': "Collected tags",
1494 'message': "Collected tags",
1496 'tags': tags,
1495 'tags': tags,
1497 })
1496 })
1498
1497
1499 response = requests.post(
1498 response = requests.post(
1500 self.url,
1499 self.url,
1501 headers={
1500 headers={
1502 'X-appenlight-api-key': self.api_key},
1501 'X-appenlight-api-key': self.api_key},
1503 json=self.stats,
1502 json=self.stats,
1504 )
1503 )
1505
1504
1506 if not response.status_code == 200:
1505 if not response.status_code == 200:
1507 pprint.pprint(self.stats)
1506 pprint.pprint(self.stats)
1508 print response.headers
1507 print response.headers
1509 print response.text
1508 print response.text
1510 raise Exception('Sending to appenlight failed')
1509 raise Exception('Sending to appenlight failed')
1511
1510
1512
1511
1513 @pytest.fixture
1512 @pytest.fixture
1514 def gist_util(request, pylonsapp):
1513 def gist_util(request, pylonsapp):
1515 """
1514 """
1516 Provides a wired instance of `GistUtility` with integrated cleanup.
1515 Provides a wired instance of `GistUtility` with integrated cleanup.
1517 """
1516 """
1518 utility = GistUtility()
1517 utility = GistUtility()
1519 request.addfinalizer(utility.cleanup)
1518 request.addfinalizer(utility.cleanup)
1520 return utility
1519 return utility
1521
1520
1522
1521
1523 class GistUtility(object):
1522 class GistUtility(object):
1524 def __init__(self):
1523 def __init__(self):
1525 self.fixture = Fixture()
1524 self.fixture = Fixture()
1526 self.gist_ids = []
1525 self.gist_ids = []
1527
1526
1528 def create_gist(self, **kwargs):
1527 def create_gist(self, **kwargs):
1529 gist = self.fixture.create_gist(**kwargs)
1528 gist = self.fixture.create_gist(**kwargs)
1530 self.gist_ids.append(gist.gist_id)
1529 self.gist_ids.append(gist.gist_id)
1531 return gist
1530 return gist
1532
1531
1533 def cleanup(self):
1532 def cleanup(self):
1534 for id_ in self.gist_ids:
1533 for id_ in self.gist_ids:
1535 self.fixture.destroy_gists(str(id_))
1534 self.fixture.destroy_gists(str(id_))
1536
1535
1537
1536
1538 @pytest.fixture
1537 @pytest.fixture
1539 def enabled_backends(request):
1538 def enabled_backends(request):
1540 backends = request.config.option.backends
1539 backends = request.config.option.backends
1541 return backends[:]
1540 return backends[:]
1542
1541
1543
1542
1544 @pytest.fixture
1543 @pytest.fixture
1545 def settings_util(request):
1544 def settings_util(request):
1546 """
1545 """
1547 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1546 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1548 """
1547 """
1549 utility = SettingsUtility()
1548 utility = SettingsUtility()
1550 request.addfinalizer(utility.cleanup)
1549 request.addfinalizer(utility.cleanup)
1551 return utility
1550 return utility
1552
1551
1553
1552
1554 class SettingsUtility(object):
1553 class SettingsUtility(object):
1555 def __init__(self):
1554 def __init__(self):
1556 self.rhodecode_ui_ids = []
1555 self.rhodecode_ui_ids = []
1557 self.rhodecode_setting_ids = []
1556 self.rhodecode_setting_ids = []
1558 self.repo_rhodecode_ui_ids = []
1557 self.repo_rhodecode_ui_ids = []
1559 self.repo_rhodecode_setting_ids = []
1558 self.repo_rhodecode_setting_ids = []
1560
1559
1561 def create_repo_rhodecode_ui(
1560 def create_repo_rhodecode_ui(
1562 self, repo, section, value, key=None, active=True, cleanup=True):
1561 self, repo, section, value, key=None, active=True, cleanup=True):
1563 key = key or hashlib.sha1(
1562 key = key or hashlib.sha1(
1564 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1563 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1565
1564
1566 setting = RepoRhodeCodeUi()
1565 setting = RepoRhodeCodeUi()
1567 setting.repository_id = repo.repo_id
1566 setting.repository_id = repo.repo_id
1568 setting.ui_section = section
1567 setting.ui_section = section
1569 setting.ui_value = value
1568 setting.ui_value = value
1570 setting.ui_key = key
1569 setting.ui_key = key
1571 setting.ui_active = active
1570 setting.ui_active = active
1572 Session().add(setting)
1571 Session().add(setting)
1573 Session().commit()
1572 Session().commit()
1574
1573
1575 if cleanup:
1574 if cleanup:
1576 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1575 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1577 return setting
1576 return setting
1578
1577
1579 def create_rhodecode_ui(
1578 def create_rhodecode_ui(
1580 self, section, value, key=None, active=True, cleanup=True):
1579 self, section, value, key=None, active=True, cleanup=True):
1581 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1580 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1582
1581
1583 setting = RhodeCodeUi()
1582 setting = RhodeCodeUi()
1584 setting.ui_section = section
1583 setting.ui_section = section
1585 setting.ui_value = value
1584 setting.ui_value = value
1586 setting.ui_key = key
1585 setting.ui_key = key
1587 setting.ui_active = active
1586 setting.ui_active = active
1588 Session().add(setting)
1587 Session().add(setting)
1589 Session().commit()
1588 Session().commit()
1590
1589
1591 if cleanup:
1590 if cleanup:
1592 self.rhodecode_ui_ids.append(setting.ui_id)
1591 self.rhodecode_ui_ids.append(setting.ui_id)
1593 return setting
1592 return setting
1594
1593
1595 def create_repo_rhodecode_setting(
1594 def create_repo_rhodecode_setting(
1596 self, repo, name, value, type_, cleanup=True):
1595 self, repo, name, value, type_, cleanup=True):
1597 setting = RepoRhodeCodeSetting(
1596 setting = RepoRhodeCodeSetting(
1598 repo.repo_id, key=name, val=value, type=type_)
1597 repo.repo_id, key=name, val=value, type=type_)
1599 Session().add(setting)
1598 Session().add(setting)
1600 Session().commit()
1599 Session().commit()
1601
1600
1602 if cleanup:
1601 if cleanup:
1603 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1602 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1604 return setting
1603 return setting
1605
1604
1606 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1605 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1607 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1606 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1608 Session().add(setting)
1607 Session().add(setting)
1609 Session().commit()
1608 Session().commit()
1610
1609
1611 if cleanup:
1610 if cleanup:
1612 self.rhodecode_setting_ids.append(setting.app_settings_id)
1611 self.rhodecode_setting_ids.append(setting.app_settings_id)
1613
1612
1614 return setting
1613 return setting
1615
1614
1616 def cleanup(self):
1615 def cleanup(self):
1617 for id_ in self.rhodecode_ui_ids:
1616 for id_ in self.rhodecode_ui_ids:
1618 setting = RhodeCodeUi.get(id_)
1617 setting = RhodeCodeUi.get(id_)
1619 Session().delete(setting)
1618 Session().delete(setting)
1620
1619
1621 for id_ in self.rhodecode_setting_ids:
1620 for id_ in self.rhodecode_setting_ids:
1622 setting = RhodeCodeSetting.get(id_)
1621 setting = RhodeCodeSetting.get(id_)
1623 Session().delete(setting)
1622 Session().delete(setting)
1624
1623
1625 for id_ in self.repo_rhodecode_ui_ids:
1624 for id_ in self.repo_rhodecode_ui_ids:
1626 setting = RepoRhodeCodeUi.get(id_)
1625 setting = RepoRhodeCodeUi.get(id_)
1627 Session().delete(setting)
1626 Session().delete(setting)
1628
1627
1629 for id_ in self.repo_rhodecode_setting_ids:
1628 for id_ in self.repo_rhodecode_setting_ids:
1630 setting = RepoRhodeCodeSetting.get(id_)
1629 setting = RepoRhodeCodeSetting.get(id_)
1631 Session().delete(setting)
1630 Session().delete(setting)
1632
1631
1633 Session().commit()
1632 Session().commit()
1634
1633
1635
1634
1636 @pytest.fixture
1635 @pytest.fixture
1637 def no_notifications(request):
1636 def no_notifications(request):
1638 notification_patcher = mock.patch(
1637 notification_patcher = mock.patch(
1639 'rhodecode.model.notification.NotificationModel.create')
1638 'rhodecode.model.notification.NotificationModel.create')
1640 notification_patcher.start()
1639 notification_patcher.start()
1641 request.addfinalizer(notification_patcher.stop)
1640 request.addfinalizer(notification_patcher.stop)
1642
1641
1643
1642
1644 @pytest.fixture
1643 @pytest.fixture
1645 def silence_action_logger(request):
1644 def silence_action_logger(request):
1646 notification_patcher = mock.patch(
1645 notification_patcher = mock.patch(
1647 'rhodecode.lib.utils.action_logger')
1646 'rhodecode.lib.utils.action_logger')
1648 notification_patcher.start()
1647 notification_patcher.start()
1649 request.addfinalizer(notification_patcher.stop)
1648 request.addfinalizer(notification_patcher.stop)
1650
1649
1651
1650
1652 @pytest.fixture(scope='session')
1651 @pytest.fixture(scope='session')
1653 def repeat(request):
1652 def repeat(request):
1654 """
1653 """
1655 The number of repetitions is based on this fixture.
1654 The number of repetitions is based on this fixture.
1656
1655
1657 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1656 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1658 tests are not too slow in our default test suite.
1657 tests are not too slow in our default test suite.
1659 """
1658 """
1660 return request.config.getoption('--repeat')
1659 return request.config.getoption('--repeat')
1661
1660
1662
1661
1663 @pytest.fixture
1662 @pytest.fixture
1664 def rhodecode_fixtures():
1663 def rhodecode_fixtures():
1665 return Fixture()
1664 return Fixture()
1666
1665
1667
1666
1668 @pytest.fixture
1667 @pytest.fixture
1669 def request_stub():
1668 def request_stub():
1670 """
1669 """
1671 Stub request object.
1670 Stub request object.
1672 """
1671 """
1673 request = pyramid.testing.DummyRequest()
1672 request = pyramid.testing.DummyRequest()
1674 request.scheme = 'https'
1673 request.scheme = 'https'
1675 return request
1674 return request
1676
1675
1677
1676
1678 @pytest.fixture
1677 @pytest.fixture
1679 def config_stub(request, request_stub):
1678 def config_stub(request, request_stub):
1680 """
1679 """
1681 Set up pyramid.testing and return the Configurator.
1680 Set up pyramid.testing and return the Configurator.
1682 """
1681 """
1683 config = pyramid.testing.setUp(request=request_stub)
1682 config = pyramid.testing.setUp(request=request_stub)
1684
1683
1685 @request.addfinalizer
1684 @request.addfinalizer
1686 def cleanup():
1685 def cleanup():
1687 pyramid.testing.tearDown()
1686 pyramid.testing.tearDown()
1688
1687
1689 return config
1688 return config
1690
1689
1691
1690
1692 @pytest.fixture
1691 @pytest.fixture
1693 def StubIntegrationType():
1692 def StubIntegrationType():
1694 class _StubIntegrationType(IntegrationTypeBase):
1693 class _StubIntegrationType(IntegrationTypeBase):
1695 """ Test integration type class """
1694 """ Test integration type class """
1696
1695
1697 key = 'test'
1696 key = 'test'
1698 display_name = 'Test integration type'
1697 display_name = 'Test integration type'
1699 description = 'A test integration type for testing'
1698 description = 'A test integration type for testing'
1700 icon = 'test_icon_html_image'
1699 icon = 'test_icon_html_image'
1701
1700
1702 def __init__(self, settings):
1701 def __init__(self, settings):
1703 super(_StubIntegrationType, self).__init__(settings)
1702 super(_StubIntegrationType, self).__init__(settings)
1704 self.sent_events = [] # for testing
1703 self.sent_events = [] # for testing
1705
1704
1706 def send_event(self, event):
1705 def send_event(self, event):
1707 self.sent_events.append(event)
1706 self.sent_events.append(event)
1708
1707
1709 def settings_schema(self):
1708 def settings_schema(self):
1710 class SettingsSchema(colander.Schema):
1709 class SettingsSchema(colander.Schema):
1711 test_string_field = colander.SchemaNode(
1710 test_string_field = colander.SchemaNode(
1712 colander.String(),
1711 colander.String(),
1713 missing=colander.required,
1712 missing=colander.required,
1714 title='test string field',
1713 title='test string field',
1715 )
1714 )
1716 test_int_field = colander.SchemaNode(
1715 test_int_field = colander.SchemaNode(
1717 colander.Int(),
1716 colander.Int(),
1718 title='some integer setting',
1717 title='some integer setting',
1719 )
1718 )
1720 return SettingsSchema()
1719 return SettingsSchema()
1721
1720
1722
1721
1723 integration_type_registry.register_integration_type(_StubIntegrationType)
1722 integration_type_registry.register_integration_type(_StubIntegrationType)
1724 return _StubIntegrationType
1723 return _StubIntegrationType
1725
1724
1726 @pytest.fixture
1725 @pytest.fixture
1727 def stub_integration_settings():
1726 def stub_integration_settings():
1728 return {
1727 return {
1729 'test_string_field': 'some data',
1728 'test_string_field': 'some data',
1730 'test_int_field': 100,
1729 'test_int_field': 100,
1731 }
1730 }
1732
1731
1733
1732
1734 @pytest.fixture
1733 @pytest.fixture
1735 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1734 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1736 stub_integration_settings):
1735 stub_integration_settings):
1737 integration = IntegrationModel().create(
1736 integration = IntegrationModel().create(
1738 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1737 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1739 name='test repo integration',
1738 name='test repo integration',
1740 repo=repo_stub, repo_group=None, child_repos_only=None)
1739 repo=repo_stub, repo_group=None, child_repos_only=None)
1741
1740
1742 @request.addfinalizer
1741 @request.addfinalizer
1743 def cleanup():
1742 def cleanup():
1744 IntegrationModel().delete(integration)
1743 IntegrationModel().delete(integration)
1745
1744
1746 return integration
1745 return integration
1747
1746
1748
1747
1749 @pytest.fixture
1748 @pytest.fixture
1750 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1749 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1751 stub_integration_settings):
1750 stub_integration_settings):
1752 integration = IntegrationModel().create(
1751 integration = IntegrationModel().create(
1753 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1752 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1754 name='test repogroup integration',
1753 name='test repogroup integration',
1755 repo=None, repo_group=test_repo_group, child_repos_only=True)
1754 repo=None, repo_group=test_repo_group, child_repos_only=True)
1756
1755
1757 @request.addfinalizer
1756 @request.addfinalizer
1758 def cleanup():
1757 def cleanup():
1759 IntegrationModel().delete(integration)
1758 IntegrationModel().delete(integration)
1760
1759
1761 return integration
1760 return integration
1762
1761
1763
1762
1764 @pytest.fixture
1763 @pytest.fixture
1765 def repogroup_recursive_integration_stub(request, test_repo_group,
1764 def repogroup_recursive_integration_stub(request, test_repo_group,
1766 StubIntegrationType, stub_integration_settings):
1765 StubIntegrationType, stub_integration_settings):
1767 integration = IntegrationModel().create(
1766 integration = IntegrationModel().create(
1768 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1767 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1769 name='test recursive repogroup integration',
1768 name='test recursive repogroup integration',
1770 repo=None, repo_group=test_repo_group, child_repos_only=False)
1769 repo=None, repo_group=test_repo_group, child_repos_only=False)
1771
1770
1772 @request.addfinalizer
1771 @request.addfinalizer
1773 def cleanup():
1772 def cleanup():
1774 IntegrationModel().delete(integration)
1773 IntegrationModel().delete(integration)
1775
1774
1776 return integration
1775 return integration
1777
1776
1778
1777
1779 @pytest.fixture
1778 @pytest.fixture
1780 def global_integration_stub(request, StubIntegrationType,
1779 def global_integration_stub(request, StubIntegrationType,
1781 stub_integration_settings):
1780 stub_integration_settings):
1782 integration = IntegrationModel().create(
1781 integration = IntegrationModel().create(
1783 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1782 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1784 name='test global integration',
1783 name='test global integration',
1785 repo=None, repo_group=None, child_repos_only=None)
1784 repo=None, repo_group=None, child_repos_only=None)
1786
1785
1787 @request.addfinalizer
1786 @request.addfinalizer
1788 def cleanup():
1787 def cleanup():
1789 IntegrationModel().delete(integration)
1788 IntegrationModel().delete(integration)
1790
1789
1791 return integration
1790 return integration
1792
1791
1793
1792
1794 @pytest.fixture
1793 @pytest.fixture
1795 def root_repos_integration_stub(request, StubIntegrationType,
1794 def root_repos_integration_stub(request, StubIntegrationType,
1796 stub_integration_settings):
1795 stub_integration_settings):
1797 integration = IntegrationModel().create(
1796 integration = IntegrationModel().create(
1798 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1797 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1799 name='test global integration',
1798 name='test global integration',
1800 repo=None, repo_group=None, child_repos_only=True)
1799 repo=None, repo_group=None, child_repos_only=True)
1801
1800
1802 @request.addfinalizer
1801 @request.addfinalizer
1803 def cleanup():
1802 def cleanup():
1804 IntegrationModel().delete(integration)
1803 IntegrationModel().delete(integration)
1805
1804
1806 return integration
1805 return integration
1807
1806
1808
1807
1809 @pytest.fixture
1808 @pytest.fixture
1810 def local_dt_to_utc():
1809 def local_dt_to_utc():
1811 def _factory(dt):
1810 def _factory(dt):
1812 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1811 return dt.replace(tzinfo=dateutil.tz.tzlocal()).astimezone(
1813 dateutil.tz.tzutc()).replace(tzinfo=None)
1812 dateutil.tz.tzutc()).replace(tzinfo=None)
1814 return _factory
1813 return _factory
General Comments 0
You need to be logged in to leave comments. Login now