##// END OF EJS Templates
pull-request: extended default reviewers functionality....
marcink -
r1769:fb9baff8 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 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 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'https://code.rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,279 +1,297 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import User
24 24 from rhodecode.model.pull_request import PullRequestModel
25 25 from rhodecode.model.repo import RepoModel
26 26 from rhodecode.model.user import UserModel
27 27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestCreatePullRequestApi(object):
33 33 finalizers = []
34 34
35 35 def teardown_method(self, method):
36 36 if self.finalizers:
37 37 for finalizer in self.finalizers:
38 38 finalizer()
39 39 self.finalizers = []
40 40
41 41 def test_create_with_wrong_data(self):
42 42 required_data = {
43 43 'source_repo': 'tests/source_repo',
44 44 'target_repo': 'tests/target_repo',
45 45 'source_ref': 'branch:default:initial',
46 46 'target_ref': 'branch:default:new-feature',
47 47 'title': 'Test PR 1'
48 48 }
49 49 for key in required_data:
50 50 data = required_data.copy()
51 51 data.pop(key)
52 52 id_, params = build_data(
53 53 self.apikey, 'create_pull_request', **data)
54 54 response = api_call(self.app, params)
55 55
56 56 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
57 57 assert_error(id_, expected, given=response.body)
58 58
59 59 @pytest.mark.backends("git", "hg")
60 60 def test_create_with_correct_data(self, backend):
61 61 data = self._prepare_data(backend)
62 62 RepoModel().revoke_user_permission(
63 63 self.source.repo_name, User.DEFAULT_USER)
64 64 id_, params = build_data(
65 65 self.apikey_regular, 'create_pull_request', **data)
66 66 response = api_call(self.app, params)
67 67 expected_message = "Created new pull request `{title}`".format(
68 68 title=data['title'])
69 69 result = response.json
70 70 assert result['result']['msg'] == expected_message
71 71 pull_request_id = result['result']['pull_request_id']
72 72 pull_request = PullRequestModel().get(pull_request_id)
73 73 assert pull_request.title == data['title']
74 74 assert pull_request.description == data['description']
75 75 assert pull_request.source_ref == data['source_ref']
76 76 assert pull_request.target_ref == data['target_ref']
77 77 assert pull_request.source_repo.repo_name == data['source_repo']
78 78 assert pull_request.target_repo.repo_name == data['target_repo']
79 79 assert pull_request.revisions == [self.commit_ids['change']]
80 80 assert pull_request.reviewers == []
81 81
82 82 @pytest.mark.backends("git", "hg")
83 83 def test_create_with_empty_description(self, backend):
84 84 data = self._prepare_data(backend)
85 85 data.pop('description')
86 86 id_, params = build_data(
87 87 self.apikey_regular, 'create_pull_request', **data)
88 88 response = api_call(self.app, params)
89 89 expected_message = "Created new pull request `{title}`".format(
90 90 title=data['title'])
91 91 result = response.json
92 92 assert result['result']['msg'] == expected_message
93 93 pull_request_id = result['result']['pull_request_id']
94 94 pull_request = PullRequestModel().get(pull_request_id)
95 95 assert pull_request.description == ''
96 96
97 97 @pytest.mark.backends("git", "hg")
98 98 def test_create_with_reviewers_specified_by_names(
99 99 self, backend, no_notifications):
100 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 107 data['reviewers'] = reviewers
103 108 id_, params = build_data(
104 109 self.apikey_regular, 'create_pull_request', **data)
105 110 response = api_call(self.app, params)
106 111
107 112 expected_message = "Created new pull request `{title}`".format(
108 113 title=data['title'])
109 114 result = response.json
110 115 assert result['result']['msg'] == expected_message
111 116 pull_request_id = result['result']['pull_request_id']
112 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 123 assert sorted(actual_reviewers) == sorted(reviewers)
115 124
116 125 @pytest.mark.backends("git", "hg")
117 126 def test_create_with_reviewers_specified_by_ids(
118 127 self, backend, no_notifications):
119 128 data = self._prepare_data(backend)
120 reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
121 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 138 data['reviewers'] = reviewers
124 139 id_, params = build_data(
125 140 self.apikey_regular, 'create_pull_request', **data)
126 141 response = api_call(self.app, params)
127 142
128 143 expected_message = "Created new pull request `{title}`".format(
129 144 title=data['title'])
130 145 result = response.json
131 146 assert result['result']['msg'] == expected_message
132 147 pull_request_id = result['result']['pull_request_id']
133 148 pull_request = PullRequestModel().get(pull_request_id)
134 actual_reviewers = [r.user.username for r in pull_request.reviewers]
135 assert sorted(actual_reviewers) == sorted(reviewer_names)
149 actual_reviewers = [
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 156 @pytest.mark.backends("git", "hg")
138 157 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
139 158 data = self._prepare_data(backend)
140 reviewers = ['somebody']
141 data['reviewers'] = reviewers
159 data['reviewers'] = [{'username': 'somebody'}]
142 160 id_, params = build_data(
143 161 self.apikey_regular, 'create_pull_request', **data)
144 162 response = api_call(self.app, params)
145 163 expected_message = 'user `somebody` does not exist'
146 164 assert_error(id_, expected_message, given=response.body)
147 165
148 166 @pytest.mark.backends("git", "hg")
149 167 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
150 168 data = self._prepare_data(backend)
151 169 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
152 170 data['reviewers'] = reviewers
153 171 id_, params = build_data(
154 172 self.apikey_regular, 'create_pull_request', **data)
155 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 175 assert_error(id_, expected_message, given=response.body)
158 176
159 177 @pytest.mark.backends("git", "hg")
160 178 def test_create_with_no_commit_hashes(self, backend):
161 179 data = self._prepare_data(backend)
162 180 expected_source_ref = data['source_ref']
163 181 expected_target_ref = data['target_ref']
164 182 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
165 183 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
166 184 id_, params = build_data(
167 185 self.apikey_regular, 'create_pull_request', **data)
168 186 response = api_call(self.app, params)
169 187 expected_message = "Created new pull request `{title}`".format(
170 188 title=data['title'])
171 189 result = response.json
172 190 assert result['result']['msg'] == expected_message
173 191 pull_request_id = result['result']['pull_request_id']
174 192 pull_request = PullRequestModel().get(pull_request_id)
175 193 assert pull_request.source_ref == expected_source_ref
176 194 assert pull_request.target_ref == expected_target_ref
177 195
178 196 @pytest.mark.backends("git", "hg")
179 197 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
180 198 def test_create_fails_with_wrong_repo(self, backend, data_key):
181 199 repo_name = 'fake-repo'
182 200 data = self._prepare_data(backend)
183 201 data[data_key] = repo_name
184 202 id_, params = build_data(
185 203 self.apikey_regular, 'create_pull_request', **data)
186 204 response = api_call(self.app, params)
187 205 expected_message = 'repository `{}` does not exist'.format(repo_name)
188 206 assert_error(id_, expected_message, given=response.body)
189 207
190 208 @pytest.mark.backends("git", "hg")
191 209 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
192 210 def test_create_fails_with_non_existing_branch(self, backend, data_key):
193 211 branch_name = 'test-branch'
194 212 data = self._prepare_data(backend)
195 213 data[data_key] = "branch:{}".format(branch_name)
196 214 id_, params = build_data(
197 215 self.apikey_regular, 'create_pull_request', **data)
198 216 response = api_call(self.app, params)
199 217 expected_message = 'The specified branch `{}` does not exist'.format(
200 218 branch_name)
201 219 assert_error(id_, expected_message, given=response.body)
202 220
203 221 @pytest.mark.backends("git", "hg")
204 222 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
205 223 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
206 224 data = self._prepare_data(backend)
207 225 ref = 'stange-ref'
208 226 data[data_key] = ref
209 227 id_, params = build_data(
210 228 self.apikey_regular, 'create_pull_request', **data)
211 229 response = api_call(self.app, params)
212 230 expected_message = (
213 231 'Ref `{ref}` given in a wrong format. Please check the API'
214 232 ' documentation for more details'.format(ref=ref))
215 233 assert_error(id_, expected_message, given=response.body)
216 234
217 235 @pytest.mark.backends("git", "hg")
218 236 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
219 237 def test_create_fails_with_non_existing_ref(self, backend, data_key):
220 238 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
221 239 ref = self._get_full_ref(backend, commit_id)
222 240 data = self._prepare_data(backend)
223 241 data[data_key] = ref
224 242 id_, params = build_data(
225 243 self.apikey_regular, 'create_pull_request', **data)
226 244 response = api_call(self.app, params)
227 245 expected_message = 'Ref `{}` does not exist'.format(ref)
228 246 assert_error(id_, expected_message, given=response.body)
229 247
230 248 @pytest.mark.backends("git", "hg")
231 249 def test_create_fails_when_no_revisions(self, backend):
232 250 data = self._prepare_data(backend, source_head='initial')
233 251 id_, params = build_data(
234 252 self.apikey_regular, 'create_pull_request', **data)
235 253 response = api_call(self.app, params)
236 254 expected_message = 'no commits found'
237 255 assert_error(id_, expected_message, given=response.body)
238 256
239 257 @pytest.mark.backends("git", "hg")
240 258 def test_create_fails_when_no_permissions(self, backend):
241 259 data = self._prepare_data(backend)
242 260 RepoModel().revoke_user_permission(
243 261 self.source.repo_name, User.DEFAULT_USER)
244 262 RepoModel().revoke_user_permission(
245 263 self.source.repo_name, self.test_user)
246 264 id_, params = build_data(
247 265 self.apikey_regular, 'create_pull_request', **data)
248 266 response = api_call(self.app, params)
249 267 expected_message = 'repository `{}` does not exist'.format(
250 268 self.source.repo_name)
251 269 assert_error(id_, expected_message, given=response.body)
252 270
253 271 def _prepare_data(
254 272 self, backend, source_head='change', target_head='initial'):
255 273 commits = [
256 274 {'message': 'initial'},
257 275 {'message': 'change'},
258 276 {'message': 'new-feature', 'parents': ['initial']},
259 277 ]
260 278 self.commit_ids = backend.create_master_repo(commits)
261 279 self.source = backend.create_repo(heads=[source_head])
262 280 self.target = backend.create_repo(heads=[target_head])
263 281 data = {
264 282 'source_repo': self.source.repo_name,
265 283 'target_repo': self.target.repo_name,
266 284 'source_ref': self._get_full_ref(
267 285 backend, self.commit_ids[source_head]),
268 286 'target_ref': self._get_full_ref(
269 287 backend, self.commit_ids[target_head]),
270 288 'title': 'Test PR 1',
271 289 'description': 'Test'
272 290 }
273 291 RepoModel().grant_user_permission(
274 292 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
275 293 return data
276 294
277 295 def _get_full_ref(self, backend, commit_id):
278 296 return 'branch:{branch}:{commit_id}'.format(
279 297 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,133 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import mock
23 23 import pytest
24 24 import urlobject
25 25 from pylons import url
26 26
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_error, assert_ok)
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequest(object):
35 35
36 36 def test_api_get_pull_request(self, pr_util):
37 37 from rhodecode.model.pull_request import PullRequestModel
38 38 pull_request = pr_util.create_pull_request(mergeable=True)
39 39 id_, params = build_data(
40 40 self.apikey, 'get_pull_request',
41 41 repoid=pull_request.target_repo.repo_name,
42 42 pullrequestid=pull_request.pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 assert response.status == '200 OK'
47 47
48 48 url_obj = urlobject.URLObject(
49 49 url(
50 50 'pullrequest_show',
51 51 repo_name=pull_request.target_repo.repo_name,
52 52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 53 pr_url = unicode(
54 54 url_obj.with_netloc('test.example.com:80'))
55 55 source_url = unicode(
56 56 pull_request.source_repo.clone_url()
57 57 .with_netloc('test.example.com:80'))
58 58 target_url = unicode(
59 59 pull_request.target_repo.clone_url()
60 60 .with_netloc('test.example.com:80'))
61 61 shadow_url = unicode(
62 62 PullRequestModel().get_shadow_clone_url(pull_request))
63 63 expected = {
64 64 'pull_request_id': pull_request.pull_request_id,
65 65 'url': pr_url,
66 66 'title': pull_request.title,
67 67 'description': pull_request.description,
68 68 'status': pull_request.status,
69 69 'created_on': pull_request.created_on,
70 70 'updated_on': pull_request.updated_on,
71 71 'commit_ids': pull_request.revisions,
72 72 'review_status': pull_request.calculated_review_status(),
73 73 'mergeable': {
74 74 'status': True,
75 75 'message': 'This pull request can be automatically merged.',
76 76 },
77 77 'source': {
78 78 'clone_url': source_url,
79 79 'repository': pull_request.source_repo.repo_name,
80 80 'reference': {
81 81 'name': pull_request.source_ref_parts.name,
82 82 'type': pull_request.source_ref_parts.type,
83 83 'commit_id': pull_request.source_ref_parts.commit_id,
84 84 },
85 85 },
86 86 'target': {
87 87 'clone_url': target_url,
88 88 'repository': pull_request.target_repo.repo_name,
89 89 'reference': {
90 90 'name': pull_request.target_ref_parts.name,
91 91 'type': pull_request.target_ref_parts.type,
92 92 'commit_id': pull_request.target_ref_parts.commit_id,
93 93 },
94 94 },
95 95 'merge': {
96 96 'clone_url': shadow_url,
97 97 'reference': {
98 98 'name': pull_request.shadow_merge_ref.name,
99 99 'type': pull_request.shadow_merge_ref.type,
100 100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 101 },
102 102 },
103 103 'author': pull_request.author.get_api_data(include_secrets=False,
104 104 details='basic'),
105 105 'reviewers': [
106 106 {
107 107 'user': reviewer.get_api_data(include_secrets=False,
108 108 details='basic'),
109 109 'reasons': reasons,
110 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 116 assert_ok(id_, expected, response.body)
116 117
117 118 def test_api_get_pull_request_repo_error(self):
118 119 id_, params = build_data(
119 120 self.apikey, 'get_pull_request',
120 121 repoid=666, pullrequestid=1)
121 122 response = api_call(self.app, params)
122 123
123 124 expected = 'repository `666` does not exist'
124 125 assert_error(id_, expected, given=response.body)
125 126
126 127 def test_api_get_pull_request_pull_request_error(self):
127 128 id_, params = build_data(
128 129 self.apikey, 'get_pull_request',
129 130 repoid=1, pullrequestid=666)
130 131 response = api_call(self.app, params)
131 132
132 133 expected = 'pull request `666` does not exist'
133 134 assert_error(id_, expected, given=response.body)
@@ -1,205 +1,213 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.vcs.nodes import FileNode
24 24 from rhodecode.model.db import User
25 25 from rhodecode.model.pull_request import PullRequestModel
26 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_ok, assert_error)
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestUpdatePullRequest(object):
33 33
34 34 @pytest.mark.backends("git", "hg")
35 35 def test_api_update_pull_request_title_or_description(
36 36 self, pr_util, silence_action_logger, no_notifications):
37 37 pull_request = pr_util.create_pull_request()
38 38
39 39 id_, params = build_data(
40 40 self.apikey, 'update_pull_request',
41 41 repoid=pull_request.target_repo.repo_name,
42 42 pullrequestid=pull_request.pull_request_id,
43 43 title='New TITLE OF A PR',
44 44 description='New DESC OF A PR',
45 45 )
46 46 response = api_call(self.app, params)
47 47
48 48 expected = {
49 49 "msg": "Updated pull request `{}`".format(
50 50 pull_request.pull_request_id),
51 51 "pull_request": response.json['result']['pull_request'],
52 52 "updated_commits": {"added": [], "common": [], "removed": []},
53 53 "updated_reviewers": {"added": [], "removed": []},
54 54 }
55 55
56 56 response_json = response.json['result']
57 57 assert response_json == expected
58 58 pr = response_json['pull_request']
59 59 assert pr['title'] == 'New TITLE OF A PR'
60 60 assert pr['description'] == 'New DESC OF A PR'
61 61
62 62 @pytest.mark.backends("git", "hg")
63 63 def test_api_try_update_closed_pull_request(
64 64 self, pr_util, silence_action_logger, no_notifications):
65 65 pull_request = pr_util.create_pull_request()
66 66 PullRequestModel().close_pull_request(
67 67 pull_request, TEST_USER_ADMIN_LOGIN)
68 68
69 69 id_, params = build_data(
70 70 self.apikey, 'update_pull_request',
71 71 repoid=pull_request.target_repo.repo_name,
72 72 pullrequestid=pull_request.pull_request_id)
73 73 response = api_call(self.app, params)
74 74
75 75 expected = 'pull request `{}` update failed, pull request ' \
76 76 'is closed'.format(pull_request.pull_request_id)
77 77
78 78 assert_error(id_, expected, response.body)
79 79
80 80 @pytest.mark.backends("git", "hg")
81 81 def test_api_update_update_commits(
82 82 self, pr_util, silence_action_logger, no_notifications):
83 83 commits = [
84 84 {'message': 'a'},
85 85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
86 86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
87 87 ]
88 88 pull_request = pr_util.create_pull_request(
89 89 commits=commits, target_head='a', source_head='b', revisions=['b'])
90 90 pr_util.update_source_repository(head='c')
91 91 repo = pull_request.source_repo.scm_instance()
92 92 commits = [x for x in repo.get_commits()]
93 93 print commits
94 94
95 95 added_commit_id = commits[-1].raw_id # c commit
96 96 common_commit_id = commits[1].raw_id # b commit is common ancestor
97 97 total_commits = [added_commit_id, common_commit_id]
98 98
99 99 id_, params = build_data(
100 100 self.apikey, 'update_pull_request',
101 101 repoid=pull_request.target_repo.repo_name,
102 102 pullrequestid=pull_request.pull_request_id,
103 103 update_commits=True
104 104 )
105 105 response = api_call(self.app, params)
106 106
107 107 expected = {
108 108 "msg": "Updated pull request `{}`".format(
109 109 pull_request.pull_request_id),
110 110 "pull_request": response.json['result']['pull_request'],
111 111 "updated_commits": {"added": [added_commit_id],
112 112 "common": [common_commit_id],
113 113 "total": total_commits,
114 114 "removed": []},
115 115 "updated_reviewers": {"added": [], "removed": []},
116 116 }
117 117
118 118 assert_ok(id_, expected, response.body)
119 119
120 120 @pytest.mark.backends("git", "hg")
121 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()]
125 new = [users.pop(0)]
126 removed = sorted(new)
127 added = sorted(users)
133 added = [b.username, c.username]
134 removed = [a.username]
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 139 id_, params = build_data(
132 140 self.apikey, 'update_pull_request',
133 141 repoid=pull_request.target_repo.repo_name,
134 142 pullrequestid=pull_request.pull_request_id,
135 reviewers=added)
143 reviewers=new_reviewers)
136 144 response = api_call(self.app, params)
137 145 expected = {
138 146 "msg": "Updated pull request `{}`".format(
139 147 pull_request.pull_request_id),
140 148 "pull_request": response.json['result']['pull_request'],
141 149 "updated_commits": {"added": [], "common": [], "removed": []},
142 150 "updated_reviewers": {"added": added, "removed": removed},
143 151 }
144 152
145 153 assert_ok(id_, expected, response.body)
146 154
147 155 @pytest.mark.backends("git", "hg")
148 156 def test_api_update_bad_user_in_reviewers(self, pr_util):
149 157 pull_request = pr_util.create_pull_request()
150 158
151 159 id_, params = build_data(
152 160 self.apikey, 'update_pull_request',
153 161 repoid=pull_request.target_repo.repo_name,
154 162 pullrequestid=pull_request.pull_request_id,
155 reviewers=['bad_name'])
163 reviewers=[{'username': 'bad_name'}])
156 164 response = api_call(self.app, params)
157 165
158 166 expected = 'user `bad_name` does not exist'
159 167
160 168 assert_error(id_, expected, response.body)
161 169
162 170 @pytest.mark.backends("git", "hg")
163 171 def test_api_update_repo_error(self, pr_util):
164 172 id_, params = build_data(
165 173 self.apikey, 'update_pull_request',
166 174 repoid='fake',
167 175 pullrequestid='fake',
168 reviewers=['bad_name'])
176 reviewers=[{'username': 'bad_name'}])
169 177 response = api_call(self.app, params)
170 178
171 179 expected = 'repository `fake` does not exist'
172 180
173 181 response_json = response.json['error']
174 182 assert response_json == expected
175 183
176 184 @pytest.mark.backends("git", "hg")
177 185 def test_api_update_pull_request_error(self, pr_util):
178 186 pull_request = pr_util.create_pull_request()
179 187
180 188 id_, params = build_data(
181 189 self.apikey, 'update_pull_request',
182 190 repoid=pull_request.target_repo.repo_name,
183 191 pullrequestid=999999,
184 reviewers=['bad_name'])
192 reviewers=[{'username': 'bad_name'}])
185 193 response = api_call(self.app, params)
186 194
187 195 expected = 'pull request `999999` does not exist'
188 196 assert_error(id_, expected, response.body)
189 197
190 198 @pytest.mark.backends("git", "hg")
191 199 def test_api_update_pull_request_no_perms_to_update(
192 200 self, user_util, pr_util):
193 201 user = user_util.create_user()
194 202 pull_request = pr_util.create_pull_request()
195 203
196 204 id_, params = build_data(
197 205 user.api_key, 'update_pull_request',
198 206 repoid=pull_request.target_repo.repo_name,
199 207 pullrequestid=pull_request.pull_request_id,)
200 208 response = api_call(self.app, params)
201 209
202 210 expected = ('pull request `%s` update failed, '
203 211 'no permission to update.') % pull_request.pull_request_id
204 212
205 213 assert_error(id_, expected, response.body)
@@ -1,730 +1,734 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 28 validate_repo_permissions, resolve_ref_or_error)
29 29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 30 from rhodecode.lib.base import vcs_operation_context
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 33 from rhodecode.model.comment import CommentsModel
34 34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 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 41 log = logging.getLogger(__name__)
39 42
40 43
41 44 @jsonrpc_method()
42 45 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 46 """
44 47 Get a pull request based on the given ID.
45 48
46 49 :param apiuser: This is filled automatically from the |authtoken|.
47 50 :type apiuser: AuthUser
48 51 :param repoid: Repository name or repository ID from where the pull
49 52 request was opened.
50 53 :type repoid: str or int
51 54 :param pullrequestid: ID of the requested pull request.
52 55 :type pullrequestid: int
53 56
54 57 Example output:
55 58
56 59 .. code-block:: bash
57 60
58 61 "id": <id_given_in_input>,
59 62 "result":
60 63 {
61 64 "pull_request_id": "<pull_request_id>",
62 65 "url": "<url>",
63 66 "title": "<title>",
64 67 "description": "<description>",
65 68 "status" : "<status>",
66 69 "created_on": "<date_time_created>",
67 70 "updated_on": "<date_time_updated>",
68 71 "commit_ids": [
69 72 ...
70 73 "<commit_id>",
71 74 "<commit_id>",
72 75 ...
73 76 ],
74 77 "review_status": "<review_status>",
75 78 "mergeable": {
76 79 "status": "<bool>",
77 80 "message": "<message>",
78 81 },
79 82 "source": {
80 83 "clone_url": "<clone_url>",
81 84 "repository": "<repository_name>",
82 85 "reference":
83 86 {
84 87 "name": "<name>",
85 88 "type": "<type>",
86 89 "commit_id": "<commit_id>",
87 90 }
88 91 },
89 92 "target": {
90 93 "clone_url": "<clone_url>",
91 94 "repository": "<repository_name>",
92 95 "reference":
93 96 {
94 97 "name": "<name>",
95 98 "type": "<type>",
96 99 "commit_id": "<commit_id>",
97 100 }
98 101 },
99 102 "merge": {
100 103 "clone_url": "<clone_url>",
101 104 "reference":
102 105 {
103 106 "name": "<name>",
104 107 "type": "<type>",
105 108 "commit_id": "<commit_id>",
106 109 }
107 110 },
108 111 "author": <user_obj>,
109 112 "reviewers": [
110 113 ...
111 114 {
112 115 "user": "<user_obj>",
113 116 "review_status": "<review_status>",
114 117 }
115 118 ...
116 119 ]
117 120 },
118 121 "error": null
119 122 """
120 123 get_repo_or_error(repoid)
121 124 pull_request = get_pull_request_or_error(pullrequestid)
122 125 if not PullRequestModel().check_user_read(
123 126 pull_request, apiuser, api=True):
124 127 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 128 data = pull_request.get_api_data()
126 129 return data
127 130
128 131
129 132 @jsonrpc_method()
130 133 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 134 """
132 135 Get all pull requests from the repository specified in `repoid`.
133 136
134 137 :param apiuser: This is filled automatically from the |authtoken|.
135 138 :type apiuser: AuthUser
136 139 :param repoid: Repository name or repository ID.
137 140 :type repoid: str or int
138 141 :param status: Only return pull requests with the specified status.
139 142 Valid options are.
140 143 * ``new`` (default)
141 144 * ``open``
142 145 * ``closed``
143 146 :type status: str
144 147
145 148 Example output:
146 149
147 150 .. code-block:: bash
148 151
149 152 "id": <id_given_in_input>,
150 153 "result":
151 154 [
152 155 ...
153 156 {
154 157 "pull_request_id": "<pull_request_id>",
155 158 "url": "<url>",
156 159 "title" : "<title>",
157 160 "description": "<description>",
158 161 "status": "<status>",
159 162 "created_on": "<date_time_created>",
160 163 "updated_on": "<date_time_updated>",
161 164 "commit_ids": [
162 165 ...
163 166 "<commit_id>",
164 167 "<commit_id>",
165 168 ...
166 169 ],
167 170 "review_status": "<review_status>",
168 171 "mergeable": {
169 172 "status": "<bool>",
170 173 "message: "<message>",
171 174 },
172 175 "source": {
173 176 "clone_url": "<clone_url>",
174 177 "reference":
175 178 {
176 179 "name": "<name>",
177 180 "type": "<type>",
178 181 "commit_id": "<commit_id>",
179 182 }
180 183 },
181 184 "target": {
182 185 "clone_url": "<clone_url>",
183 186 "reference":
184 187 {
185 188 "name": "<name>",
186 189 "type": "<type>",
187 190 "commit_id": "<commit_id>",
188 191 }
189 192 },
190 193 "merge": {
191 194 "clone_url": "<clone_url>",
192 195 "reference":
193 196 {
194 197 "name": "<name>",
195 198 "type": "<type>",
196 199 "commit_id": "<commit_id>",
197 200 }
198 201 },
199 202 "author": <user_obj>,
200 203 "reviewers": [
201 204 ...
202 205 {
203 206 "user": "<user_obj>",
204 207 "review_status": "<review_status>",
205 208 }
206 209 ...
207 210 ]
208 211 }
209 212 ...
210 213 ],
211 214 "error": null
212 215
213 216 """
214 217 repo = get_repo_or_error(repoid)
215 218 if not has_superadmin_permission(apiuser):
216 219 _perms = (
217 220 'repository.admin', 'repository.write', 'repository.read',)
218 221 validate_repo_permissions(apiuser, repoid, repo, _perms)
219 222
220 223 status = Optional.extract(status)
221 224 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 225 data = [pr.get_api_data() for pr in pull_requests]
223 226 return data
224 227
225 228
226 229 @jsonrpc_method()
227 230 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 231 userid=Optional(OAttr('apiuser'))):
229 232 """
230 233 Merge the pull request specified by `pullrequestid` into its target
231 234 repository.
232 235
233 236 :param apiuser: This is filled automatically from the |authtoken|.
234 237 :type apiuser: AuthUser
235 238 :param repoid: The Repository name or repository ID of the
236 239 target repository to which the |pr| is to be merged.
237 240 :type repoid: str or int
238 241 :param pullrequestid: ID of the pull request which shall be merged.
239 242 :type pullrequestid: int
240 243 :param userid: Merge the pull request as this user.
241 244 :type userid: Optional(str or int)
242 245
243 246 Example output:
244 247
245 248 .. code-block:: bash
246 249
247 250 "id": <id_given_in_input>,
248 251 "result": {
249 252 "executed": "<bool>",
250 253 "failure_reason": "<int>",
251 254 "merge_commit_id": "<merge_commit_id>",
252 255 "possible": "<bool>",
253 256 "merge_ref": {
254 257 "commit_id": "<commit_id>",
255 258 "type": "<type>",
256 259 "name": "<name>"
257 260 }
258 261 },
259 262 "error": null
260 263 """
261 264 repo = get_repo_or_error(repoid)
262 265 if not isinstance(userid, Optional):
263 266 if (has_superadmin_permission(apiuser) or
264 267 HasRepoPermissionAnyApi('repository.admin')(
265 268 user=apiuser, repo_name=repo.repo_name)):
266 269 apiuser = get_user_or_error(userid)
267 270 else:
268 271 raise JSONRPCError('userid is not the same as your user')
269 272
270 273 pull_request = get_pull_request_or_error(pullrequestid)
271 274
272 275 check = MergeCheck.validate(pull_request, user=apiuser)
273 276 merge_possible = not check.failed
274 277
275 278 if not merge_possible:
276 279 error_messages = []
277 280 for err_type, error_msg in check.errors:
278 281 error_msg = request.translate(error_msg)
279 282 error_messages.append(error_msg)
280 283
281 284 reasons = ','.join(error_messages)
282 285 raise JSONRPCError(
283 286 'merge not possible for following reasons: {}'.format(reasons))
284 287
285 288 target_repo = pull_request.target_repo
286 289 extras = vcs_operation_context(
287 290 request.environ, repo_name=target_repo.repo_name,
288 291 username=apiuser.username, action='push',
289 292 scm=target_repo.repo_type)
290 293 merge_response = PullRequestModel().merge(
291 294 pull_request, apiuser, extras=extras)
292 295 if merge_response.executed:
293 296 PullRequestModel().close_pull_request(
294 297 pull_request.pull_request_id, apiuser)
295 298
296 299 Session().commit()
297 300
298 301 # In previous versions the merge response directly contained the merge
299 302 # commit id. It is now contained in the merge reference object. To be
300 303 # backwards compatible we have to extract it again.
301 304 merge_response = merge_response._asdict()
302 305 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
303 306
304 307 return merge_response
305 308
306 309
307 310 @jsonrpc_method()
308 311 def close_pull_request(request, apiuser, repoid, pullrequestid,
309 312 userid=Optional(OAttr('apiuser'))):
310 313 """
311 314 Close the pull request specified by `pullrequestid`.
312 315
313 316 :param apiuser: This is filled automatically from the |authtoken|.
314 317 :type apiuser: AuthUser
315 318 :param repoid: Repository name or repository ID to which the pull
316 319 request belongs.
317 320 :type repoid: str or int
318 321 :param pullrequestid: ID of the pull request to be closed.
319 322 :type pullrequestid: int
320 323 :param userid: Close the pull request as this user.
321 324 :type userid: Optional(str or int)
322 325
323 326 Example output:
324 327
325 328 .. code-block:: bash
326 329
327 330 "id": <id_given_in_input>,
328 331 "result": {
329 332 "pull_request_id": "<int>",
330 333 "closed": "<bool>"
331 334 },
332 335 "error": null
333 336
334 337 """
335 338 repo = get_repo_or_error(repoid)
336 339 if not isinstance(userid, Optional):
337 340 if (has_superadmin_permission(apiuser) or
338 341 HasRepoPermissionAnyApi('repository.admin')(
339 342 user=apiuser, repo_name=repo.repo_name)):
340 343 apiuser = get_user_or_error(userid)
341 344 else:
342 345 raise JSONRPCError('userid is not the same as your user')
343 346
344 347 pull_request = get_pull_request_or_error(pullrequestid)
345 348 if not PullRequestModel().check_user_update(
346 349 pull_request, apiuser, api=True):
347 350 raise JSONRPCError(
348 351 'pull request `%s` close failed, no permission to close.' % (
349 352 pullrequestid,))
350 353 if pull_request.is_closed():
351 354 raise JSONRPCError(
352 355 'pull request `%s` is already closed' % (pullrequestid,))
353 356
354 357 PullRequestModel().close_pull_request(
355 358 pull_request.pull_request_id, apiuser)
356 359 Session().commit()
357 360 data = {
358 361 'pull_request_id': pull_request.pull_request_id,
359 362 'closed': True,
360 363 }
361 364 return data
362 365
363 366
364 367 @jsonrpc_method()
365 368 def comment_pull_request(
366 369 request, apiuser, repoid, pullrequestid, message=Optional(None),
367 370 commit_id=Optional(None), status=Optional(None),
368 371 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
369 372 resolves_comment_id=Optional(None),
370 373 userid=Optional(OAttr('apiuser'))):
371 374 """
372 375 Comment on the pull request specified with the `pullrequestid`,
373 376 in the |repo| specified by the `repoid`, and optionally change the
374 377 review status.
375 378
376 379 :param apiuser: This is filled automatically from the |authtoken|.
377 380 :type apiuser: AuthUser
378 381 :param repoid: The repository name or repository ID.
379 382 :type repoid: str or int
380 383 :param pullrequestid: The pull request ID.
381 384 :type pullrequestid: int
382 385 :param commit_id: Specify the commit_id for which to set a comment. If
383 386 given commit_id is different than latest in the PR status
384 387 change won't be performed.
385 388 :type commit_id: str
386 389 :param message: The text content of the comment.
387 390 :type message: str
388 391 :param status: (**Optional**) Set the approval status of the pull
389 392 request. One of: 'not_reviewed', 'approved', 'rejected',
390 393 'under_review'
391 394 :type status: str
392 395 :param comment_type: Comment type, one of: 'note', 'todo'
393 396 :type comment_type: Optional(str), default: 'note'
394 397 :param userid: Comment on the pull request as this user
395 398 :type userid: Optional(str or int)
396 399
397 400 Example output:
398 401
399 402 .. code-block:: bash
400 403
401 404 id : <id_given_in_input>
402 405 result : {
403 406 "pull_request_id": "<Integer>",
404 407 "comment_id": "<Integer>",
405 408 "status": {"given": <given_status>,
406 409 "was_changed": <bool status_was_actually_changed> },
407 410 },
408 411 error : null
409 412 """
410 413 repo = get_repo_or_error(repoid)
411 414 if not isinstance(userid, Optional):
412 415 if (has_superadmin_permission(apiuser) or
413 416 HasRepoPermissionAnyApi('repository.admin')(
414 417 user=apiuser, repo_name=repo.repo_name)):
415 418 apiuser = get_user_or_error(userid)
416 419 else:
417 420 raise JSONRPCError('userid is not the same as your user')
418 421
419 422 pull_request = get_pull_request_or_error(pullrequestid)
420 423 if not PullRequestModel().check_user_read(
421 424 pull_request, apiuser, api=True):
422 425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
423 426 message = Optional.extract(message)
424 427 status = Optional.extract(status)
425 428 commit_id = Optional.extract(commit_id)
426 429 comment_type = Optional.extract(comment_type)
427 430 resolves_comment_id = Optional.extract(resolves_comment_id)
428 431
429 432 if not message and not status:
430 433 raise JSONRPCError(
431 434 'Both message and status parameters are missing. '
432 435 'At least one is required.')
433 436
434 437 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
435 438 status is not None):
436 439 raise JSONRPCError('Unknown comment status: `%s`' % status)
437 440
438 441 if commit_id and commit_id not in pull_request.revisions:
439 442 raise JSONRPCError(
440 443 'Invalid commit_id `%s` for this pull request.' % commit_id)
441 444
442 445 allowed_to_change_status = PullRequestModel().check_user_change_status(
443 446 pull_request, apiuser)
444 447
445 448 # if commit_id is passed re-validated if user is allowed to change status
446 449 # based on latest commit_id from the PR
447 450 if commit_id:
448 451 commit_idx = pull_request.revisions.index(commit_id)
449 452 if commit_idx != 0:
450 453 allowed_to_change_status = False
451 454
452 455 if resolves_comment_id:
453 456 comment = ChangesetComment.get(resolves_comment_id)
454 457 if not comment:
455 458 raise JSONRPCError(
456 459 'Invalid resolves_comment_id `%s` for this pull request.'
457 460 % resolves_comment_id)
458 461 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
459 462 raise JSONRPCError(
460 463 'Comment `%s` is wrong type for setting status to resolved.'
461 464 % resolves_comment_id)
462 465
463 466 text = message
464 467 status_label = ChangesetStatus.get_status_lbl(status)
465 468 if status and allowed_to_change_status:
466 469 st_message = ('Status change %(transition_icon)s %(status)s'
467 470 % {'transition_icon': '>', 'status': status_label})
468 471 text = message or st_message
469 472
470 473 rc_config = SettingsModel().get_all_settings()
471 474 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
472 475
473 476 status_change = status and allowed_to_change_status
474 477 comment = CommentsModel().create(
475 478 text=text,
476 479 repo=pull_request.target_repo.repo_id,
477 480 user=apiuser.user_id,
478 481 pull_request=pull_request.pull_request_id,
479 482 f_path=None,
480 483 line_no=None,
481 484 status_change=(status_label if status_change else None),
482 485 status_change_type=(status if status_change else None),
483 486 closing_pr=False,
484 487 renderer=renderer,
485 488 comment_type=comment_type,
486 489 resolves_comment_id=resolves_comment_id
487 490 )
488 491
489 492 if allowed_to_change_status and status:
490 493 ChangesetStatusModel().set_status(
491 494 pull_request.target_repo.repo_id,
492 495 status,
493 496 apiuser.user_id,
494 497 comment,
495 498 pull_request=pull_request.pull_request_id
496 499 )
497 500 Session().flush()
498 501
499 502 Session().commit()
500 503 data = {
501 504 'pull_request_id': pull_request.pull_request_id,
502 505 'comment_id': comment.comment_id if comment else None,
503 506 'status': {'given': status, 'was_changed': status_change},
504 507 }
505 508 return data
506 509
507 510
508 511 @jsonrpc_method()
509 512 def create_pull_request(
510 513 request, apiuser, source_repo, target_repo, source_ref, target_ref,
511 514 title, description=Optional(''), reviewers=Optional(None)):
512 515 """
513 516 Creates a new pull request.
514 517
515 518 Accepts refs in the following formats:
516 519
517 520 * branch:<branch_name>:<sha>
518 521 * branch:<branch_name>
519 522 * bookmark:<bookmark_name>:<sha> (Mercurial only)
520 523 * bookmark:<bookmark_name> (Mercurial only)
521 524
522 525 :param apiuser: This is filled automatically from the |authtoken|.
523 526 :type apiuser: AuthUser
524 527 :param source_repo: Set the source repository name.
525 528 :type source_repo: str
526 529 :param target_repo: Set the target repository name.
527 530 :type target_repo: str
528 531 :param source_ref: Set the source ref name.
529 532 :type source_ref: str
530 533 :param target_ref: Set the target ref name.
531 534 :type target_ref: str
532 535 :param title: Set the pull request title.
533 536 :type title: str
534 537 :param description: Set the pull request description.
535 538 :type description: Optional(str)
536 539 :param reviewers: Set the new pull request reviewers list.
537 540 :type reviewers: Optional(list)
538 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 546 source = get_repo_or_error(source_repo)
544 547 target = get_repo_or_error(target_repo)
545 548 if not has_superadmin_permission(apiuser):
546 549 _perms = ('repository.admin', 'repository.write', 'repository.read',)
547 550 validate_repo_permissions(apiuser, source_repo, source, _perms)
548 551
549 552 full_source_ref = resolve_ref_or_error(source_ref, source)
550 553 full_target_ref = resolve_ref_or_error(target_ref, target)
551 554 source_commit = get_commit_or_error(full_source_ref, source)
552 555 target_commit = get_commit_or_error(full_target_ref, target)
553 556 source_scm = source.scm_instance()
554 557 target_scm = target.scm_instance()
555 558
556 559 commit_ranges = target_scm.compare(
557 560 target_commit.raw_id, source_commit.raw_id, source_scm,
558 561 merge=True, pre_load=[])
559 562
560 563 ancestor = target_scm.get_common_ancestor(
561 564 target_commit.raw_id, source_commit.raw_id, source_scm)
562 565
563 566 if not commit_ranges:
564 567 raise JSONRPCError('no commits found')
565 568
566 569 if not ancestor:
567 570 raise JSONRPCError('no common ancestor found')
568 571
569 572 reviewer_objects = Optional.extract(reviewers) or []
570 if not isinstance(reviewer_objects, list):
571 raise JSONRPCError('reviewers should be specified as a list')
573 if reviewer_objects:
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 581 for reviewer_object in reviewer_objects:
575 reviewer_reasons = []
576 if isinstance(reviewer_object, (basestring, int)):
577 reviewer_username = reviewer_object
578 else:
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))
582 user = get_user_or_error(reviewer_object['username'])
583 reasons = reviewer_object['reasons']
584 mandatory = reviewer_object['mandatory']
585 reviewers.append((user.user_id, reasons, mandatory))
584 586
585 587 pull_request_model = PullRequestModel()
586 588 pull_request = pull_request_model.create(
587 589 created_by=apiuser.user_id,
588 590 source_repo=source_repo,
589 591 source_ref=full_source_ref,
590 592 target_repo=target_repo,
591 593 target_ref=full_target_ref,
592 594 revisions=reversed(
593 595 [commit.raw_id for commit in reversed(commit_ranges)]),
594 reviewers=reviewers_reasons,
596 reviewers=reviewers,
595 597 title=title,
596 598 description=Optional.extract(description)
597 599 )
598 600
599 601 Session().commit()
600 602 data = {
601 603 'msg': 'Created new pull request `{}`'.format(title),
602 604 'pull_request_id': pull_request.pull_request_id,
603 605 }
604 606 return data
605 607
606 608
607 609 @jsonrpc_method()
608 610 def update_pull_request(
609 611 request, apiuser, repoid, pullrequestid, title=Optional(''),
610 612 description=Optional(''), reviewers=Optional(None),
611 613 update_commits=Optional(None), close_pull_request=Optional(None)):
612 614 """
613 615 Updates a pull request.
614 616
615 617 :param apiuser: This is filled automatically from the |authtoken|.
616 618 :type apiuser: AuthUser
617 619 :param repoid: The repository name or repository ID.
618 620 :type repoid: str or int
619 621 :param pullrequestid: The pull request ID.
620 622 :type pullrequestid: int
621 623 :param title: Set the pull request title.
622 624 :type title: str
623 625 :param description: Update pull request description.
624 626 :type description: Optional(str)
625 627 :param reviewers: Update pull request reviewers list with new value.
626 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 633 :param update_commits: Trigger update of commits for this pull request
628 634 :type: update_commits: Optional(bool)
629 635 :param close_pull_request: Close this pull request with rejected state
630 636 :type: close_pull_request: Optional(bool)
631 637
632 638 Example output:
633 639
634 640 .. code-block:: bash
635 641
636 642 id : <id_given_in_input>
637 643 result : {
638 644 "msg": "Updated pull request `63`",
639 645 "pull_request": <pull_request_object>,
640 646 "updated_reviewers": {
641 647 "added": [
642 648 "username"
643 649 ],
644 650 "removed": []
645 651 },
646 652 "updated_commits": {
647 653 "added": [
648 654 "<sha1_hash>"
649 655 ],
650 656 "common": [
651 657 "<sha1_hash>",
652 658 "<sha1_hash>",
653 659 ],
654 660 "removed": []
655 661 }
656 662 }
657 663 error : null
658 664 """
659 665
660 666 repo = get_repo_or_error(repoid)
661 667 pull_request = get_pull_request_or_error(pullrequestid)
662 668 if not PullRequestModel().check_user_update(
663 669 pull_request, apiuser, api=True):
664 670 raise JSONRPCError(
665 671 'pull request `%s` update failed, no permission to update.' % (
666 672 pullrequestid,))
667 673 if pull_request.is_closed():
668 674 raise JSONRPCError(
669 675 'pull request `%s` update failed, pull request is closed' % (
670 676 pullrequestid,))
671 677
678
672 679 reviewer_objects = Optional.extract(reviewers) or []
673 if not isinstance(reviewer_objects, list):
674 raise JSONRPCError('reviewers should be specified as a list')
680 if reviewer_objects:
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 = []
677 reviewer_ids = set()
687 reviewers = []
678 688 for reviewer_object in reviewer_objects:
679 reviewer_reasons = []
680 if isinstance(reviewer_object, (int, basestring)):
681 reviewer_username = reviewer_object
682 else:
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 user = get_user_or_error(reviewer_object['username'])
690 reasons = reviewer_object['reasons']
691 mandatory = reviewer_object['mandatory']
692 reviewers.append((user.user_id, reasons, mandatory))
689 693
690 694 title = Optional.extract(title)
691 695 description = Optional.extract(description)
692 696 if title or description:
693 697 PullRequestModel().edit(
694 698 pull_request, title or pull_request.title,
695 699 description or pull_request.description)
696 700 Session().commit()
697 701
698 702 commit_changes = {"added": [], "common": [], "removed": []}
699 703 if str2bool(Optional.extract(update_commits)):
700 704 if PullRequestModel().has_valid_update_type(pull_request):
701 705 update_response = PullRequestModel().update_commits(
702 706 pull_request)
703 707 commit_changes = update_response.changes or commit_changes
704 708 Session().commit()
705 709
706 710 reviewers_changes = {"added": [], "removed": []}
707 if reviewer_ids:
711 if reviewers:
708 712 added_reviewers, removed_reviewers = \
709 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
713 PullRequestModel().update_reviewers(pull_request, reviewers)
710 714
711 715 reviewers_changes['added'] = sorted(
712 716 [get_user_or_error(n).username for n in added_reviewers])
713 717 reviewers_changes['removed'] = sorted(
714 718 [get_user_or_error(n).username for n in removed_reviewers])
715 719 Session().commit()
716 720
717 721 if str2bool(Optional.extract(close_pull_request)):
718 722 PullRequestModel().close_pull_request_with_comment(
719 723 pull_request, apiuser, repo)
720 724 Session().commit()
721 725
722 726 data = {
723 727 'msg': 'Updated pull request `{}`'.format(
724 728 pull_request.pull_request_id),
725 729 'pull_request': pull_request.get_api_data(),
726 730 'updated_commits': commit_changes,
727 731 'updated_reviewers': reviewers_changes
728 732 }
729 733
730 734 return data
@@ -1,301 +1,308 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 from pylons import tmpl_context as c
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils import PartialRenderer
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.model import repo
32 32 from rhodecode.model.db import User
33 33 from rhodecode.model.scm import ScmModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41
42 42 def get_format_ref_id(repo):
43 43 """Returns a `repo` specific reference formatter function"""
44 44 if h.is_svn(repo):
45 45 return _format_ref_id_svn
46 46 else:
47 47 return _format_ref_id
48 48
49 49
50 50 def _format_ref_id(name, raw_id):
51 51 """Default formatting of a given reference `name`"""
52 52 return name
53 53
54 54
55 55 def _format_ref_id_svn(name, raw_id):
56 56 """Special way of formatting a reference for Subversion including path"""
57 57 return '%s@%s' % (name, raw_id)
58 58
59 59
60 60 class TemplateArgs(StrictAttributeDict):
61 61 pass
62 62
63 63
64 64 class BaseAppView(object):
65 65
66 66 def __init__(self, context, request):
67 67 self.request = request
68 68 self.context = context
69 69 self.session = request.session
70 70 self._rhodecode_user = request.user # auth user
71 71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 72 self._maybe_needs_password_change(
73 73 request.matched_route.name, self._rhodecode_db_user)
74 74
75 75 def _maybe_needs_password_change(self, view_name, user_obj):
76 76 log.debug('Checking if user %s needs password change on view %s',
77 77 user_obj, view_name)
78 78 skip_user_views = [
79 79 'logout', 'login',
80 80 'my_account_password', 'my_account_password_update'
81 81 ]
82 82
83 83 if not user_obj:
84 84 return
85 85
86 86 if user_obj.username == User.DEFAULT_USER:
87 87 return
88 88
89 89 now = time.time()
90 90 should_change = user_obj.user_data.get('force_password_change')
91 91 change_after = safe_int(should_change) or 0
92 92 if should_change and now > change_after:
93 93 log.debug('User %s requires password change', user_obj)
94 94 h.flash('You are required to change your password', 'warning',
95 95 ignore_duplicate=True)
96 96
97 97 if view_name not in skip_user_views:
98 98 raise HTTPFound(
99 99 self.request.route_path('my_account_password'))
100 100
101 101 def _get_local_tmpl_context(self):
102 102 c = TemplateArgs()
103 103 c.auth_user = self.request.user
104 104 return c
105 105
106 106 def _register_global_c(self, tmpl_args):
107 107 """
108 108 Registers attributes to pylons global `c`
109 109 """
110 110 # TODO(marcink): remove once pyramid migration is finished
111 111 for k, v in tmpl_args.items():
112 112 setattr(c, k, v)
113 113
114 114 def _get_template_context(self, tmpl_args):
115 115 self._register_global_c(tmpl_args)
116 116
117 117 local_tmpl_args = {
118 118 'defaults': {},
119 119 'errors': {},
120 120 }
121 121 local_tmpl_args.update(tmpl_args)
122 122 return local_tmpl_args
123 123
124 124 def load_default_context(self):
125 125 """
126 126 example:
127 127
128 128 def load_default_context(self):
129 129 c = self._get_local_tmpl_context()
130 130 c.custom_var = 'foobar'
131 131 self._register_global_c(c)
132 132 return c
133 133 """
134 134 raise NotImplementedError('Needs implementation in view class')
135 135
136 136
137 137 class RepoAppView(BaseAppView):
138 138
139 139 def __init__(self, context, request):
140 140 super(RepoAppView, self).__init__(context, request)
141 141 self.db_repo = request.db_repo
142 142 self.db_repo_name = self.db_repo.repo_name
143 143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144 144
145 145 def _handle_missing_requirements(self, error):
146 146 log.error(
147 147 'Requirements are missing for repository %s: %s',
148 148 self.db_repo_name, error.message)
149 149
150 150 def _get_local_tmpl_context(self):
151 151 c = super(RepoAppView, self)._get_local_tmpl_context()
152 152 # register common vars for this type of view
153 153 c.rhodecode_db_repo = self.db_repo
154 154 c.repo_name = self.db_repo_name
155 155 c.repository_pull_requests = self.db_repo_pull_requests
156 156
157 157 c.repository_requirements_missing = False
158 158 try:
159 159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 160 except RepositoryRequirementError as e:
161 161 c.repository_requirements_missing = True
162 162 self._handle_missing_requirements(e)
163 163
164 164 return c
165 165
166 166
167 167 class DataGridAppView(object):
168 168 """
169 169 Common class to have re-usable grid rendering components
170 170 """
171 171
172 172 def _extract_ordering(self, request, column_map=None):
173 173 column_map = column_map or {}
174 174 column_index = safe_int(request.GET.get('order[0][column]'))
175 175 order_dir = request.GET.get(
176 176 'order[0][dir]', 'desc')
177 177 order_by = request.GET.get(
178 178 'columns[%s][data][sort]' % column_index, 'name_raw')
179 179
180 180 # translate datatable to DB columns
181 181 order_by = column_map.get(order_by) or order_by
182 182
183 183 search_q = request.GET.get('search[value]')
184 184 return search_q, order_by, order_dir
185 185
186 186 def _extract_chunk(self, request):
187 187 start = safe_int(request.GET.get('start'), 0)
188 188 length = safe_int(request.GET.get('length'), 25)
189 189 draw = safe_int(request.GET.get('draw'))
190 190 return draw, start, length
191 191
192 192
193 193 class BaseReferencesView(RepoAppView):
194 194 """
195 195 Base for reference view for branches, tags and bookmarks.
196 196 """
197 197 def load_default_context(self):
198 198 c = self._get_local_tmpl_context()
199 199
200 200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 201 c.repo_info = self.db_repo
202 202
203 203 self._register_global_c(c)
204 204 return c
205 205
206 206 def load_refs_context(self, ref_items, partials_template):
207 207 _render = PartialRenderer(partials_template)
208 208 _data = []
209 209 pre_load = ["author", "date", "message"]
210 210
211 211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213 213
214 214 for ref_name, commit_id in ref_items:
215 215 commit = self.rhodecode_vcs_repo.get_commit(
216 216 commit_id=commit_id, pre_load=pre_load)
217 217
218 218 # TODO: johbo: Unify generation of reference links
219 219 use_commit_id = '/' in ref_name or is_svn
220 220 files_url = h.url(
221 221 'files_home',
222 222 repo_name=c.repo_name,
223 223 f_path=ref_name if is_svn else '',
224 224 revision=commit_id if use_commit_id else ref_name,
225 225 at=ref_name)
226 226
227 227 _data.append({
228 228 "name": _render('name', ref_name, files_url),
229 229 "name_raw": ref_name,
230 230 "date": _render('date', commit.date),
231 231 "date_raw": datetime_to_time(commit.date),
232 232 "author": _render('author', commit.author),
233 233 "commit": _render(
234 234 'commit', commit.message, commit.raw_id, commit.idx),
235 235 "commit_raw": commit.idx,
236 236 "compare": _render(
237 237 'compare', format_ref_id(ref_name, commit.raw_id)),
238 238 })
239 239 c.has_references = bool(_data)
240 240 c.data = json.dumps(_data)
241 241
242 242
243 243 class RepoRoutePredicate(object):
244 244 def __init__(self, val, config):
245 245 self.val = val
246 246
247 247 def text(self):
248 248 return 'repo_route = %s' % self.val
249 249
250 250 phash = text
251 251
252 252 def __call__(self, info, request):
253 253 repo_name = info['match']['repo_name']
254 254 repo_model = repo.RepoModel()
255 255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
256 256 # if we match quickly from database, short circuit the operation,
257 257 # and validate repo based on the type.
258 258 if by_name_match:
259 259 # register this as request object we can re-use later
260 260 request.db_repo = by_name_match
261 261 return True
262 262
263 263 by_id_match = repo_model.get_repo_by_id(repo_name)
264 264 if by_id_match:
265 265 request.db_repo = by_id_match
266 266 return True
267 267
268 268 return False
269 269
270 270
271 271 class RepoTypeRoutePredicate(object):
272 272 def __init__(self, val, config):
273 273 self.val = val or ['hg', 'git', 'svn']
274 274
275 275 def text(self):
276 276 return 'repo_accepted_type = %s' % self.val
277 277
278 278 phash = text
279 279
280 280 def __call__(self, info, request):
281 281
282 282 rhodecode_db_repo = request.db_repo
283 283
284 284 log.debug(
285 285 '%s checking repo type for %s in %s',
286 286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287 287
288 288 if rhodecode_db_repo.repo_type in self.val:
289 289 return True
290 290 else:
291 291 log.warning('Current view is not supported for repo type:%s',
292 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 300 return False
294 301
295 302
296 303
297 304 def includeme(config):
298 305 config.add_route_predicate(
299 306 'repo_route', RepoRoutePredicate)
300 307 config.add_route_predicate(
301 308 'repo_accepted_types', RepoTypeRoutePredicate) No newline at end of file
@@ -1,69 +1,64 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.view import view_config
24 24
25 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 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class RepoReviewRulesView(RepoAppView):
33 33 def load_default_context(self):
34 34 c = self._get_local_tmpl_context()
35 35
36 36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
37 37 c.repo_info = self.db_repo
38 38
39 39 self._register_global_c(c)
40 40 return c
41 41
42 42 @LoginRequired()
43 43 @HasRepoPermissionAnyDecorator('repository.admin')
44 44 @view_config(
45 45 route_name='repo_reviewers', request_method='GET',
46 46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
47 47 def repo_review_rules(self):
48 48 c = self.load_default_context()
49 49 c.active = 'reviewers'
50 50
51 51 return self._get_template_context(c)
52 52
53 53 @LoginRequired()
54 54 @HasRepoPermissionAnyDecorator(
55 55 'repository.read', 'repository.write', 'repository.admin')
56 56 @view_config(
57 57 route_name='repo_default_reviewers_data', request_method='GET',
58 58 renderer='json_ext')
59 59 def repo_default_reviewers_data(self):
60 reasons = ['Default reviewer', 'Repository owner']
61 default = utils.reviewer_as_json(
62 user=self.db_repo.user, reasons=reasons, mandatory=False)
60 review_data = get_default_reviewers_data(
61 self.db_repo.user, None, None, None, None)
62 return review_data
63 63
64 return {
65 'api_ver': 'v1', # define version for later possible schema upgrade
66 'reviewers': [default],
67 'rules': {},
68 'rules_data': {},
69 }
64
@@ -1,978 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(PullrequestsController, self).__before__()
72 72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74 74
75 75 @LoginRequired()
76 76 @NotAnonymous()
77 77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 78 'repository.admin')
79 79 @HasAcceptedRepoType('git', 'hg')
80 80 def index(self):
81 81 source_repo = c.rhodecode_db_repo
82 82
83 83 try:
84 84 source_repo.scm_instance().get_commit()
85 85 except EmptyRepositoryError:
86 86 h.flash(h.literal(_('There are no commits yet')),
87 87 category='warning')
88 88 redirect(url('summary_home', repo_name=source_repo.repo_name))
89 89
90 90 commit_id = request.GET.get('commit')
91 91 branch_ref = request.GET.get('branch')
92 92 bookmark_ref = request.GET.get('bookmark')
93 93
94 94 try:
95 95 source_repo_data = PullRequestModel().generate_repo_data(
96 96 source_repo, commit_id=commit_id,
97 97 branch=branch_ref, bookmark=bookmark_ref)
98 98 except CommitDoesNotExistError as e:
99 99 log.exception(e)
100 100 h.flash(_('Commit does not exist'), 'error')
101 101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102 102
103 103 default_target_repo = source_repo
104 104
105 105 if source_repo.parent:
106 106 parent_vcs_obj = source_repo.parent.scm_instance()
107 107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 108 # change default if we have a parent repo
109 109 default_target_repo = source_repo.parent
110 110
111 111 target_repo_data = PullRequestModel().generate_repo_data(
112 112 default_target_repo)
113 113
114 114 selected_source_ref = source_repo_data['refs']['selected_ref']
115 115
116 116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 118 source=source_repo.repo_name,
119 119 source_ref=title_source_ref,
120 120 target=default_target_repo.repo_name
121 121 )
122 122
123 123 c.default_repo_data = {
124 124 'source_repo_name': source_repo.repo_name,
125 125 'source_refs_json': json.dumps(source_repo_data),
126 126 'target_repo_name': default_target_repo.repo_name,
127 127 'target_refs_json': json.dumps(target_repo_data),
128 128 }
129 129 c.default_source_ref = selected_source_ref
130 130
131 131 return render('/pullrequests/pullrequest.mako')
132 132
133 133 @LoginRequired()
134 134 @NotAnonymous()
135 135 @XHRRequired()
136 136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 137 'repository.admin')
138 138 @jsonify
139 139 def get_repo_refs(self, repo_name, target_repo_name):
140 140 repo = Repository.get_by_repo_name(target_repo_name)
141 141 if not repo:
142 142 raise HTTPNotFound
143 143 return PullRequestModel().generate_repo_data(repo)
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @XHRRequired()
148 148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 149 'repository.admin')
150 150 @jsonify
151 151 def get_repo_destinations(self, repo_name):
152 152 repo = Repository.get_by_repo_name(repo_name)
153 153 if not repo:
154 154 raise HTTPNotFound
155 155 filter_query = request.GET.get('query')
156 156
157 157 query = Repository.query() \
158 158 .order_by(func.length(Repository.repo_name)) \
159 159 .filter(or_(
160 160 Repository.repo_name == repo.repo_name,
161 161 Repository.fork_id == repo.repo_id))
162 162
163 163 if filter_query:
164 164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 165 query = query.filter(
166 166 Repository.repo_name.ilike(ilike_expression))
167 167
168 168 add_parent = False
169 169 if repo.parent:
170 170 if filter_query in repo.parent.repo_name:
171 171 parent_vcs_obj = repo.parent.scm_instance()
172 172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 173 add_parent = True
174 174
175 175 limit = 20 - 1 if add_parent else 20
176 176 all_repos = query.limit(limit).all()
177 177 if add_parent:
178 178 all_repos += [repo.parent]
179 179
180 180 repos = []
181 181 for obj in self.scm_model.get_repos(all_repos):
182 182 repos.append({
183 183 'id': obj['name'],
184 184 'text': obj['name'],
185 185 'type': 'repo',
186 186 'obj': obj['dbrepo']
187 187 })
188 188
189 189 data = {
190 190 'more': False,
191 191 'results': [{
192 192 'text': _('Repositories'),
193 193 'children': repos
194 194 }] if repos else []
195 195 }
196 196 return data
197 197
198 198 @LoginRequired()
199 199 @NotAnonymous()
200 200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 201 'repository.admin')
202 202 @HasAcceptedRepoType('git', 'hg')
203 203 @auth.CSRFRequired()
204 204 def create(self, repo_name):
205 205 repo = Repository.get_by_repo_name(repo_name)
206 206 if not repo:
207 207 raise HTTPNotFound
208 208
209 209 controls = peppercorn.parse(request.POST.items())
210 210
211 211 try:
212 212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 213 except formencode.Invalid as errors:
214 214 if errors.error_dict.get('revisions'):
215 215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 216 elif errors.error_dict.get('pullrequest_title'):
217 217 msg = _('Pull request requires a title with min. 3 chars')
218 218 else:
219 219 msg = _('Error creating pull request: {}').format(errors)
220 220 log.exception(msg)
221 221 h.flash(msg, 'error')
222 222
223 223 # would rather just go back to form ...
224 224 return redirect(url('pullrequest_home', repo_name=repo_name))
225 225
226 226 source_repo = _form['source_repo']
227 227 source_ref = _form['source_ref']
228 228 target_repo = _form['target_repo']
229 229 target_ref = _form['target_ref']
230 230 commit_ids = _form['revisions'][::-1]
231 reviewers = [
232 (r['user_id'], r['reasons']) for r in _form['review_members']]
233 231
234 232 # find the ancestor for this pr
235 233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
236 234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
237 235
238 236 source_scm = source_db_repo.scm_instance()
239 237 target_scm = target_db_repo.scm_instance()
240 238
241 239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
242 240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
243 241
244 242 ancestor = source_scm.get_common_ancestor(
245 243 source_commit.raw_id, target_commit.raw_id, target_scm)
246 244
247 245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
248 246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
249 247
250 248 pullrequest_title = _form['pullrequest_title']
251 249 title_source_ref = source_ref.split(':', 2)[1]
252 250 if not pullrequest_title:
253 251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
254 252 source=source_repo,
255 253 source_ref=title_source_ref,
256 254 target=target_repo
257 255 )
258 256
259 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 270 try:
261 271 pull_request = PullRequestModel().create(
262 272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
263 273 target_ref, commit_ids, reviewers, pullrequest_title,
264 description
274 description, reviewer_rules
265 275 )
266 276 Session().commit()
267 277 h.flash(_('Successfully opened new pull request'),
268 278 category='success')
269 279 except Exception as e:
270 msg = _('Error occurred during sending pull request')
280 msg = _('Error occurred during creation of this pull request.')
271 281 log.exception(msg)
272 282 h.flash(msg, category='error')
273 283 return redirect(url('pullrequest_home', repo_name=repo_name))
274 284
275 285 return redirect(url('pullrequest_show', repo_name=target_repo,
276 286 pull_request_id=pull_request.pull_request_id))
277 287
278 288 @LoginRequired()
279 289 @NotAnonymous()
280 290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
281 291 'repository.admin')
282 292 @auth.CSRFRequired()
283 293 @jsonify
284 294 def update(self, repo_name, pull_request_id):
285 295 pull_request_id = safe_int(pull_request_id)
286 296 pull_request = PullRequest.get_or_404(pull_request_id)
287 297 # only owner or admin can update it
288 298 allowed_to_update = PullRequestModel().check_user_update(
289 299 pull_request, c.rhodecode_user)
290 300 if allowed_to_update:
291 301 controls = peppercorn.parse(request.POST.items())
292 302
293 303 if 'review_members' in controls:
294 304 self._update_reviewers(
295 pull_request_id, controls['review_members'])
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
296 307 elif str2bool(request.POST.get('update_commits', 'false')):
297 308 self._update_commits(pull_request)
298 309 elif str2bool(request.POST.get('close_pull_request', 'false')):
299 310 self._reject_close(pull_request)
300 311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
301 312 self._edit_pull_request(pull_request)
302 313 else:
303 314 raise HTTPBadRequest()
304 315 return True
305 316 raise HTTPForbidden()
306 317
307 318 def _edit_pull_request(self, pull_request):
308 319 try:
309 320 PullRequestModel().edit(
310 321 pull_request, request.POST.get('title'),
311 322 request.POST.get('description'))
312 323 except ValueError:
313 324 msg = _(u'Cannot update closed pull requests.')
314 325 h.flash(msg, category='error')
315 326 return
316 327 else:
317 328 Session().commit()
318 329
319 330 msg = _(u'Pull request title & description updated.')
320 331 h.flash(msg, category='success')
321 332 return
322 333
323 334 def _update_commits(self, pull_request):
324 335 resp = PullRequestModel().update_commits(pull_request)
325 336
326 337 if resp.executed:
327 338
328 339 if resp.target_changed and resp.source_changed:
329 340 changed = 'target and source repositories'
330 341 elif resp.target_changed and not resp.source_changed:
331 342 changed = 'target repository'
332 343 elif not resp.target_changed and resp.source_changed:
333 344 changed = 'source repository'
334 345 else:
335 346 changed = 'nothing'
336 347
337 348 msg = _(
338 349 u'Pull request updated to "{source_commit_id}" with '
339 350 u'{count_added} added, {count_removed} removed commits. '
340 351 u'Source of changes: {change_source}')
341 352 msg = msg.format(
342 353 source_commit_id=pull_request.source_ref_parts.commit_id,
343 354 count_added=len(resp.changes.added),
344 355 count_removed=len(resp.changes.removed),
345 356 change_source=changed)
346 357 h.flash(msg, category='success')
347 358
348 359 registry = get_current_registry()
349 360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
350 361 channelstream_config = rhodecode_plugins.get('channelstream', {})
351 362 if channelstream_config.get('enabled'):
352 363 message = msg + (
353 364 ' - <a onclick="window.location.reload()">'
354 365 '<strong>{}</strong></a>'.format(_('Reload page')))
355 366 channel = '/repo${}$/pr/{}'.format(
356 367 pull_request.target_repo.repo_name,
357 368 pull_request.pull_request_id
358 369 )
359 370 payload = {
360 371 'type': 'message',
361 372 'user': 'system',
362 373 'exclude_users': [request.user.username],
363 374 'channel': channel,
364 375 'message': {
365 376 'message': message,
366 377 'level': 'success',
367 378 'topic': '/notifications'
368 379 }
369 380 }
370 381 channelstream_request(
371 382 channelstream_config, [payload], '/message',
372 383 raise_exc=False)
373 384 else:
374 385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
375 386 warning_reasons = [
376 387 UpdateFailureReason.NO_CHANGE,
377 388 UpdateFailureReason.WRONG_REF_TYPE,
378 389 ]
379 390 category = 'warning' if resp.reason in warning_reasons else 'error'
380 391 h.flash(msg, category=category)
381 392
382 393 @auth.CSRFRequired()
383 394 @LoginRequired()
384 395 @NotAnonymous()
385 396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 397 'repository.admin')
387 398 def merge(self, repo_name, pull_request_id):
388 399 """
389 400 POST /{repo_name}/pull-request/{pull_request_id}
390 401
391 402 Merge will perform a server-side merge of the specified
392 403 pull request, if the pull request is approved and mergeable.
393 404 After successful merging, the pull request is automatically
394 405 closed, with a relevant comment.
395 406 """
396 407 pull_request_id = safe_int(pull_request_id)
397 408 pull_request = PullRequest.get_or_404(pull_request_id)
398 409 user = c.rhodecode_user
399 410
400 411 check = MergeCheck.validate(pull_request, user)
401 412 merge_possible = not check.failed
402 413
403 414 for err_type, error_msg in check.errors:
404 415 h.flash(error_msg, category=err_type)
405 416
406 417 if merge_possible:
407 418 log.debug("Pre-conditions checked, trying to merge.")
408 419 extras = vcs_operation_context(
409 420 request.environ, repo_name=pull_request.target_repo.repo_name,
410 421 username=user.username, action='push',
411 422 scm=pull_request.target_repo.repo_type)
412 423 self._merge_pull_request(pull_request, user, extras)
413 424
414 425 return redirect(url(
415 426 'pullrequest_show',
416 427 repo_name=pull_request.target_repo.repo_name,
417 428 pull_request_id=pull_request.pull_request_id))
418 429
419 430 def _merge_pull_request(self, pull_request, user, extras):
420 431 merge_resp = PullRequestModel().merge(
421 432 pull_request, user, extras=extras)
422 433
423 434 if merge_resp.executed:
424 435 log.debug("The merge was successful, closing the pull request.")
425 436 PullRequestModel().close_pull_request(
426 437 pull_request.pull_request_id, user)
427 438 Session().commit()
428 439 msg = _('Pull request was successfully merged and closed.')
429 440 h.flash(msg, category='success')
430 441 else:
431 442 log.debug(
432 443 "The merge was not successful. Merge response: %s",
433 444 merge_resp)
434 445 msg = PullRequestModel().merge_status_message(
435 446 merge_resp.failure_reason)
436 447 h.flash(msg, category='error')
437 448
438 def _update_reviewers(self, pull_request_id, review_members):
439 reviewers = [
440 (int(r['user_id']), r['reasons']) for r in review_members]
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
450
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 461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
442 463 Session().commit()
443 464
444 465 def _reject_close(self, pull_request):
445 466 if pull_request.is_closed():
446 467 raise HTTPForbidden()
447 468
448 469 PullRequestModel().close_pull_request_with_comment(
449 470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
450 471 Session().commit()
451 472
452 473 @LoginRequired()
453 474 @NotAnonymous()
454 475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 476 'repository.admin')
456 477 @auth.CSRFRequired()
457 478 @jsonify
458 479 def delete(self, repo_name, pull_request_id):
459 480 pull_request_id = safe_int(pull_request_id)
460 481 pull_request = PullRequest.get_or_404(pull_request_id)
461 482
462 483 pr_closed = pull_request.is_closed()
463 484 allowed_to_delete = PullRequestModel().check_user_delete(
464 485 pull_request, c.rhodecode_user) and not pr_closed
465 486
466 487 # only owner can delete it !
467 488 if allowed_to_delete:
468 489 PullRequestModel().delete(pull_request)
469 490 Session().commit()
470 491 h.flash(_('Successfully deleted pull request'),
471 492 category='success')
472 493 return redirect(url('my_account_pullrequests'))
473 494
474 495 h.flash(_('Your are not allowed to delete this pull request'),
475 496 category='error')
476 497 raise HTTPForbidden()
477 498
478 499 def _get_pr_version(self, pull_request_id, version=None):
479 500 pull_request_id = safe_int(pull_request_id)
480 501 at_version = None
481 502
482 503 if version and version == 'latest':
483 504 pull_request_ver = PullRequest.get(pull_request_id)
484 505 pull_request_obj = pull_request_ver
485 506 _org_pull_request_obj = pull_request_obj
486 507 at_version = 'latest'
487 508 elif version:
488 509 pull_request_ver = PullRequestVersion.get_or_404(version)
489 510 pull_request_obj = pull_request_ver
490 511 _org_pull_request_obj = pull_request_ver.pull_request
491 512 at_version = pull_request_ver.pull_request_version_id
492 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 517 pull_request_display_obj = PullRequest.get_pr_display_object(
496 518 pull_request_obj, _org_pull_request_obj)
497 519
498 520 return _org_pull_request_obj, pull_request_obj, \
499 521 pull_request_display_obj, at_version
500 522
501 523 def _get_diffset(
502 524 self, source_repo, source_ref_id, target_ref_id, target_commit,
503 525 source_commit, diff_limit, file_limit, display_inline_comments):
504 526 vcs_diff = PullRequestModel().get_diff(
505 527 source_repo, source_ref_id, target_ref_id)
506 528
507 529 diff_processor = diffs.DiffProcessor(
508 530 vcs_diff, format='newdiff', diff_limit=diff_limit,
509 531 file_limit=file_limit, show_full_diff=c.fulldiff)
510 532
511 533 _parsed = diff_processor.prepare()
512 534
513 535 def _node_getter(commit):
514 536 def get_node(fname):
515 537 try:
516 538 return commit.get_node(fname)
517 539 except NodeDoesNotExistError:
518 540 return None
519 541
520 542 return get_node
521 543
522 544 diffset = codeblocks.DiffSet(
523 545 repo_name=c.repo_name,
524 546 source_repo_name=c.source_repo.repo_name,
525 547 source_node_getter=_node_getter(target_commit),
526 548 target_node_getter=_node_getter(source_commit),
527 549 comments=display_inline_comments
528 550 )
529 551 diffset = diffset.render_patchset(
530 552 _parsed, target_commit.raw_id, source_commit.raw_id)
531 553
532 554 return diffset
533 555
534 556 @LoginRequired()
535 557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 558 'repository.admin')
537 559 def show(self, repo_name, pull_request_id):
538 560 pull_request_id = safe_int(pull_request_id)
539 561 version = request.GET.get('version')
540 562 from_version = request.GET.get('from_version') or version
541 563 merge_checks = request.GET.get('merge_checks')
542 564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
543 565
544 566 (pull_request_latest,
545 567 pull_request_at_ver,
546 568 pull_request_display_obj,
547 569 at_version) = self._get_pr_version(
548 570 pull_request_id, version=version)
549 571 pr_closed = pull_request_latest.is_closed()
550 572
551 573 if pr_closed and (version or from_version):
552 574 # not allow to browse versions
553 575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
554 576 pull_request_id=pull_request_id))
555 577
556 578 versions = pull_request_display_obj.versions()
557 579
558 580 c.at_version = at_version
559 581 c.at_version_num = (at_version
560 582 if at_version and at_version != 'latest'
561 583 else None)
562 584 c.at_version_pos = ChangesetComment.get_index_from_version(
563 585 c.at_version_num, versions)
564 586
565 587 (prev_pull_request_latest,
566 588 prev_pull_request_at_ver,
567 589 prev_pull_request_display_obj,
568 590 prev_at_version) = self._get_pr_version(
569 591 pull_request_id, version=from_version)
570 592
571 593 c.from_version = prev_at_version
572 594 c.from_version_num = (prev_at_version
573 595 if prev_at_version and prev_at_version != 'latest'
574 596 else None)
575 597 c.from_version_pos = ChangesetComment.get_index_from_version(
576 598 c.from_version_num, versions)
577 599
578 600 # define if we're in COMPARE mode or VIEW at version mode
579 601 compare = at_version != prev_at_version
580 602
581 603 # pull_requests repo_name we opened it against
582 604 # ie. target_repo must match
583 605 if repo_name != pull_request_at_ver.target_repo.repo_name:
584 606 raise HTTPNotFound
585 607
586 608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
587 609 pull_request_at_ver)
588 610
589 611 c.pull_request = pull_request_display_obj
590 612 c.pull_request_latest = pull_request_latest
591 613
592 614 if compare or (at_version and not at_version == 'latest'):
593 615 c.allowed_to_change_status = False
594 616 c.allowed_to_update = False
595 617 c.allowed_to_merge = False
596 618 c.allowed_to_delete = False
597 619 c.allowed_to_comment = False
598 620 c.allowed_to_close = False
599 621 else:
600 c.allowed_to_change_status = PullRequestModel(). \
601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
602 and not pr_closed
622 can_change_status = PullRequestModel().check_user_change_status(
623 pull_request_at_ver, c.rhodecode_user)
624 c.allowed_to_change_status = can_change_status and not pr_closed
603 625
604 626 c.allowed_to_update = PullRequestModel().check_user_update(
605 627 pull_request_latest, c.rhodecode_user) and not pr_closed
606 628 c.allowed_to_merge = PullRequestModel().check_user_merge(
607 629 pull_request_latest, c.rhodecode_user) and not pr_closed
608 630 c.allowed_to_delete = PullRequestModel().check_user_delete(
609 631 pull_request_latest, c.rhodecode_user) and not pr_closed
610 632 c.allowed_to_comment = not pr_closed
611 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 647 # check merge capabilities
614 648 _merge_check = MergeCheck.validate(
615 649 pull_request_latest, user=c.rhodecode_user)
616 650 c.pr_merge_errors = _merge_check.error_details
617 651 c.pr_merge_possible = not _merge_check.failed
618 652 c.pr_merge_message = _merge_check.merge_msg
619 653
620 654 c.pull_request_review_status = _merge_check.review_status
621 655 if merge_checks:
622 656 return render('/pullrequests/pullrequest_merge_checks.mako')
623 657
624 658 comments_model = CommentsModel()
625 659
626 660 # reviewers and statuses
627 661 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
628 662 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
629 663
630 664 # GENERAL COMMENTS with versions #
631 665 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
632 666 q = q.order_by(ChangesetComment.comment_id.asc())
633 667 general_comments = q
634 668
635 669 # pick comments we want to render at current version
636 670 c.comment_versions = comments_model.aggregate_comments(
637 671 general_comments, versions, c.at_version_num)
638 672 c.comments = c.comment_versions[c.at_version_num]['until']
639 673
640 674 # INLINE COMMENTS with versions #
641 675 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
642 676 q = q.order_by(ChangesetComment.comment_id.asc())
643 677 inline_comments = q
644 678
645 679 c.inline_versions = comments_model.aggregate_comments(
646 680 inline_comments, versions, c.at_version_num, inline=True)
647 681
648 682 # inject latest version
649 683 latest_ver = PullRequest.get_pr_display_object(
650 684 pull_request_latest, pull_request_latest)
651 685
652 686 c.versions = versions + [latest_ver]
653 687
654 688 # if we use version, then do not show later comments
655 689 # than current version
656 690 display_inline_comments = collections.defaultdict(
657 691 lambda: collections.defaultdict(list))
658 692 for co in inline_comments:
659 693 if c.at_version_num:
660 694 # pick comments that are at least UPTO given version, so we
661 695 # don't render comments for higher version
662 696 should_render = co.pull_request_version_id and \
663 697 co.pull_request_version_id <= c.at_version_num
664 698 else:
665 699 # showing all, for 'latest'
666 700 should_render = True
667 701
668 702 if should_render:
669 703 display_inline_comments[co.f_path][co.line_no].append(co)
670 704
671 705 # load diff data into template context, if we use compare mode then
672 706 # diff is calculated based on changes between versions of PR
673 707
674 708 source_repo = pull_request_at_ver.source_repo
675 709 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
676 710
677 711 target_repo = pull_request_at_ver.target_repo
678 712 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
679 713
680 714 if compare:
681 715 # in compare switch the diff base to latest commit from prev version
682 716 target_ref_id = prev_pull_request_display_obj.revisions[0]
683 717
684 718 # despite opening commits for bookmarks/branches/tags, we always
685 719 # convert this to rev to prevent changes after bookmark or branch change
686 720 c.source_ref_type = 'rev'
687 721 c.source_ref = source_ref_id
688 722
689 723 c.target_ref_type = 'rev'
690 724 c.target_ref = target_ref_id
691 725
692 726 c.source_repo = source_repo
693 727 c.target_repo = target_repo
694 728
695 729 # diff_limit is the old behavior, will cut off the whole diff
696 730 # if the limit is applied otherwise will just hide the
697 731 # big files from the front-end
698 732 diff_limit = self.cut_off_limit_diff
699 733 file_limit = self.cut_off_limit_file
700 734
701 735 c.commit_ranges = []
702 736 source_commit = EmptyCommit()
703 737 target_commit = EmptyCommit()
704 738 c.missing_requirements = False
705 739
706 740 source_scm = source_repo.scm_instance()
707 741 target_scm = target_repo.scm_instance()
708 742
709 743 # try first shadow repo, fallback to regular repo
710 744 try:
711 745 commits_source_repo = pull_request_latest.get_shadow_repo()
712 746 except Exception:
713 747 log.debug('Failed to get shadow repo', exc_info=True)
714 748 commits_source_repo = source_scm
715 749
716 750 c.commits_source_repo = commits_source_repo
717 751 commit_cache = {}
718 752 try:
719 753 pre_load = ["author", "branch", "date", "message"]
720 754 show_revs = pull_request_at_ver.revisions
721 755 for rev in show_revs:
722 756 comm = commits_source_repo.get_commit(
723 757 commit_id=rev, pre_load=pre_load)
724 758 c.commit_ranges.append(comm)
725 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 763 target_commit = commits_source_repo.get_commit(
728 764 commit_id=safe_str(target_ref_id))
765
729 766 source_commit = commits_source_repo.get_commit(
730 767 commit_id=safe_str(source_ref_id))
768
731 769 except CommitDoesNotExistError:
732 pass
770 log.warning(
771 'Failed to get commit from `{}` repo'.format(
772 commits_source_repo), exc_info=True)
733 773 except RepositoryRequirementError:
734 774 log.warning(
735 775 'Failed to get all required data from repo', exc_info=True)
736 776 c.missing_requirements = True
737 777
738 778 c.ancestor = None # set it to None, to hide it from PR view
739 779
740 780 try:
741 781 ancestor_id = source_scm.get_common_ancestor(
742 782 source_commit.raw_id, target_commit.raw_id, target_scm)
743 783 c.ancestor_commit = source_scm.get_commit(ancestor_id)
744 784 except Exception:
745 785 c.ancestor_commit = None
746 786
747 787 c.statuses = source_repo.statuses(
748 788 [x.raw_id for x in c.commit_ranges])
749 789
750 790 # auto collapse if we have more than limit
751 791 collapse_limit = diffs.DiffProcessor._collapse_commits_over
752 792 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
753 793 c.compare_mode = compare
754 794
755 795 c.missing_commits = False
756 796 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
757 797 or source_commit == target_commit):
758 798
759 799 c.missing_commits = True
760 800 else:
761 801
762 802 c.diffset = self._get_diffset(
763 803 commits_source_repo, source_ref_id, target_ref_id,
764 804 target_commit, source_commit,
765 805 diff_limit, file_limit, display_inline_comments)
766 806
767 807 c.limited_diff = c.diffset.limited_diff
768 808
769 809 # calculate removed files that are bound to comments
770 810 comment_deleted_files = [
771 811 fname for fname in display_inline_comments
772 812 if fname not in c.diffset.file_stats]
773 813
774 814 c.deleted_files_comments = collections.defaultdict(dict)
775 815 for fname, per_line_comments in display_inline_comments.items():
776 816 if fname in comment_deleted_files:
777 817 c.deleted_files_comments[fname]['stats'] = 0
778 818 c.deleted_files_comments[fname]['comments'] = list()
779 819 for lno, comments in per_line_comments.items():
780 820 c.deleted_files_comments[fname]['comments'].extend(
781 821 comments)
782 822
783 823 # this is a hack to properly display links, when creating PR, the
784 824 # compare view and others uses different notation, and
785 825 # compare_commits.mako renders links based on the target_repo.
786 826 # We need to swap that here to generate it properly on the html side
787 827 c.target_repo = c.source_repo
788 828
789 829 c.commit_statuses = ChangesetStatus.STATUSES
790 830
791 831 c.show_version_changes = not pr_closed
792 832 if c.show_version_changes:
793 833 cur_obj = pull_request_at_ver
794 834 prev_obj = prev_pull_request_at_ver
795 835
796 836 old_commit_ids = prev_obj.revisions
797 837 new_commit_ids = cur_obj.revisions
798 838 commit_changes = PullRequestModel()._calculate_commit_id_changes(
799 839 old_commit_ids, new_commit_ids)
800 840 c.commit_changes_summary = commit_changes
801 841
802 842 # calculate the diff for commits between versions
803 843 c.commit_changes = []
804 844 mark = lambda cs, fw: list(
805 845 h.itertools.izip_longest([], cs, fillvalue=fw))
806 846 for c_type, raw_id in mark(commit_changes.added, 'a') \
807 847 + mark(commit_changes.removed, 'r') \
808 848 + mark(commit_changes.common, 'c'):
809 849
810 850 if raw_id in commit_cache:
811 851 commit = commit_cache[raw_id]
812 852 else:
813 853 try:
814 854 commit = commits_source_repo.get_commit(raw_id)
815 855 except CommitDoesNotExistError:
816 856 # in case we fail extracting still use "dummy" commit
817 857 # for display in commit diff
818 858 commit = h.AttributeDict(
819 859 {'raw_id': raw_id,
820 860 'message': 'EMPTY or MISSING COMMIT'})
821 861 c.commit_changes.append([c_type, commit])
822 862
823 863 # current user review statuses for each version
824 864 c.review_versions = {}
825 865 if c.rhodecode_user.user_id in allowed_reviewers:
826 866 for co in general_comments:
827 867 if co.author.user_id == c.rhodecode_user.user_id:
828 868 # each comment has a status change
829 869 status = co.status_change
830 870 if status:
831 871 _ver_pr = status[0].comment.pull_request_version_id
832 872 c.review_versions[_ver_pr] = status[0]
833 873
834 874 return render('/pullrequests/pullrequest_show.mako')
835 875
836 876 @LoginRequired()
837 877 @NotAnonymous()
838 878 @HasRepoPermissionAnyDecorator(
839 879 'repository.read', 'repository.write', 'repository.admin')
840 880 @auth.CSRFRequired()
841 881 @jsonify
842 882 def comment(self, repo_name, pull_request_id):
843 883 pull_request_id = safe_int(pull_request_id)
844 884 pull_request = PullRequest.get_or_404(pull_request_id)
845 885 if pull_request.is_closed():
846 886 raise HTTPForbidden()
847 887
848 888 status = request.POST.get('changeset_status', None)
849 889 text = request.POST.get('text')
850 890 comment_type = request.POST.get('comment_type')
851 891 resolves_comment_id = request.POST.get('resolves_comment_id', None)
852 892 close_pull_request = request.POST.get('close_pull_request')
853 893
854 894 close_pr = False
855 895 # only owner or admin or person with write permissions
856 896 allowed_to_close = PullRequestModel().check_user_update(
857 897 pull_request, c.rhodecode_user)
858 898
859 899 if close_pull_request and allowed_to_close:
860 900 close_pr = True
861 901 pull_request_review_status = pull_request.calculated_review_status()
862 902 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
863 903 # approved only if we have voting consent
864 904 status = ChangesetStatus.STATUS_APPROVED
865 905 else:
866 906 status = ChangesetStatus.STATUS_REJECTED
867 907
868 908 allowed_to_change_status = PullRequestModel().check_user_change_status(
869 909 pull_request, c.rhodecode_user)
870 910
871 911 if status and allowed_to_change_status:
872 912 message = (_('Status change %(transition_icon)s %(status)s')
873 913 % {'transition_icon': '>',
874 914 'status': ChangesetStatus.get_status_lbl(status)})
875 915 if close_pr:
876 916 message = _('Closing with') + ' ' + message
877 917 text = text or message
878 918 comm = CommentsModel().create(
879 919 text=text,
880 920 repo=c.rhodecode_db_repo.repo_id,
881 921 user=c.rhodecode_user.user_id,
882 922 pull_request=pull_request_id,
883 923 f_path=request.POST.get('f_path'),
884 924 line_no=request.POST.get('line'),
885 925 status_change=(ChangesetStatus.get_status_lbl(status)
886 926 if status and allowed_to_change_status else None),
887 927 status_change_type=(status
888 928 if status and allowed_to_change_status else None),
889 929 closing_pr=close_pr,
890 930 comment_type=comment_type,
891 931 resolves_comment_id=resolves_comment_id
892 932 )
893 933
894 934 if allowed_to_change_status:
895 935 old_calculated_status = pull_request.calculated_review_status()
896 936 # get status if set !
897 937 if status:
898 938 ChangesetStatusModel().set_status(
899 939 c.rhodecode_db_repo.repo_id,
900 940 status,
901 941 c.rhodecode_user.user_id,
902 942 comm,
903 943 pull_request=pull_request_id
904 944 )
905 945
906 946 Session().flush()
907 947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
908 948 # we now calculate the status of pull request, and based on that
909 949 # calculation we set the commits status
910 950 calculated_status = pull_request.calculated_review_status()
911 951 if old_calculated_status != calculated_status:
912 952 PullRequestModel()._trigger_pull_request_hook(
913 953 pull_request, c.rhodecode_user, 'review_status_change')
914 954
915 955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
916 956 calculated_status)
917 957
918 958 if close_pr:
919 959 status_completed = (
920 960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
921 961 ChangesetStatus.STATUS_REJECTED])
922 962 if close_pull_request or status_completed:
923 963 PullRequestModel().close_pull_request(
924 964 pull_request_id, c.rhodecode_user)
925 965 else:
926 966 h.flash(_('Closing pull request on other statuses than '
927 967 'rejected or approved is forbidden. '
928 968 'Calculated status from all reviewers '
929 969 'is currently: %s') % calculated_status_lbl,
930 970 category='warning')
931 971
932 972 Session().commit()
933 973
934 974 if not request.is_xhr:
935 975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
936 976 pull_request_id=pull_request_id))
937 977
938 978 data = {
939 979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
940 980 }
941 981 if comm:
942 982 c.co = comm
943 983 c.inline_comment = True if comm.line_no else False
944 984 data.update(comm.get_dict())
945 985 data.update({'rendered_text':
946 986 render('changeset/changeset_comment_block.mako')})
947 987
948 988 return data
949 989
950 990 @LoginRequired()
951 991 @NotAnonymous()
952 992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
953 993 'repository.admin')
954 994 @auth.CSRFRequired()
955 995 @jsonify
956 996 def delete_comment(self, repo_name, comment_id):
957 997 return self._delete_comment(comment_id)
958 998
959 999 def _delete_comment(self, comment_id):
960 1000 comment_id = safe_int(comment_id)
961 1001 co = ChangesetComment.get_or_404(comment_id)
962 1002 if co.pull_request.is_closed():
963 1003 # don't allow deleting comments on closed pull request
964 1004 raise HTTPForbidden()
965 1005
966 1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
967 1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
968 1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
969 1009 old_calculated_status = co.pull_request.calculated_review_status()
970 1010 CommentsModel().delete(comment=co)
971 1011 Session().commit()
972 1012 calculated_status = co.pull_request.calculated_review_status()
973 1013 if old_calculated_status != calculated_status:
974 1014 PullRequestModel()._trigger_pull_request_hook(
975 1015 co.pull_request, c.rhodecode_user, 'review_status_change')
976 1016 return True
977 1017 else:
978 1018 raise HTTPForbidden()
@@ -1,109 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Utilities to be shared by multiple controllers.
23 23
24 24 Should only contain utilities to be shared in the controller layer.
25 25 """
26 26
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.vcs.exceptions import RepositoryError
29 29
30 30
31 31 def parse_path_ref(ref, default_path=None):
32 32 """
33 33 Parse out a path and reference combination and return both parts of it.
34 34
35 35 This is used to allow support of path based comparisons for Subversion
36 36 as an iterim solution in parameter handling.
37 37 """
38 38 if '@' in ref:
39 39 return ref.rsplit('@', 1)
40 40 else:
41 41 return default_path, ref
42 42
43 43
44 44 def get_format_ref_id(repo):
45 45 """Returns a `repo` specific reference formatter function"""
46 46 if h.is_svn(repo):
47 47 return _format_ref_id_svn
48 48 else:
49 49 return _format_ref_id
50 50
51 51
52 52 def _format_ref_id(name, raw_id):
53 53 """Default formatting of a given reference `name`"""
54 54 return name
55 55
56 56
57 57 def _format_ref_id_svn(name, raw_id):
58 58 """Special way of formatting a reference for Subversion including path"""
59 59 return '%s@%s' % (name, raw_id)
60 60
61 61
62 62 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
63 63 """
64 64 Gets the commit for a `ref_name` taking into account `ref_type`.
65 65 Needed in case a bookmark / tag share the same name.
66 66
67 67 :param repo: the repo instance
68 68 :param ref_name: the name of the ref to get
69 69 :param ref_type: optional, used to disambiguate colliding refs
70 70 """
71 71 repo_scm = repo.scm_instance()
72 72 ref_type_mapping = {
73 73 'book': repo_scm.bookmarks,
74 74 'bookmark': repo_scm.bookmarks,
75 75 'tag': repo_scm.tags,
76 76 'branch': repo_scm.branches,
77 77 }
78 78
79 79 commit_id = ref_name
80 80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
81 81 # the branch issue with svn is fixed
82 82 if ref_type and ref_type in ref_type_mapping:
83 83 try:
84 84 commit_id = ref_type_mapping[ref_type][ref_name]
85 85 except KeyError:
86 86 raise RepositoryError(
87 87 '%s "%s" does not exist' % (ref_type, ref_name))
88 88
89 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Changeset status conttroller
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 from collections import defaultdict
28 28
29 29 from rhodecode.model import BaseModel
30 30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
31 31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 32 from rhodecode.lib.markup_renderer import (
33 33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class ChangesetStatusModel(BaseModel):
39 39
40 40 cls = ChangesetStatus
41 41
42 42 def __get_changeset_status(self, changeset_status):
43 43 return self._get_instance(ChangesetStatus, changeset_status)
44 44
45 45 def __get_pull_request(self, pull_request):
46 46 return self._get_instance(PullRequest, pull_request)
47 47
48 48 def _get_status_query(self, repo, revision, pull_request,
49 49 with_revisions=False):
50 50 repo = self._get_repo(repo)
51 51
52 52 q = ChangesetStatus.query()\
53 53 .filter(ChangesetStatus.repo == repo)
54 54 if not with_revisions:
55 55 q = q.filter(ChangesetStatus.version == 0)
56 56
57 57 if revision:
58 58 q = q.filter(ChangesetStatus.revision == revision)
59 59 elif pull_request:
60 60 pull_request = self.__get_pull_request(pull_request)
61 61 # TODO: johbo: Think about the impact of this join, there must
62 62 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 63 # to the pull request. Might be that we want to do the same for
64 64 # the pull_request_version_id.
65 65 q = q.join(ChangesetComment).filter(
66 66 ChangesetStatus.pull_request == pull_request,
67 67 ChangesetComment.pull_request_version_id == None)
68 68 else:
69 69 raise Exception('Please specify revision or pull_request')
70 70 q = q.order_by(ChangesetStatus.version.asc())
71 71 return q
72 72
73 73 def calculate_status(self, statuses_by_reviewers):
74 74 """
75 75 Given the approval statuses from reviewers, calculates final approval
76 76 status. There can only be 3 results, all approved, all rejected. If
77 77 there is no consensus the PR is under review.
78 78
79 79 :param statuses_by_reviewers:
80 80 """
81 81 votes = defaultdict(int)
82 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 84 if statuses:
85 85 ver, latest = statuses[0]
86 86 votes[latest.status] += 1
87 87 else:
88 88 votes[ChangesetStatus.DEFAULT] += 1
89 89
90 90 # all approved
91 91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 92 return ChangesetStatus.STATUS_APPROVED
93 93
94 94 # all rejected
95 95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 96 return ChangesetStatus.STATUS_REJECTED
97 97
98 98 return ChangesetStatus.STATUS_UNDER_REVIEW
99 99
100 100 def get_statuses(self, repo, revision=None, pull_request=None,
101 101 with_revisions=False):
102 102 q = self._get_status_query(repo, revision, pull_request,
103 103 with_revisions)
104 104 return q.all()
105 105
106 106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 107 """
108 108 Returns latest status of changeset for given revision or for given
109 109 pull request. Statuses are versioned inside a table itself and
110 110 version == 0 is always the current one
111 111
112 112 :param repo:
113 113 :param revision: 40char hash or None
114 114 :param pull_request: pull_request reference
115 115 :param as_str: return status as string not object
116 116 """
117 117 q = self._get_status_query(repo, revision, pull_request)
118 118
119 119 # need to use first here since there can be multiple statuses
120 120 # returned from pull_request
121 121 status = q.first()
122 122 if as_str:
123 123 status = status.status if status else status
124 124 st = status or ChangesetStatus.DEFAULT
125 125 return str(st)
126 126 return status
127 127
128 128 def _render_auto_status_message(
129 129 self, status, commit_id=None, pull_request=None):
130 130 """
131 131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 132 so it's always looking the same disregarding on which default
133 133 renderer system is using.
134 134
135 135 :param status: status text to change into
136 136 :param commit_id: the commit_id we change the status for
137 137 :param pull_request: the pull request we change the status for
138 138 """
139 139
140 140 new_status = ChangesetStatus.get_status_lbl(status)
141 141
142 142 params = {
143 143 'new_status_label': new_status,
144 144 'pull_request': pull_request,
145 145 'commit_id': commit_id,
146 146 }
147 147 renderer = RstTemplateRenderer()
148 148 return renderer.render('auto_status_change.mako', **params)
149 149
150 150 def set_status(self, repo, status, user, comment=None, revision=None,
151 151 pull_request=None, dont_allow_on_closed_pull_request=False):
152 152 """
153 153 Creates new status for changeset or updates the old ones bumping their
154 154 version, leaving the current status at
155 155
156 156 :param repo:
157 157 :param revision:
158 158 :param status:
159 159 :param user:
160 160 :param comment:
161 161 :param dont_allow_on_closed_pull_request: don't allow a status change
162 162 if last status was for pull request and it's closed. We shouldn't
163 163 mess around this manually
164 164 """
165 165 repo = self._get_repo(repo)
166 166
167 167 q = ChangesetStatus.query()
168 168
169 169 if revision:
170 170 q = q.filter(ChangesetStatus.repo == repo)
171 171 q = q.filter(ChangesetStatus.revision == revision)
172 172 elif pull_request:
173 173 pull_request = self.__get_pull_request(pull_request)
174 174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 176 cur_statuses = q.all()
177 177
178 178 # if statuses exists and last is associated with a closed pull request
179 179 # we need to check if we can allow this status change
180 180 if (dont_allow_on_closed_pull_request and cur_statuses
181 181 and getattr(cur_statuses[0].pull_request, 'status', '')
182 182 == PullRequest.STATUS_CLOSED):
183 183 raise StatusChangeOnClosedPullRequestError(
184 184 'Changing status on closed pull request is not allowed'
185 185 )
186 186
187 187 # update all current statuses with older version
188 188 if cur_statuses:
189 189 for st in cur_statuses:
190 190 st.version += 1
191 191 self.sa.add(st)
192 192
193 193 def _create_status(user, repo, status, comment, revision, pull_request):
194 194 new_status = ChangesetStatus()
195 195 new_status.author = self._get_user(user)
196 196 new_status.repo = self._get_repo(repo)
197 197 new_status.status = status
198 198 new_status.comment = comment
199 199 new_status.revision = revision
200 200 new_status.pull_request = pull_request
201 201 return new_status
202 202
203 203 if not comment:
204 204 from rhodecode.model.comment import CommentsModel
205 205 comment = CommentsModel().create(
206 206 text=self._render_auto_status_message(
207 207 status, commit_id=revision, pull_request=pull_request),
208 208 repo=repo,
209 209 user=user,
210 210 pull_request=pull_request,
211 211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 212 )
213 213
214 214 if revision:
215 215 new_status = _create_status(
216 216 user=user, repo=repo, status=status, comment=comment,
217 217 revision=revision, pull_request=pull_request)
218 218 self.sa.add(new_status)
219 219 return new_status
220 220 elif pull_request:
221 221 # pull request can have more than one revision associated to it
222 222 # we need to create new version for each one
223 223 new_statuses = []
224 224 repo = pull_request.source_repo
225 225 for rev in pull_request.revisions:
226 226 new_status = _create_status(
227 227 user=user, repo=repo, status=status, comment=comment,
228 228 revision=rev, pull_request=pull_request)
229 229 new_statuses.append(new_status)
230 230 self.sa.add(new_status)
231 231 return new_statuses
232 232
233 233 def reviewers_statuses(self, pull_request):
234 234 _commit_statuses = self.get_statuses(
235 235 pull_request.source_repo,
236 236 pull_request=pull_request,
237 237 with_revisions=True)
238 238
239 239 commit_statuses = defaultdict(list)
240 240 for st in _commit_statuses:
241 241 commit_statuses[st.author.username] += [st]
242 242
243 243 pull_request_reviewers = []
244 244
245 245 def version(commit_status):
246 246 return commit_status.version
247 247
248 248 for o in pull_request.reviewers:
249 249 if not o.user:
250 250 continue
251 st = commit_statuses.get(o.user.username, None)
252 if st:
253 st = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
255 version))]
251 statuses = commit_statuses.get(o.user.username, None)
252 if statuses:
253 statuses = [(x, list(y)[0])
254 for x, y in (itertools.groupby(
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 259 return pull_request_reviewers
259 260
260 261 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 262 """
262 263 calculate pull request status based on reviewers, it should be a list
263 264 of two element lists.
264 265
265 266 :param reviewers_statuses:
266 267 """
267 268 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 269 return self.calculate_status(reviewers)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,562 +1,563 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 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 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 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 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 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 105 def UserForm(edit=False, available_languages=[], old_data={}):
106 106 class _UserForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 110 v.ValidUsername(edit, old_data))
111 111 if edit:
112 112 new_password = All(
113 113 v.ValidPassword(),
114 114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 115 )
116 116 password_confirmation = All(
117 117 v.ValidPassword(),
118 118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 119 )
120 120 admin = v.StringBoolean(if_missing=False)
121 121 else:
122 122 password = All(
123 123 v.ValidPassword(),
124 124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 125 )
126 126 password_confirmation = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130
131 131 password_change = v.StringBoolean(if_missing=False)
132 132 create_repo_group = v.StringBoolean(if_missing=False)
133 133
134 134 active = v.StringBoolean(if_missing=False)
135 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 138 extern_name = v.UnicodeString(strip=True)
139 139 extern_type = v.UnicodeString(strip=True)
140 140 language = v.OneOf(available_languages, hideList=False,
141 141 testValueList=True, if_missing=None)
142 142 chained_validators = [v.ValidPasswordsMatch()]
143 143 return _UserForm
144 144
145 145
146 146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
147 147 old_data = old_data or {}
148 148
149 149 class _UserGroupForm(formencode.Schema):
150 150 allow_extra_fields = True
151 151 filter_extra_fields = True
152 152
153 153 users_group_name = All(
154 154 v.UnicodeString(strip=True, min=1, not_empty=True),
155 155 v.ValidUserGroup(edit, old_data)
156 156 )
157 157 user_group_description = v.UnicodeString(strip=True, min=1,
158 158 not_empty=False)
159 159
160 160 users_group_active = v.StringBoolean(if_missing=False)
161 161
162 162 if edit:
163 163 # this is user group owner
164 164 user = All(
165 165 v.UnicodeString(not_empty=True),
166 166 v.ValidRepoUser(allow_disabled))
167 167 return _UserGroupForm
168 168
169 169
170 170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
171 171 can_create_in_root=False, allow_disabled=False):
172 172 old_data = old_data or {}
173 173 available_groups = available_groups or []
174 174
175 175 class _RepoGroupForm(formencode.Schema):
176 176 allow_extra_fields = True
177 177 filter_extra_fields = False
178 178
179 179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
180 180 v.SlugifyName(),)
181 181 group_description = v.UnicodeString(strip=True, min=1,
182 182 not_empty=False)
183 183 group_copy_permissions = v.StringBoolean(if_missing=False)
184 184
185 185 group_parent_id = v.OneOf(available_groups, hideList=False,
186 186 testValueList=True, not_empty=True)
187 187 enable_locking = v.StringBoolean(if_missing=False)
188 188 chained_validators = [
189 189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
190 190
191 191 if edit:
192 192 # this is repo group owner
193 193 user = All(
194 194 v.UnicodeString(not_empty=True),
195 195 v.ValidRepoUser(allow_disabled))
196 196
197 197 return _RepoGroupForm
198 198
199 199
200 200 def RegisterForm(edit=False, old_data={}):
201 201 class _RegisterForm(formencode.Schema):
202 202 allow_extra_fields = True
203 203 filter_extra_fields = True
204 204 username = All(
205 205 v.ValidUsername(edit, old_data),
206 206 v.UnicodeString(strip=True, min=1, not_empty=True)
207 207 )
208 208 password = All(
209 209 v.ValidPassword(),
210 210 v.UnicodeString(strip=False, min=6, not_empty=True)
211 211 )
212 212 password_confirmation = All(
213 213 v.ValidPassword(),
214 214 v.UnicodeString(strip=False, min=6, not_empty=True)
215 215 )
216 216 active = v.StringBoolean(if_missing=False)
217 217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
219 219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
220 220
221 221 chained_validators = [v.ValidPasswordsMatch()]
222 222
223 223 return _RegisterForm
224 224
225 225
226 226 def PasswordResetForm():
227 227 class _PasswordResetForm(formencode.Schema):
228 228 allow_extra_fields = True
229 229 filter_extra_fields = True
230 230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
231 231 return _PasswordResetForm
232 232
233 233
234 234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
235 235 allow_disabled=False):
236 236 old_data = old_data or {}
237 237 repo_groups = repo_groups or []
238 238 landing_revs = landing_revs or []
239 239 supported_backends = BACKENDS.keys()
240 240
241 241 class _RepoForm(formencode.Schema):
242 242 allow_extra_fields = True
243 243 filter_extra_fields = False
244 244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
245 245 v.SlugifyName(), v.CannotHaveGitSuffix())
246 246 repo_group = All(v.CanWriteGroup(old_data),
247 247 v.OneOf(repo_groups, hideList=True))
248 248 repo_type = v.OneOf(supported_backends, required=False,
249 249 if_missing=old_data.get('repo_type'))
250 250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
251 251 repo_private = v.StringBoolean(if_missing=False)
252 252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
253 253 repo_copy_permissions = v.StringBoolean(if_missing=False)
254 254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
255 255
256 256 repo_enable_statistics = v.StringBoolean(if_missing=False)
257 257 repo_enable_downloads = v.StringBoolean(if_missing=False)
258 258 repo_enable_locking = v.StringBoolean(if_missing=False)
259 259
260 260 if edit:
261 261 # this is repo owner
262 262 user = All(
263 263 v.UnicodeString(not_empty=True),
264 264 v.ValidRepoUser(allow_disabled))
265 265 clone_uri_change = v.UnicodeString(
266 266 not_empty=False, if_missing=v.Missing)
267 267
268 268 chained_validators = [v.ValidCloneUri(),
269 269 v.ValidRepoName(edit, old_data)]
270 270 return _RepoForm
271 271
272 272
273 273 def RepoPermsForm():
274 274 class _RepoPermsForm(formencode.Schema):
275 275 allow_extra_fields = True
276 276 filter_extra_fields = False
277 277 chained_validators = [v.ValidPerms(type_='repo')]
278 278 return _RepoPermsForm
279 279
280 280
281 281 def RepoGroupPermsForm(valid_recursive_choices):
282 282 class _RepoGroupPermsForm(formencode.Schema):
283 283 allow_extra_fields = True
284 284 filter_extra_fields = False
285 285 recursive = v.OneOf(valid_recursive_choices)
286 286 chained_validators = [v.ValidPerms(type_='repo_group')]
287 287 return _RepoGroupPermsForm
288 288
289 289
290 290 def UserGroupPermsForm():
291 291 class _UserPermsForm(formencode.Schema):
292 292 allow_extra_fields = True
293 293 filter_extra_fields = False
294 294 chained_validators = [v.ValidPerms(type_='user_group')]
295 295 return _UserPermsForm
296 296
297 297
298 298 def RepoFieldForm():
299 299 class _RepoFieldForm(formencode.Schema):
300 300 filter_extra_fields = True
301 301 allow_extra_fields = True
302 302
303 303 new_field_key = All(v.FieldKey(),
304 304 v.UnicodeString(strip=True, min=3, not_empty=True))
305 305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
306 306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
307 307 if_missing='str')
308 308 new_field_label = v.UnicodeString(not_empty=False)
309 309 new_field_desc = v.UnicodeString(not_empty=False)
310 310
311 311 return _RepoFieldForm
312 312
313 313
314 314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
315 315 repo_groups=[], landing_revs=[]):
316 316 class _RepoForkForm(formencode.Schema):
317 317 allow_extra_fields = True
318 318 filter_extra_fields = False
319 319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
320 320 v.SlugifyName())
321 321 repo_group = All(v.CanWriteGroup(),
322 322 v.OneOf(repo_groups, hideList=True))
323 323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
324 324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
325 325 private = v.StringBoolean(if_missing=False)
326 326 copy_permissions = v.StringBoolean(if_missing=False)
327 327 fork_parent_id = v.UnicodeString()
328 328 chained_validators = [v.ValidForkName(edit, old_data)]
329 329 landing_rev = v.OneOf(landing_revs, hideList=True)
330 330
331 331 return _RepoForkForm
332 332
333 333
334 334 def ApplicationSettingsForm():
335 335 class _ApplicationSettingsForm(formencode.Schema):
336 336 allow_extra_fields = True
337 337 filter_extra_fields = False
338 338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
339 339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
340 340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
342 342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
344 344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
345 345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
346 346
347 347 return _ApplicationSettingsForm
348 348
349 349
350 350 def ApplicationVisualisationForm():
351 351 class _ApplicationVisualisationForm(formencode.Schema):
352 352 allow_extra_fields = True
353 353 filter_extra_fields = False
354 354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
355 355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
356 356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
357 357
358 358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
359 359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
360 360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
361 361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
362 362 rhodecode_show_version = v.StringBoolean(if_missing=False)
363 363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
364 364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
365 365 rhodecode_gravatar_url = v.UnicodeString(min=3)
366 366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
367 367 rhodecode_support_url = v.UnicodeString()
368 368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
369 369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
370 370
371 371 return _ApplicationVisualisationForm
372 372
373 373
374 374 class _BaseVcsSettingsForm(formencode.Schema):
375 375 allow_extra_fields = True
376 376 filter_extra_fields = False
377 377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
378 378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
379 379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
380 380
381 381 # PR/Code-review
382 382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
383 383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
384 384
385 385 # hg
386 386 extensions_largefiles = v.StringBoolean(if_missing=False)
387 387 extensions_evolve = v.StringBoolean(if_missing=False)
388 388 phases_publish = v.StringBoolean(if_missing=False)
389 389 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
390 390
391 391 # git
392 392 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
393 393
394 394 # svn
395 395 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
396 396 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
397 397
398 398
399 399 def ApplicationUiSettingsForm():
400 400 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
401 401 web_push_ssl = v.StringBoolean(if_missing=False)
402 402 paths_root_path = All(
403 403 v.ValidPath(),
404 404 v.UnicodeString(strip=True, min=1, not_empty=True)
405 405 )
406 406 largefiles_usercache = All(
407 407 v.ValidPath(),
408 408 v.UnicodeString(strip=True, min=2, not_empty=True))
409 409 vcs_git_lfs_store_location = All(
410 410 v.ValidPath(),
411 411 v.UnicodeString(strip=True, min=2, not_empty=True))
412 412 extensions_hgsubversion = v.StringBoolean(if_missing=False)
413 413 extensions_hggit = v.StringBoolean(if_missing=False)
414 414 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
415 415 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
416 416
417 417 return _ApplicationUiSettingsForm
418 418
419 419
420 420 def RepoVcsSettingsForm(repo_name):
421 421 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
422 422 inherit_global_settings = v.StringBoolean(if_missing=False)
423 423 new_svn_branch = v.ValidSvnPattern(
424 424 section='vcs_svn_branch', repo_name=repo_name)
425 425 new_svn_tag = v.ValidSvnPattern(
426 426 section='vcs_svn_tag', repo_name=repo_name)
427 427
428 428 return _RepoVcsSettingsForm
429 429
430 430
431 431 def LabsSettingsForm():
432 432 class _LabSettingsForm(formencode.Schema):
433 433 allow_extra_fields = True
434 434 filter_extra_fields = False
435 435
436 436 return _LabSettingsForm
437 437
438 438
439 439 def ApplicationPermissionsForm(
440 440 register_choices, password_reset_choices, extern_activate_choices):
441 441 class _DefaultPermissionsForm(formencode.Schema):
442 442 allow_extra_fields = True
443 443 filter_extra_fields = True
444 444
445 445 anonymous = v.StringBoolean(if_missing=False)
446 446 default_register = v.OneOf(register_choices)
447 447 default_register_message = v.UnicodeString()
448 448 default_password_reset = v.OneOf(password_reset_choices)
449 449 default_extern_activate = v.OneOf(extern_activate_choices)
450 450
451 451 return _DefaultPermissionsForm
452 452
453 453
454 454 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
455 455 user_group_perms_choices):
456 456 class _ObjectPermissionsForm(formencode.Schema):
457 457 allow_extra_fields = True
458 458 filter_extra_fields = True
459 459 overwrite_default_repo = v.StringBoolean(if_missing=False)
460 460 overwrite_default_group = v.StringBoolean(if_missing=False)
461 461 overwrite_default_user_group = v.StringBoolean(if_missing=False)
462 462 default_repo_perm = v.OneOf(repo_perms_choices)
463 463 default_group_perm = v.OneOf(group_perms_choices)
464 464 default_user_group_perm = v.OneOf(user_group_perms_choices)
465 465
466 466 return _ObjectPermissionsForm
467 467
468 468
469 469 def UserPermissionsForm(create_choices, create_on_write_choices,
470 470 repo_group_create_choices, user_group_create_choices,
471 471 fork_choices, inherit_default_permissions_choices):
472 472 class _DefaultPermissionsForm(formencode.Schema):
473 473 allow_extra_fields = True
474 474 filter_extra_fields = True
475 475
476 476 anonymous = v.StringBoolean(if_missing=False)
477 477
478 478 default_repo_create = v.OneOf(create_choices)
479 479 default_repo_create_on_write = v.OneOf(create_on_write_choices)
480 480 default_user_group_create = v.OneOf(user_group_create_choices)
481 481 default_repo_group_create = v.OneOf(repo_group_create_choices)
482 482 default_fork_create = v.OneOf(fork_choices)
483 483 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
484 484
485 485 return _DefaultPermissionsForm
486 486
487 487
488 488 def UserIndividualPermissionsForm():
489 489 class _DefaultPermissionsForm(formencode.Schema):
490 490 allow_extra_fields = True
491 491 filter_extra_fields = True
492 492
493 493 inherit_default_permissions = v.StringBoolean(if_missing=False)
494 494
495 495 return _DefaultPermissionsForm
496 496
497 497
498 498 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
499 499 class _DefaultsForm(formencode.Schema):
500 500 allow_extra_fields = True
501 501 filter_extra_fields = True
502 502 default_repo_type = v.OneOf(supported_backends)
503 503 default_repo_private = v.StringBoolean(if_missing=False)
504 504 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
505 505 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
506 506 default_repo_enable_locking = v.StringBoolean(if_missing=False)
507 507
508 508 return _DefaultsForm
509 509
510 510
511 511 def AuthSettingsForm():
512 512 class _AuthSettingsForm(formencode.Schema):
513 513 allow_extra_fields = True
514 514 filter_extra_fields = True
515 515 auth_plugins = All(v.ValidAuthPlugins(),
516 516 v.UniqueListFromString()(not_empty=True))
517 517
518 518 return _AuthSettingsForm
519 519
520 520
521 521 def UserExtraEmailForm():
522 522 class _UserExtraEmailForm(formencode.Schema):
523 523 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
524 524 return _UserExtraEmailForm
525 525
526 526
527 527 def UserExtraIpForm():
528 528 class _UserExtraIpForm(formencode.Schema):
529 529 ip = v.ValidIp()(not_empty=True)
530 530 return _UserExtraIpForm
531 531
532 532
533 533
534 534 def PullRequestForm(repo_id):
535 535 class ReviewerForm(formencode.Schema):
536 536 user_id = v.Int(not_empty=True)
537 537 reasons = All()
538 mandatory = v.StringBoolean()
538 539
539 540 class _PullRequestForm(formencode.Schema):
540 541 allow_extra_fields = True
541 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 545 source_repo = v.UnicodeString(strip=True, required=True)
545 546 source_ref = v.UnicodeString(strip=True, required=True)
546 547 target_repo = v.UnicodeString(strip=True, required=True)
547 548 target_ref = v.UnicodeString(strip=True, required=True)
548 549 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 550 v.UniqueList()(not_empty=True))
550 551 review_members = formencode.ForEach(ReviewerForm())
551 552 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553 554
554 555 return _PullRequestForm
555 556
556 557
557 558 def IssueTrackerPatternsForm():
558 559 class _IssueTrackerPatternsForm(formencode.Schema):
559 560 allow_extra_fields = True
560 561 filter_extra_fields = False
561 562 chained_validators = [v.ValidPattern()]
562 563 return _IssueTrackerPatternsForm
@@ -1,1471 +1,1499 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 34 from sqlalchemy import or_
35 35
36 36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 37 from rhodecode.lib.compat import OrderedDict
38 38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 39 from rhodecode.lib.markup_renderer import (
40 40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 41 from rhodecode.lib.utils import action_logger
42 42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 CommitDoesNotExistError, EmptyRepositoryError)
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 50 from rhodecode.model.comment import CommentsModel
51 51 from rhodecode.model.db import (
52 52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 53 PullRequestVersion, ChangesetComment, Repository)
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.notification import NotificationModel, \
56 56 EmailNotificationModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.settings import VcsSettingsModel
59 59
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 # Data structure to hold the response data when updating commits during a pull
65 65 # request update.
66 66 UpdateResponse = namedtuple('UpdateResponse', [
67 67 'executed', 'reason', 'new', 'old', 'changes',
68 68 'source_changed', 'target_changed'])
69 69
70 70
71 71 class PullRequestModel(BaseModel):
72 72
73 73 cls = PullRequest
74 74
75 75 DIFF_CONTEXT = 3
76 76
77 77 MERGE_STATUS_MESSAGES = {
78 78 MergeFailureReason.NONE: lazy_ugettext(
79 79 'This pull request can be automatically merged.'),
80 80 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 81 'This pull request cannot be merged because of an unhandled'
82 82 ' exception.'),
83 83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 84 'This pull request cannot be merged because of merge conflicts.'),
85 85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 86 'This pull request could not be merged because push to target'
87 87 ' failed.'),
88 88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 89 'This pull request cannot be merged because the target is not a'
90 90 ' head.'),
91 91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 92 'This pull request cannot be merged because the source contains'
93 93 ' more branches than the target.'),
94 94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 95 'This pull request cannot be merged because the target has'
96 96 ' multiple heads.'),
97 97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 98 'This pull request cannot be merged because the target repository'
99 99 ' is locked.'),
100 100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 101 'This pull request cannot be merged because the target or the '
102 102 'source reference is missing.'),
103 103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 104 'This pull request cannot be merged because the target '
105 105 'reference is missing.'),
106 106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 107 'This pull request cannot be merged because the source '
108 108 'reference is missing.'),
109 109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 110 'This pull request cannot be merged because of conflicts related '
111 111 'to sub repositories.'),
112 112 }
113 113
114 114 UPDATE_STATUS_MESSAGES = {
115 115 UpdateFailureReason.NONE: lazy_ugettext(
116 116 'Pull request update successful.'),
117 117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 118 'Pull request update failed because of an unknown error.'),
119 119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 120 'No update needed because the source and target have not changed.'),
121 121 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 122 'Pull request cannot be updated because the reference type is '
123 123 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 125 'This pull request cannot be updated because the target '
126 126 'reference is missing.'),
127 127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 128 'This pull request cannot be updated because the source '
129 129 'reference is missing.'),
130 130 }
131 131
132 132 def __get_pull_request(self, pull_request):
133 133 return self._get_instance((
134 134 PullRequest, PullRequestVersion), pull_request)
135 135
136 136 def _check_perms(self, perms, pull_request, user, api=False):
137 137 if not api:
138 138 return h.HasRepoPermissionAny(*perms)(
139 139 user=user, repo_name=pull_request.target_repo.repo_name)
140 140 else:
141 141 return h.HasRepoPermissionAnyApi(*perms)(
142 142 user=user, repo_name=pull_request.target_repo.repo_name)
143 143
144 144 def check_user_read(self, pull_request, user, api=False):
145 145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 146 return self._check_perms(_perms, pull_request, user, api)
147 147
148 148 def check_user_merge(self, pull_request, user, api=False):
149 149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 150 return self._check_perms(_perms, pull_request, user, api)
151 151
152 152 def check_user_update(self, pull_request, user, api=False):
153 153 owner = user.user_id == pull_request.user_id
154 154 return self.check_user_merge(pull_request, user, api) or owner
155 155
156 156 def check_user_delete(self, pull_request, user):
157 157 owner = user.user_id == pull_request.user_id
158 158 _perms = ('repository.admin',)
159 159 return self._check_perms(_perms, pull_request, user) or owner
160 160
161 161 def check_user_change_status(self, pull_request, user, api=False):
162 162 reviewer = user.user_id in [x.user_id for x in
163 163 pull_request.reviewers]
164 164 return self.check_user_update(pull_request, user, api) or reviewer
165 165
166 166 def get(self, pull_request):
167 167 return self.__get_pull_request(pull_request)
168 168
169 169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 170 opened_by=None, order_by=None,
171 171 order_dir='desc'):
172 172 repo = None
173 173 if repo_name:
174 174 repo = self._get_repo(repo_name)
175 175
176 176 q = PullRequest.query()
177 177
178 178 # source or target
179 179 if repo and source:
180 180 q = q.filter(PullRequest.source_repo == repo)
181 181 elif repo:
182 182 q = q.filter(PullRequest.target_repo == repo)
183 183
184 184 # closed,opened
185 185 if statuses:
186 186 q = q.filter(PullRequest.status.in_(statuses))
187 187
188 188 # opened by filter
189 189 if opened_by:
190 190 q = q.filter(PullRequest.user_id.in_(opened_by))
191 191
192 192 if order_by:
193 193 order_map = {
194 194 'name_raw': PullRequest.pull_request_id,
195 195 'title': PullRequest.title,
196 196 'updated_on_raw': PullRequest.updated_on,
197 197 'target_repo': PullRequest.target_repo_id
198 198 }
199 199 if order_dir == 'asc':
200 200 q = q.order_by(order_map[order_by].asc())
201 201 else:
202 202 q = q.order_by(order_map[order_by].desc())
203 203
204 204 return q
205 205
206 206 def count_all(self, repo_name, source=False, statuses=None,
207 207 opened_by=None):
208 208 """
209 209 Count the number of pull requests for a specific repository.
210 210
211 211 :param repo_name: target or source repo
212 212 :param source: boolean flag to specify if repo_name refers to source
213 213 :param statuses: list of pull request statuses
214 214 :param opened_by: author user of the pull request
215 215 :returns: int number of pull requests
216 216 """
217 217 q = self._prepare_get_all_query(
218 218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219 219
220 220 return q.count()
221 221
222 222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 223 offset=0, length=None, order_by=None, order_dir='desc'):
224 224 """
225 225 Get all pull requests for a specific repository.
226 226
227 227 :param repo_name: target or source repo
228 228 :param source: boolean flag to specify if repo_name refers to source
229 229 :param statuses: list of pull request statuses
230 230 :param opened_by: author user of the pull request
231 231 :param offset: pagination offset
232 232 :param length: length of returned list
233 233 :param order_by: order of the returned list
234 234 :param order_dir: 'asc' or 'desc' ordering direction
235 235 :returns: list of pull requests
236 236 """
237 237 q = self._prepare_get_all_query(
238 238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 239 order_by=order_by, order_dir=order_dir)
240 240
241 241 if length:
242 242 pull_requests = q.limit(length).offset(offset).all()
243 243 else:
244 244 pull_requests = q.all()
245 245
246 246 return pull_requests
247 247
248 248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 249 opened_by=None):
250 250 """
251 251 Count the number of pull requests for a specific repository that are
252 252 awaiting review.
253 253
254 254 :param repo_name: target or source repo
255 255 :param source: boolean flag to specify if repo_name refers to source
256 256 :param statuses: list of pull request statuses
257 257 :param opened_by: author user of the pull request
258 258 :returns: int number of pull requests
259 259 """
260 260 pull_requests = self.get_awaiting_review(
261 261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262 262
263 263 return len(pull_requests)
264 264
265 265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 266 opened_by=None, offset=0, length=None,
267 267 order_by=None, order_dir='desc'):
268 268 """
269 269 Get all pull requests for a specific repository that are awaiting
270 270 review.
271 271
272 272 :param repo_name: target or source repo
273 273 :param source: boolean flag to specify if repo_name refers to source
274 274 :param statuses: list of pull request statuses
275 275 :param opened_by: author user of the pull request
276 276 :param offset: pagination offset
277 277 :param length: length of returned list
278 278 :param order_by: order of the returned list
279 279 :param order_dir: 'asc' or 'desc' ordering direction
280 280 :returns: list of pull requests
281 281 """
282 282 pull_requests = self.get_all(
283 283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 284 order_by=order_by, order_dir=order_dir)
285 285
286 286 _filtered_pull_requests = []
287 287 for pr in pull_requests:
288 288 status = pr.calculated_review_status()
289 289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 291 _filtered_pull_requests.append(pr)
292 292 if length:
293 293 return _filtered_pull_requests[offset:offset+length]
294 294 else:
295 295 return _filtered_pull_requests
296 296
297 297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 298 opened_by=None, user_id=None):
299 299 """
300 300 Count the number of pull requests for a specific repository that are
301 301 awaiting review from a specific user.
302 302
303 303 :param repo_name: target or source repo
304 304 :param source: boolean flag to specify if repo_name refers to source
305 305 :param statuses: list of pull request statuses
306 306 :param opened_by: author user of the pull request
307 307 :param user_id: reviewer user of the pull request
308 308 :returns: int number of pull requests
309 309 """
310 310 pull_requests = self.get_awaiting_my_review(
311 311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 312 user_id=user_id)
313 313
314 314 return len(pull_requests)
315 315
316 316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 317 opened_by=None, user_id=None, offset=0,
318 318 length=None, order_by=None, order_dir='desc'):
319 319 """
320 320 Get all pull requests for a specific repository that are awaiting
321 321 review from a specific user.
322 322
323 323 :param repo_name: target or source repo
324 324 :param source: boolean flag to specify if repo_name refers to source
325 325 :param statuses: list of pull request statuses
326 326 :param opened_by: author user of the pull request
327 327 :param user_id: reviewer user of the pull request
328 328 :param offset: pagination offset
329 329 :param length: length of returned list
330 330 :param order_by: order of the returned list
331 331 :param order_dir: 'asc' or 'desc' ordering direction
332 332 :returns: list of pull requests
333 333 """
334 334 pull_requests = self.get_all(
335 335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 336 order_by=order_by, order_dir=order_dir)
337 337
338 338 _my = PullRequestModel().get_not_reviewed(user_id)
339 339 my_participation = []
340 340 for pr in pull_requests:
341 341 if pr in _my:
342 342 my_participation.append(pr)
343 343 _filtered_pull_requests = my_participation
344 344 if length:
345 345 return _filtered_pull_requests[offset:offset+length]
346 346 else:
347 347 return _filtered_pull_requests
348 348
349 349 def get_not_reviewed(self, user_id):
350 350 return [
351 351 x.pull_request for x in PullRequestReviewers.query().filter(
352 352 PullRequestReviewers.user_id == user_id).all()
353 353 ]
354 354
355 355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 356 order_by=None, order_dir='desc'):
357 357 q = PullRequest.query()
358 358 if user_id:
359 359 reviewers_subquery = Session().query(
360 360 PullRequestReviewers.pull_request_id).filter(
361 361 PullRequestReviewers.user_id == user_id).subquery()
362 362 user_filter= or_(
363 363 PullRequest.user_id == user_id,
364 364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 365 )
366 366 q = PullRequest.query().filter(user_filter)
367 367
368 368 # closed,opened
369 369 if statuses:
370 370 q = q.filter(PullRequest.status.in_(statuses))
371 371
372 372 if order_by:
373 373 order_map = {
374 374 'name_raw': PullRequest.pull_request_id,
375 375 'title': PullRequest.title,
376 376 'updated_on_raw': PullRequest.updated_on,
377 377 'target_repo': PullRequest.target_repo_id
378 378 }
379 379 if order_dir == 'asc':
380 380 q = q.order_by(order_map[order_by].asc())
381 381 else:
382 382 q = q.order_by(order_map[order_by].desc())
383 383
384 384 return q
385 385
386 386 def count_im_participating_in(self, user_id=None, statuses=None):
387 387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 388 return q.count()
389 389
390 390 def get_im_participating_in(
391 391 self, user_id=None, statuses=None, offset=0,
392 392 length=None, order_by=None, order_dir='desc'):
393 393 """
394 394 Get all Pull requests that i'm participating in, or i have opened
395 395 """
396 396
397 397 q = self._prepare_participating_query(
398 398 user_id, statuses=statuses, order_by=order_by,
399 399 order_dir=order_dir)
400 400
401 401 if length:
402 402 pull_requests = q.limit(length).offset(offset).all()
403 403 else:
404 404 pull_requests = q.all()
405 405
406 406 return pull_requests
407 407
408 408 def get_versions(self, pull_request):
409 409 """
410 410 returns version of pull request sorted by ID descending
411 411 """
412 412 return PullRequestVersion.query()\
413 413 .filter(PullRequestVersion.pull_request == pull_request)\
414 414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 415 .all()
416 416
417 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 421 created_by_user = self._get_user(created_by)
420 422 source_repo = self._get_repo(source_repo)
421 423 target_repo = self._get_repo(target_repo)
422 424
423 425 pull_request = PullRequest()
424 426 pull_request.source_repo = source_repo
425 427 pull_request.source_ref = source_ref
426 428 pull_request.target_repo = target_repo
427 429 pull_request.target_ref = target_ref
428 430 pull_request.revisions = revisions
429 431 pull_request.title = title
430 432 pull_request.description = description
431 433 pull_request.author = created_by_user
434 pull_request.reviewer_data = reviewer_data
432 435
433 436 Session().add(pull_request)
434 437 Session().flush()
435 438
436 439 reviewer_ids = set()
437 440 # members / reviewers
438 441 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons, mandatory = reviewer_object
443 443
444 444 user = self._get_user(user_id)
445 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 452 Session().add(reviewer)
449 453
450 454 # Set approval status to "Under Review" for all commits which are
451 455 # part of this pull request.
452 456 ChangesetStatusModel().set_status(
453 457 repo=target_repo,
454 458 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 459 user=created_by_user,
456 460 pull_request=pull_request
457 461 )
458 462
459 463 self.notify_reviewers(pull_request, reviewer_ids)
460 464 self._trigger_pull_request_hook(
461 465 pull_request, created_by_user, 'create')
462 466
463 467 return pull_request
464 468
465 469 def _trigger_pull_request_hook(self, pull_request, user, action):
466 470 pull_request = self.__get_pull_request(pull_request)
467 471 target_scm = pull_request.target_repo.scm_instance()
468 472 if action == 'create':
469 473 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 474 elif action == 'merge':
471 475 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 476 elif action == 'close':
473 477 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 478 elif action == 'review_status_change':
475 479 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 480 elif action == 'update':
477 481 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 482 else:
479 483 return
480 484
481 485 trigger_hook(
482 486 username=user.username,
483 487 repo_name=pull_request.target_repo.repo_name,
484 488 repo_alias=target_scm.alias,
485 489 pull_request=pull_request)
486 490
487 491 def _get_commit_ids(self, pull_request):
488 492 """
489 493 Return the commit ids of the merged pull request.
490 494
491 495 This method is not dealing correctly yet with the lack of autoupdates
492 496 nor with the implicit target updates.
493 497 For example: if a commit in the source repo is already in the target it
494 498 will be reported anyways.
495 499 """
496 500 merge_rev = pull_request.merge_rev
497 501 if merge_rev is None:
498 502 raise ValueError('This pull request was not merged yet')
499 503
500 504 commit_ids = list(pull_request.revisions)
501 505 if merge_rev not in commit_ids:
502 506 commit_ids.append(merge_rev)
503 507
504 508 return commit_ids
505 509
506 510 def merge(self, pull_request, user, extras):
507 511 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 512 merge_state = self._merge_pull_request(pull_request, user, extras)
509 513 if merge_state.executed:
510 514 log.debug(
511 515 "Merge was successful, updating the pull request comments.")
512 516 self._comment_and_close_pr(pull_request, user, merge_state)
513 517 self._log_action('user_merged_pull_request', user, pull_request)
514 518 else:
515 519 log.warn("Merge failed, not updating the pull request.")
516 520 return merge_state
517 521
518 522 def _merge_pull_request(self, pull_request, user, extras):
519 523 target_vcs = pull_request.target_repo.scm_instance()
520 524 source_vcs = pull_request.source_repo.scm_instance()
521 525 target_ref = self._refresh_reference(
522 526 pull_request.target_ref_parts, target_vcs)
523 527
524 528 message = _(
525 529 'Merge pull request #%(pr_id)s from '
526 530 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 531 'pr_id': pull_request.pull_request_id,
528 532 'source_repo': source_vcs.name,
529 533 'source_ref_name': pull_request.source_ref_parts.name,
530 534 'pr_title': pull_request.title
531 535 }
532 536
533 537 workspace_id = self._workspace_id(pull_request)
534 538 use_rebase = self._use_rebase_for_merging(pull_request)
535 539
536 540 callback_daemon, extras = prepare_callback_daemon(
537 541 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 542 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539 543
540 544 with callback_daemon:
541 545 # TODO: johbo: Implement a clean way to run a config_override
542 546 # for a single call.
543 547 target_vcs.config.set(
544 548 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 549 merge_state = target_vcs.merge(
546 550 target_ref, source_vcs, pull_request.source_ref_parts,
547 551 workspace_id, user_name=user.username,
548 552 user_email=user.email, message=message, use_rebase=use_rebase)
549 553 return merge_state
550 554
551 555 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 556 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 557 pull_request.updated_on = datetime.datetime.now()
554 558
555 559 CommentsModel().create(
556 560 text=unicode(_('Pull request merged and closed')),
557 561 repo=pull_request.target_repo.repo_id,
558 562 user=user.user_id,
559 563 pull_request=pull_request.pull_request_id,
560 564 f_path=None,
561 565 line_no=None,
562 566 closing_pr=True
563 567 )
564 568
565 569 Session().add(pull_request)
566 570 Session().flush()
567 571 # TODO: paris: replace invalidation with less radical solution
568 572 ScmModel().mark_for_invalidation(
569 573 pull_request.target_repo.repo_name)
570 574 self._trigger_pull_request_hook(pull_request, user, 'merge')
571 575
572 576 def has_valid_update_type(self, pull_request):
573 577 source_ref_type = pull_request.source_ref_parts.type
574 578 return source_ref_type in ['book', 'branch', 'tag']
575 579
576 580 def update_commits(self, pull_request):
577 581 """
578 582 Get the updated list of commits for the pull request
579 583 and return the new pull request version and the list
580 584 of commits processed by this update action
581 585 """
582 586 pull_request = self.__get_pull_request(pull_request)
583 587 source_ref_type = pull_request.source_ref_parts.type
584 588 source_ref_name = pull_request.source_ref_parts.name
585 589 source_ref_id = pull_request.source_ref_parts.commit_id
586 590
587 591 target_ref_type = pull_request.target_ref_parts.type
588 592 target_ref_name = pull_request.target_ref_parts.name
589 593 target_ref_id = pull_request.target_ref_parts.commit_id
590 594
591 595 if not self.has_valid_update_type(pull_request):
592 596 log.debug(
593 597 "Skipping update of pull request %s due to ref type: %s",
594 598 pull_request, source_ref_type)
595 599 return UpdateResponse(
596 600 executed=False,
597 601 reason=UpdateFailureReason.WRONG_REF_TYPE,
598 602 old=pull_request, new=None, changes=None,
599 603 source_changed=False, target_changed=False)
600 604
601 605 # source repo
602 606 source_repo = pull_request.source_repo.scm_instance()
603 607 try:
604 608 source_commit = source_repo.get_commit(commit_id=source_ref_name)
605 609 except CommitDoesNotExistError:
606 610 return UpdateResponse(
607 611 executed=False,
608 612 reason=UpdateFailureReason.MISSING_SOURCE_REF,
609 613 old=pull_request, new=None, changes=None,
610 614 source_changed=False, target_changed=False)
611 615
612 616 source_changed = source_ref_id != source_commit.raw_id
613 617
614 618 # target repo
615 619 target_repo = pull_request.target_repo.scm_instance()
616 620 try:
617 621 target_commit = target_repo.get_commit(commit_id=target_ref_name)
618 622 except CommitDoesNotExistError:
619 623 return UpdateResponse(
620 624 executed=False,
621 625 reason=UpdateFailureReason.MISSING_TARGET_REF,
622 626 old=pull_request, new=None, changes=None,
623 627 source_changed=False, target_changed=False)
624 628 target_changed = target_ref_id != target_commit.raw_id
625 629
626 630 if not (source_changed or target_changed):
627 631 log.debug("Nothing changed in pull request %s", pull_request)
628 632 return UpdateResponse(
629 633 executed=False,
630 634 reason=UpdateFailureReason.NO_CHANGE,
631 635 old=pull_request, new=None, changes=None,
632 636 source_changed=target_changed, target_changed=source_changed)
633 637
634 638 change_in_found = 'target repo' if target_changed else 'source repo'
635 639 log.debug('Updating pull request because of change in %s detected',
636 640 change_in_found)
637 641
638 642 # Finally there is a need for an update, in case of source change
639 643 # we create a new version, else just an update
640 644 if source_changed:
641 645 pull_request_version = self._create_version_from_snapshot(pull_request)
642 646 self._link_comments_to_version(pull_request_version)
643 647 else:
644 648 try:
645 649 ver = pull_request.versions[-1]
646 650 except IndexError:
647 651 ver = None
648 652
649 653 pull_request.pull_request_version_id = \
650 654 ver.pull_request_version_id if ver else None
651 655 pull_request_version = pull_request
652 656
653 657 try:
654 658 if target_ref_type in ('tag', 'branch', 'book'):
655 659 target_commit = target_repo.get_commit(target_ref_name)
656 660 else:
657 661 target_commit = target_repo.get_commit(target_ref_id)
658 662 except CommitDoesNotExistError:
659 663 return UpdateResponse(
660 664 executed=False,
661 665 reason=UpdateFailureReason.MISSING_TARGET_REF,
662 666 old=pull_request, new=None, changes=None,
663 667 source_changed=source_changed, target_changed=target_changed)
664 668
665 669 # re-compute commit ids
666 670 old_commit_ids = pull_request.revisions
667 671 pre_load = ["author", "branch", "date", "message"]
668 672 commit_ranges = target_repo.compare(
669 673 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
670 674 pre_load=pre_load)
671 675
672 676 ancestor = target_repo.get_common_ancestor(
673 677 target_commit.raw_id, source_commit.raw_id, source_repo)
674 678
675 679 pull_request.source_ref = '%s:%s:%s' % (
676 680 source_ref_type, source_ref_name, source_commit.raw_id)
677 681 pull_request.target_ref = '%s:%s:%s' % (
678 682 target_ref_type, target_ref_name, ancestor)
679 683
680 684 pull_request.revisions = [
681 685 commit.raw_id for commit in reversed(commit_ranges)]
682 686 pull_request.updated_on = datetime.datetime.now()
683 687 Session().add(pull_request)
684 688 new_commit_ids = pull_request.revisions
685 689
686 690 old_diff_data, new_diff_data = self._generate_update_diffs(
687 691 pull_request, pull_request_version)
688 692
689 693 # calculate commit and file changes
690 694 changes = self._calculate_commit_id_changes(
691 695 old_commit_ids, new_commit_ids)
692 696 file_changes = self._calculate_file_changes(
693 697 old_diff_data, new_diff_data)
694 698
695 699 # set comments as outdated if DIFFS changed
696 700 CommentsModel().outdate_comments(
697 701 pull_request, old_diff_data=old_diff_data,
698 702 new_diff_data=new_diff_data)
699 703
700 704 commit_changes = (changes.added or changes.removed)
701 705 file_node_changes = (
702 706 file_changes.added or file_changes.modified or file_changes.removed)
703 707 pr_has_changes = commit_changes or file_node_changes
704 708
705 709 # Add an automatic comment to the pull request, in case
706 710 # anything has changed
707 711 if pr_has_changes:
708 712 update_comment = CommentsModel().create(
709 713 text=self._render_update_message(changes, file_changes),
710 714 repo=pull_request.target_repo,
711 715 user=pull_request.author,
712 716 pull_request=pull_request,
713 717 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
714 718
715 719 # Update status to "Under Review" for added commits
716 720 for commit_id in changes.added:
717 721 ChangesetStatusModel().set_status(
718 722 repo=pull_request.source_repo,
719 723 status=ChangesetStatus.STATUS_UNDER_REVIEW,
720 724 comment=update_comment,
721 725 user=pull_request.author,
722 726 pull_request=pull_request,
723 727 revision=commit_id)
724 728
725 729 log.debug(
726 730 'Updated pull request %s, added_ids: %s, common_ids: %s, '
727 731 'removed_ids: %s', pull_request.pull_request_id,
728 732 changes.added, changes.common, changes.removed)
729 733 log.debug(
730 734 'Updated pull request with the following file changes: %s',
731 735 file_changes)
732 736
733 737 log.info(
734 738 "Updated pull request %s from commit %s to commit %s, "
735 739 "stored new version %s of this pull request.",
736 740 pull_request.pull_request_id, source_ref_id,
737 741 pull_request.source_ref_parts.commit_id,
738 742 pull_request_version.pull_request_version_id)
739 743 Session().commit()
740 744 self._trigger_pull_request_hook(
741 745 pull_request, pull_request.author, 'update')
742 746
743 747 return UpdateResponse(
744 748 executed=True, reason=UpdateFailureReason.NONE,
745 749 old=pull_request, new=pull_request_version, changes=changes,
746 750 source_changed=source_changed, target_changed=target_changed)
747 751
748 752 def _create_version_from_snapshot(self, pull_request):
749 753 version = PullRequestVersion()
750 754 version.title = pull_request.title
751 755 version.description = pull_request.description
752 756 version.status = pull_request.status
753 757 version.created_on = datetime.datetime.now()
754 758 version.updated_on = pull_request.updated_on
755 759 version.user_id = pull_request.user_id
756 760 version.source_repo = pull_request.source_repo
757 761 version.source_ref = pull_request.source_ref
758 762 version.target_repo = pull_request.target_repo
759 763 version.target_ref = pull_request.target_ref
760 764
761 765 version._last_merge_source_rev = pull_request._last_merge_source_rev
762 766 version._last_merge_target_rev = pull_request._last_merge_target_rev
763 767 version._last_merge_status = pull_request._last_merge_status
764 768 version.shadow_merge_ref = pull_request.shadow_merge_ref
765 769 version.merge_rev = pull_request.merge_rev
770 version.reviewer_data = pull_request.reviewer_data
766 771
767 772 version.revisions = pull_request.revisions
768 773 version.pull_request = pull_request
769 774 Session().add(version)
770 775 Session().flush()
771 776
772 777 return version
773 778
774 779 def _generate_update_diffs(self, pull_request, pull_request_version):
775 780
776 781 diff_context = (
777 782 self.DIFF_CONTEXT +
778 783 CommentsModel.needed_extra_diff_context())
779 784
780 785 source_repo = pull_request_version.source_repo
781 786 source_ref_id = pull_request_version.source_ref_parts.commit_id
782 787 target_ref_id = pull_request_version.target_ref_parts.commit_id
783 788 old_diff = self._get_diff_from_pr_or_version(
784 789 source_repo, source_ref_id, target_ref_id, context=diff_context)
785 790
786 791 source_repo = pull_request.source_repo
787 792 source_ref_id = pull_request.source_ref_parts.commit_id
788 793 target_ref_id = pull_request.target_ref_parts.commit_id
789 794
790 795 new_diff = self._get_diff_from_pr_or_version(
791 796 source_repo, source_ref_id, target_ref_id, context=diff_context)
792 797
793 798 old_diff_data = diffs.DiffProcessor(old_diff)
794 799 old_diff_data.prepare()
795 800 new_diff_data = diffs.DiffProcessor(new_diff)
796 801 new_diff_data.prepare()
797 802
798 803 return old_diff_data, new_diff_data
799 804
800 805 def _link_comments_to_version(self, pull_request_version):
801 806 """
802 807 Link all unlinked comments of this pull request to the given version.
803 808
804 809 :param pull_request_version: The `PullRequestVersion` to which
805 810 the comments shall be linked.
806 811
807 812 """
808 813 pull_request = pull_request_version.pull_request
809 814 comments = ChangesetComment.query()\
810 815 .filter(
811 816 # TODO: johbo: Should we query for the repo at all here?
812 817 # Pending decision on how comments of PRs are to be related
813 818 # to either the source repo, the target repo or no repo at all.
814 819 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
815 820 ChangesetComment.pull_request == pull_request,
816 821 ChangesetComment.pull_request_version == None)\
817 822 .order_by(ChangesetComment.comment_id.asc())
818 823
819 824 # TODO: johbo: Find out why this breaks if it is done in a bulk
820 825 # operation.
821 826 for comment in comments:
822 827 comment.pull_request_version_id = (
823 828 pull_request_version.pull_request_version_id)
824 829 Session().add(comment)
825 830
826 831 def _calculate_commit_id_changes(self, old_ids, new_ids):
827 832 added = [x for x in new_ids if x not in old_ids]
828 833 common = [x for x in new_ids if x in old_ids]
829 834 removed = [x for x in old_ids if x not in new_ids]
830 835 total = new_ids
831 836 return ChangeTuple(added, common, removed, total)
832 837
833 838 def _calculate_file_changes(self, old_diff_data, new_diff_data):
834 839
835 840 old_files = OrderedDict()
836 841 for diff_data in old_diff_data.parsed_diff:
837 842 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
838 843
839 844 added_files = []
840 845 modified_files = []
841 846 removed_files = []
842 847 for diff_data in new_diff_data.parsed_diff:
843 848 new_filename = diff_data['filename']
844 849 new_hash = md5_safe(diff_data['raw_diff'])
845 850
846 851 old_hash = old_files.get(new_filename)
847 852 if not old_hash:
848 853 # file is not present in old diff, means it's added
849 854 added_files.append(new_filename)
850 855 else:
851 856 if new_hash != old_hash:
852 857 modified_files.append(new_filename)
853 858 # now remove a file from old, since we have seen it already
854 859 del old_files[new_filename]
855 860
856 861 # removed files is when there are present in old, but not in NEW,
857 862 # since we remove old files that are present in new diff, left-overs
858 863 # if any should be the removed files
859 864 removed_files.extend(old_files.keys())
860 865
861 866 return FileChangeTuple(added_files, modified_files, removed_files)
862 867
863 868 def _render_update_message(self, changes, file_changes):
864 869 """
865 870 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
866 871 so it's always looking the same disregarding on which default
867 872 renderer system is using.
868 873
869 874 :param changes: changes named tuple
870 875 :param file_changes: file changes named tuple
871 876
872 877 """
873 878 new_status = ChangesetStatus.get_status_lbl(
874 879 ChangesetStatus.STATUS_UNDER_REVIEW)
875 880
876 881 changed_files = (
877 882 file_changes.added + file_changes.modified + file_changes.removed)
878 883
879 884 params = {
880 885 'under_review_label': new_status,
881 886 'added_commits': changes.added,
882 887 'removed_commits': changes.removed,
883 888 'changed_files': changed_files,
884 889 'added_files': file_changes.added,
885 890 'modified_files': file_changes.modified,
886 891 'removed_files': file_changes.removed,
887 892 }
888 893 renderer = RstTemplateRenderer()
889 894 return renderer.render('pull_request_update.mako', **params)
890 895
891 896 def edit(self, pull_request, title, description):
892 897 pull_request = self.__get_pull_request(pull_request)
893 898 if pull_request.is_closed():
894 899 raise ValueError('This pull request is closed')
895 900 if title:
896 901 pull_request.title = title
897 902 pull_request.description = description
898 903 pull_request.updated_on = datetime.datetime.now()
899 904 Session().add(pull_request)
900 905
901 906 def update_reviewers(self, pull_request, reviewer_data):
902 907 """
903 908 Update the reviewers in the pull request
904 909
905 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 = {}
910 for user_id, reasons in reviewer_data:
915 reviewers = {}
916 for user_id, reasons, mandatory in reviewer_data:
911 917 if isinstance(user_id, (int, basestring)):
912 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 923 pull_request = self.__get_pull_request(pull_request)
917 924 current_reviewers = PullRequestReviewers.query()\
918 925 .filter(PullRequestReviewers.pull_request ==
919 926 pull_request).all()
920 927 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
921 928
922 929 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
923 930 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
924 931
925 932 log.debug("Adding %s reviewers", ids_to_add)
926 933 log.debug("Removing %s reviewers", ids_to_remove)
927 934 changed = False
928 935 for uid in ids_to_add:
929 936 changed = True
930 937 _usr = self._get_user(uid)
931 reasons = reviewers_reasons[uid]
932 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
938 reviewer = PullRequestReviewers()
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 944 Session().add(reviewer)
934 945
935 946 for uid in ids_to_remove:
936 947 changed = True
937 948 reviewers = PullRequestReviewers.query()\
938 949 .filter(PullRequestReviewers.user_id == uid,
939 950 PullRequestReviewers.pull_request == pull_request)\
940 951 .all()
941 952 # use .all() in case we accidentally added the same person twice
942 953 # this CAN happen due to the lack of DB checks
943 954 for obj in reviewers:
944 955 Session().delete(obj)
945 956
946 957 if changed:
947 958 pull_request.updated_on = datetime.datetime.now()
948 959 Session().add(pull_request)
949 960
950 961 self.notify_reviewers(pull_request, ids_to_add)
951 962 return ids_to_add, ids_to_remove
952 963
953 964 def get_url(self, pull_request):
954 965 return h.url('pullrequest_show',
955 966 repo_name=safe_str(pull_request.target_repo.repo_name),
956 967 pull_request_id=pull_request.pull_request_id,
957 968 qualified=True)
958 969
959 970 def get_shadow_clone_url(self, pull_request):
960 971 """
961 972 Returns qualified url pointing to the shadow repository. If this pull
962 973 request is closed there is no shadow repository and ``None`` will be
963 974 returned.
964 975 """
965 976 if pull_request.is_closed():
966 977 return None
967 978 else:
968 979 pr_url = urllib.unquote(self.get_url(pull_request))
969 980 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
970 981
971 982 def notify_reviewers(self, pull_request, reviewers_ids):
972 983 # notification to reviewers
973 984 if not reviewers_ids:
974 985 return
975 986
976 987 pull_request_obj = pull_request
977 988 # get the current participants of this pull request
978 989 recipients = reviewers_ids
979 990 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
980 991
981 992 pr_source_repo = pull_request_obj.source_repo
982 993 pr_target_repo = pull_request_obj.target_repo
983 994
984 995 pr_url = h.url(
985 996 'pullrequest_show',
986 997 repo_name=pr_target_repo.repo_name,
987 998 pull_request_id=pull_request_obj.pull_request_id,
988 999 qualified=True,)
989 1000
990 1001 # set some variables for email notification
991 1002 pr_target_repo_url = h.url(
992 1003 'summary_home',
993 1004 repo_name=pr_target_repo.repo_name,
994 1005 qualified=True)
995 1006
996 1007 pr_source_repo_url = h.url(
997 1008 'summary_home',
998 1009 repo_name=pr_source_repo.repo_name,
999 1010 qualified=True)
1000 1011
1001 1012 # pull request specifics
1002 1013 pull_request_commits = [
1003 1014 (x.raw_id, x.message)
1004 1015 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1005 1016
1006 1017 kwargs = {
1007 1018 'user': pull_request.author,
1008 1019 'pull_request': pull_request_obj,
1009 1020 'pull_request_commits': pull_request_commits,
1010 1021
1011 1022 'pull_request_target_repo': pr_target_repo,
1012 1023 'pull_request_target_repo_url': pr_target_repo_url,
1013 1024
1014 1025 'pull_request_source_repo': pr_source_repo,
1015 1026 'pull_request_source_repo_url': pr_source_repo_url,
1016 1027
1017 1028 'pull_request_url': pr_url,
1018 1029 }
1019 1030
1020 1031 # pre-generate the subject for notification itself
1021 1032 (subject,
1022 1033 _h, _e, # we don't care about those
1023 1034 body_plaintext) = EmailNotificationModel().render_email(
1024 1035 notification_type, **kwargs)
1025 1036
1026 1037 # create notification objects, and emails
1027 1038 NotificationModel().create(
1028 1039 created_by=pull_request.author,
1029 1040 notification_subject=subject,
1030 1041 notification_body=body_plaintext,
1031 1042 notification_type=notification_type,
1032 1043 recipients=recipients,
1033 1044 email_kwargs=kwargs,
1034 1045 )
1035 1046
1036 1047 def delete(self, pull_request):
1037 1048 pull_request = self.__get_pull_request(pull_request)
1038 1049 self._cleanup_merge_workspace(pull_request)
1039 1050 Session().delete(pull_request)
1040 1051
1041 1052 def close_pull_request(self, pull_request, user):
1042 1053 pull_request = self.__get_pull_request(pull_request)
1043 1054 self._cleanup_merge_workspace(pull_request)
1044 1055 pull_request.status = PullRequest.STATUS_CLOSED
1045 1056 pull_request.updated_on = datetime.datetime.now()
1046 1057 Session().add(pull_request)
1047 1058 self._trigger_pull_request_hook(
1048 1059 pull_request, pull_request.author, 'close')
1049 1060 self._log_action('user_closed_pull_request', user, pull_request)
1050 1061
1051 1062 def close_pull_request_with_comment(self, pull_request, user, repo,
1052 1063 message=None):
1053 1064 status = ChangesetStatus.STATUS_REJECTED
1054 1065
1055 1066 if not message:
1056 1067 message = (
1057 1068 _('Status change %(transition_icon)s %(status)s') % {
1058 1069 'transition_icon': '>',
1059 1070 'status': ChangesetStatus.get_status_lbl(status)})
1060 1071
1061 1072 internal_message = _('Closing with') + ' ' + message
1062 1073
1063 1074 comm = CommentsModel().create(
1064 1075 text=internal_message,
1065 1076 repo=repo.repo_id,
1066 1077 user=user.user_id,
1067 1078 pull_request=pull_request.pull_request_id,
1068 1079 f_path=None,
1069 1080 line_no=None,
1070 1081 status_change=ChangesetStatus.get_status_lbl(status),
1071 1082 status_change_type=status,
1072 1083 closing_pr=True
1073 1084 )
1074 1085
1075 1086 ChangesetStatusModel().set_status(
1076 1087 repo.repo_id,
1077 1088 status,
1078 1089 user.user_id,
1079 1090 comm,
1080 1091 pull_request=pull_request.pull_request_id
1081 1092 )
1082 1093 Session().flush()
1083 1094
1084 1095 PullRequestModel().close_pull_request(
1085 1096 pull_request.pull_request_id, user)
1086 1097
1087 1098 def merge_status(self, pull_request):
1088 1099 if not self._is_merge_enabled(pull_request):
1089 1100 return False, _('Server-side pull request merging is disabled.')
1090 1101 if pull_request.is_closed():
1091 1102 return False, _('This pull request is closed.')
1092 1103 merge_possible, msg = self._check_repo_requirements(
1093 1104 target=pull_request.target_repo, source=pull_request.source_repo)
1094 1105 if not merge_possible:
1095 1106 return merge_possible, msg
1096 1107
1097 1108 try:
1098 1109 resp = self._try_merge(pull_request)
1099 1110 log.debug("Merge response: %s", resp)
1100 1111 status = resp.possible, self.merge_status_message(
1101 1112 resp.failure_reason)
1102 1113 except NotImplementedError:
1103 1114 status = False, _('Pull request merging is not supported.')
1104 1115
1105 1116 return status
1106 1117
1107 1118 def _check_repo_requirements(self, target, source):
1108 1119 """
1109 1120 Check if `target` and `source` have compatible requirements.
1110 1121
1111 1122 Currently this is just checking for largefiles.
1112 1123 """
1113 1124 target_has_largefiles = self._has_largefiles(target)
1114 1125 source_has_largefiles = self._has_largefiles(source)
1115 1126 merge_possible = True
1116 1127 message = u''
1117 1128
1118 1129 if target_has_largefiles != source_has_largefiles:
1119 1130 merge_possible = False
1120 1131 if source_has_largefiles:
1121 1132 message = _(
1122 1133 'Target repository large files support is disabled.')
1123 1134 else:
1124 1135 message = _(
1125 1136 'Source repository large files support is disabled.')
1126 1137
1127 1138 return merge_possible, message
1128 1139
1129 1140 def _has_largefiles(self, repo):
1130 1141 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1131 1142 'extensions', 'largefiles')
1132 1143 return largefiles_ui and largefiles_ui[0].active
1133 1144
1134 1145 def _try_merge(self, pull_request):
1135 1146 """
1136 1147 Try to merge the pull request and return the merge status.
1137 1148 """
1138 1149 log.debug(
1139 1150 "Trying out if the pull request %s can be merged.",
1140 1151 pull_request.pull_request_id)
1141 1152 target_vcs = pull_request.target_repo.scm_instance()
1142 1153
1143 1154 # Refresh the target reference.
1144 1155 try:
1145 1156 target_ref = self._refresh_reference(
1146 1157 pull_request.target_ref_parts, target_vcs)
1147 1158 except CommitDoesNotExistError:
1148 1159 merge_state = MergeResponse(
1149 1160 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1150 1161 return merge_state
1151 1162
1152 1163 target_locked = pull_request.target_repo.locked
1153 1164 if target_locked and target_locked[0]:
1154 1165 log.debug("The target repository is locked.")
1155 1166 merge_state = MergeResponse(
1156 1167 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1157 1168 elif self._needs_merge_state_refresh(pull_request, target_ref):
1158 1169 log.debug("Refreshing the merge status of the repository.")
1159 1170 merge_state = self._refresh_merge_state(
1160 1171 pull_request, target_vcs, target_ref)
1161 1172 else:
1162 1173 possible = pull_request.\
1163 1174 _last_merge_status == MergeFailureReason.NONE
1164 1175 merge_state = MergeResponse(
1165 1176 possible, False, None, pull_request._last_merge_status)
1166 1177
1167 1178 return merge_state
1168 1179
1169 1180 def _refresh_reference(self, reference, vcs_repository):
1170 1181 if reference.type in ('branch', 'book'):
1171 1182 name_or_id = reference.name
1172 1183 else:
1173 1184 name_or_id = reference.commit_id
1174 1185 refreshed_commit = vcs_repository.get_commit(name_or_id)
1175 1186 refreshed_reference = Reference(
1176 1187 reference.type, reference.name, refreshed_commit.raw_id)
1177 1188 return refreshed_reference
1178 1189
1179 1190 def _needs_merge_state_refresh(self, pull_request, target_reference):
1180 1191 return not(
1181 1192 pull_request.revisions and
1182 1193 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1183 1194 target_reference.commit_id == pull_request._last_merge_target_rev)
1184 1195
1185 1196 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1186 1197 workspace_id = self._workspace_id(pull_request)
1187 1198 source_vcs = pull_request.source_repo.scm_instance()
1188 1199 use_rebase = self._use_rebase_for_merging(pull_request)
1189 1200 merge_state = target_vcs.merge(
1190 1201 target_reference, source_vcs, pull_request.source_ref_parts,
1191 1202 workspace_id, dry_run=True, use_rebase=use_rebase)
1192 1203
1193 1204 # Do not store the response if there was an unknown error.
1194 1205 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1195 1206 pull_request._last_merge_source_rev = \
1196 1207 pull_request.source_ref_parts.commit_id
1197 1208 pull_request._last_merge_target_rev = target_reference.commit_id
1198 1209 pull_request._last_merge_status = merge_state.failure_reason
1199 1210 pull_request.shadow_merge_ref = merge_state.merge_ref
1200 1211 Session().add(pull_request)
1201 1212 Session().commit()
1202 1213
1203 1214 return merge_state
1204 1215
1205 1216 def _workspace_id(self, pull_request):
1206 1217 workspace_id = 'pr-%s' % pull_request.pull_request_id
1207 1218 return workspace_id
1208 1219
1209 1220 def merge_status_message(self, status_code):
1210 1221 """
1211 1222 Return a human friendly error message for the given merge status code.
1212 1223 """
1213 1224 return self.MERGE_STATUS_MESSAGES[status_code]
1214 1225
1215 1226 def generate_repo_data(self, repo, commit_id=None, branch=None,
1216 1227 bookmark=None):
1217 1228 all_refs, selected_ref = \
1218 1229 self._get_repo_pullrequest_sources(
1219 1230 repo.scm_instance(), commit_id=commit_id,
1220 1231 branch=branch, bookmark=bookmark)
1221 1232
1222 1233 refs_select2 = []
1223 1234 for element in all_refs:
1224 1235 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1225 1236 refs_select2.append({'text': element[1], 'children': children})
1226 1237
1227 1238 return {
1228 1239 'user': {
1229 1240 'user_id': repo.user.user_id,
1230 1241 'username': repo.user.username,
1231 1242 'firstname': repo.user.firstname,
1232 1243 'lastname': repo.user.lastname,
1233 1244 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1234 1245 },
1235 1246 'description': h.chop_at_smart(repo.description, '\n'),
1236 1247 'refs': {
1237 1248 'all_refs': all_refs,
1238 1249 'selected_ref': selected_ref,
1239 1250 'select2_refs': refs_select2
1240 1251 }
1241 1252 }
1242 1253
1243 1254 def generate_pullrequest_title(self, source, source_ref, target):
1244 1255 return u'{source}#{at_ref} to {target}'.format(
1245 1256 source=source,
1246 1257 at_ref=source_ref,
1247 1258 target=target,
1248 1259 )
1249 1260
1250 1261 def _cleanup_merge_workspace(self, pull_request):
1251 1262 # Merging related cleanup
1252 1263 target_scm = pull_request.target_repo.scm_instance()
1253 1264 workspace_id = 'pr-%s' % pull_request.pull_request_id
1254 1265
1255 1266 try:
1256 1267 target_scm.cleanup_merge_workspace(workspace_id)
1257 1268 except NotImplementedError:
1258 1269 pass
1259 1270
1260 1271 def _get_repo_pullrequest_sources(
1261 1272 self, repo, commit_id=None, branch=None, bookmark=None):
1262 1273 """
1263 1274 Return a structure with repo's interesting commits, suitable for
1264 1275 the selectors in pullrequest controller
1265 1276
1266 1277 :param commit_id: a commit that must be in the list somehow
1267 1278 and selected by default
1268 1279 :param branch: a branch that must be in the list and selected
1269 1280 by default - even if closed
1270 1281 :param bookmark: a bookmark that must be in the list and selected
1271 1282 """
1272 1283
1273 1284 commit_id = safe_str(commit_id) if commit_id else None
1274 1285 branch = safe_str(branch) if branch else None
1275 1286 bookmark = safe_str(bookmark) if bookmark else None
1276 1287
1277 1288 selected = None
1278 1289
1279 1290 # order matters: first source that has commit_id in it will be selected
1280 1291 sources = []
1281 1292 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1282 1293 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1283 1294
1284 1295 if commit_id:
1285 1296 ref_commit = (h.short_id(commit_id), commit_id)
1286 1297 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1287 1298
1288 1299 sources.append(
1289 1300 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1290 1301 )
1291 1302
1292 1303 groups = []
1293 1304 for group_key, ref_list, group_name, match in sources:
1294 1305 group_refs = []
1295 1306 for ref_name, ref_id in ref_list:
1296 1307 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1297 1308 group_refs.append((ref_key, ref_name))
1298 1309
1299 1310 if not selected:
1300 1311 if set([commit_id, match]) & set([ref_id, ref_name]):
1301 1312 selected = ref_key
1302 1313
1303 1314 if group_refs:
1304 1315 groups.append((group_refs, group_name))
1305 1316
1306 1317 if not selected:
1307 1318 ref = commit_id or branch or bookmark
1308 1319 if ref:
1309 1320 raise CommitDoesNotExistError(
1310 1321 'No commit refs could be found matching: %s' % ref)
1311 1322 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1312 1323 selected = 'branch:%s:%s' % (
1313 1324 repo.DEFAULT_BRANCH_NAME,
1314 1325 repo.branches[repo.DEFAULT_BRANCH_NAME]
1315 1326 )
1316 1327 elif repo.commit_ids:
1317 1328 rev = repo.commit_ids[0]
1318 1329 selected = 'rev:%s:%s' % (rev, rev)
1319 1330 else:
1320 1331 raise EmptyRepositoryError()
1321 1332 return groups, selected
1322 1333
1323 1334 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1324 1335 return self._get_diff_from_pr_or_version(
1325 1336 source_repo, source_ref_id, target_ref_id, context=context)
1326 1337
1327 1338 def _get_diff_from_pr_or_version(
1328 1339 self, source_repo, source_ref_id, target_ref_id, context):
1329 1340 target_commit = source_repo.get_commit(
1330 1341 commit_id=safe_str(target_ref_id))
1331 1342 source_commit = source_repo.get_commit(
1332 1343 commit_id=safe_str(source_ref_id))
1333 1344 if isinstance(source_repo, Repository):
1334 1345 vcs_repo = source_repo.scm_instance()
1335 1346 else:
1336 1347 vcs_repo = source_repo
1337 1348
1338 1349 # TODO: johbo: In the context of an update, we cannot reach
1339 1350 # the old commit anymore with our normal mechanisms. It needs
1340 1351 # some sort of special support in the vcs layer to avoid this
1341 1352 # workaround.
1342 1353 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1343 1354 vcs_repo.alias == 'git'):
1344 1355 source_commit.raw_id = safe_str(source_ref_id)
1345 1356
1346 1357 log.debug('calculating diff between '
1347 1358 'source_ref:%s and target_ref:%s for repo `%s`',
1348 1359 target_ref_id, source_ref_id,
1349 1360 safe_unicode(vcs_repo.path))
1350 1361
1351 1362 vcs_diff = vcs_repo.get_diff(
1352 1363 commit1=target_commit, commit2=source_commit, context=context)
1353 1364 return vcs_diff
1354 1365
1355 1366 def _is_merge_enabled(self, pull_request):
1356 1367 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1357 1368 settings = settings_model.get_general_settings()
1358 1369 return settings.get('rhodecode_pr_merge_enabled', False)
1359 1370
1360 1371 def _use_rebase_for_merging(self, pull_request):
1361 1372 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1362 1373 settings = settings_model.get_general_settings()
1363 1374 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1364 1375
1365 1376 def _log_action(self, action, user, pull_request):
1366 1377 action_logger(
1367 1378 user,
1368 1379 '{action}:{pr_id}'.format(
1369 1380 action=action, pr_id=pull_request.pull_request_id),
1370 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 1401 class MergeCheck(object):
1374 1402 """
1375 1403 Perform Merge Checks and returns a check object which stores information
1376 1404 about merge errors, and merge conditions
1377 1405 """
1378 1406 TODO_CHECK = 'todo'
1379 1407 PERM_CHECK = 'perm'
1380 1408 REVIEW_CHECK = 'review'
1381 1409 MERGE_CHECK = 'merge'
1382 1410
1383 1411 def __init__(self):
1384 1412 self.review_status = None
1385 1413 self.merge_possible = None
1386 1414 self.merge_msg = ''
1387 1415 self.failed = None
1388 1416 self.errors = []
1389 1417 self.error_details = OrderedDict()
1390 1418
1391 1419 def push_error(self, error_type, message, error_key, details):
1392 1420 self.failed = True
1393 1421 self.errors.append([error_type, message])
1394 1422 self.error_details[error_key] = dict(
1395 1423 details=details,
1396 1424 error_type=error_type,
1397 1425 message=message
1398 1426 )
1399 1427
1400 1428 @classmethod
1401 1429 def validate(cls, pull_request, user, fail_early=False, translator=None):
1402 1430 # if migrated to pyramid...
1403 1431 # _ = lambda: translator or _ # use passed in translator if any
1404 1432
1405 1433 merge_check = cls()
1406 1434
1407 1435 # permissions to merge
1408 1436 user_allowed_to_merge = PullRequestModel().check_user_merge(
1409 1437 pull_request, user)
1410 1438 if not user_allowed_to_merge:
1411 1439 log.debug("MergeCheck: cannot merge, approval is pending.")
1412 1440
1413 1441 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1414 1442 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1415 1443 if fail_early:
1416 1444 return merge_check
1417 1445
1418 1446 # review status, must be always present
1419 1447 review_status = pull_request.calculated_review_status()
1420 1448 merge_check.review_status = review_status
1421 1449
1422 1450 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1423 1451 if not status_approved:
1424 1452 log.debug("MergeCheck: cannot merge, approval is pending.")
1425 1453
1426 1454 msg = _('Pull request reviewer approval is pending.')
1427 1455
1428 1456 merge_check.push_error(
1429 1457 'warning', msg, cls.REVIEW_CHECK, review_status)
1430 1458
1431 1459 if fail_early:
1432 1460 return merge_check
1433 1461
1434 1462 # left over TODOs
1435 1463 todos = CommentsModel().get_unresolved_todos(pull_request)
1436 1464 if todos:
1437 1465 log.debug("MergeCheck: cannot merge, {} "
1438 1466 "unresolved todos left.".format(len(todos)))
1439 1467
1440 1468 if len(todos) == 1:
1441 1469 msg = _('Cannot merge, {} TODO still not resolved.').format(
1442 1470 len(todos))
1443 1471 else:
1444 1472 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1445 1473 len(todos))
1446 1474
1447 1475 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1448 1476
1449 1477 if fail_early:
1450 1478 return merge_check
1451 1479
1452 1480 # merge possible
1453 1481 merge_status, msg = PullRequestModel().merge_status(pull_request)
1454 1482 merge_check.merge_possible = merge_status
1455 1483 merge_check.merge_msg = msg
1456 1484 if not merge_status:
1457 1485 log.debug(
1458 1486 "MergeCheck: cannot merge, pull request merge not possible.")
1459 1487 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1460 1488
1461 1489 if fail_early:
1462 1490 return merge_check
1463 1491
1464 1492 return merge_check
1465 1493
1466 1494
1467 1495 ChangeTuple = namedtuple('ChangeTuple',
1468 1496 ['added', 'common', 'removed', 'total'])
1469 1497
1470 1498 FileChangeTuple = namedtuple('FileChangeTuple',
1471 1499 ['added', 'modified', 'removed'])
@@ -1,196 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import colander
24 24 from rhodecode.model.validation_schema import preparers
25 25 from rhodecode.model.db import User, UserGroup
26 26
27 27
28 28 class _RootLocation(object):
29 29 pass
30 30
31 31 RootLocation = _RootLocation()
32 32
33 33
34 34 def _normalize(seperator, path):
35 35
36 36 if not path:
37 37 return ''
38 38 elif path is colander.null:
39 39 return colander.null
40 40
41 41 parts = path.split(seperator)
42 42
43 43 def bad_parts(value):
44 44 if not value:
45 45 return False
46 46 if re.match(r'^[.]+$', value):
47 47 return False
48 48
49 49 return True
50 50
51 51 def slugify(value):
52 52 value = preparers.slugify_preparer(value)
53 53 value = re.sub(r'[.]{2,}', '.', value)
54 54 return value
55 55
56 56 clean_parts = [slugify(item) for item in parts if item]
57 57 path = filter(bad_parts, clean_parts)
58 58 return seperator.join(path)
59 59
60 60
61 61 class RepoNameType(colander.String):
62 62 SEPARATOR = '/'
63 63
64 64 def deserialize(self, node, cstruct):
65 65 result = super(RepoNameType, self).deserialize(node, cstruct)
66 66 if cstruct is colander.null:
67 67 return colander.null
68 68 return self._normalize(result)
69 69
70 70 def _normalize(self, path):
71 71 return _normalize(self.SEPARATOR, path)
72 72
73 73
74 74 class GroupNameType(colander.String):
75 75 SEPARATOR = '/'
76 76
77 77 def deserialize(self, node, cstruct):
78 78 if cstruct is RootLocation:
79 79 return cstruct
80 80
81 81 result = super(GroupNameType, self).deserialize(node, cstruct)
82 82 if cstruct is colander.null:
83 83 return colander.null
84 84 return self._normalize(result)
85 85
86 86 def _normalize(self, path):
87 87 return _normalize(self.SEPARATOR, path)
88 88
89 89
90 90 class StringBooleanType(colander.String):
91 91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
92 92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
93 93
94 94 def serialize(self, node, appstruct):
95 95 if appstruct is colander.null:
96 96 return colander.null
97 97 if not isinstance(appstruct, bool):
98 98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
99 99
100 100 return appstruct and 'true' or 'false'
101 101
102 102 def deserialize(self, node, cstruct):
103 103 if cstruct is colander.null:
104 104 return colander.null
105 105
106 106 if isinstance(cstruct, bool):
107 107 return cstruct
108 108
109 109 if not isinstance(cstruct, basestring):
110 110 raise colander.Invalid(node, '%r is not a string' % cstruct)
111 111
112 112 value = cstruct.lower()
113 113 if value in self.true_values:
114 114 return True
115 115 elif value in self.false_values:
116 116 return False
117 117 else:
118 118 raise colander.Invalid(
119 119 node, '{} value cannot be translated to bool'.format(value))
120 120
121 121
122 122 class UserOrUserGroupType(colander.SchemaType):
123 123 """ colander Schema type for valid rhodecode user and/or usergroup """
124 124 scopes = ('user', 'usergroup')
125 125
126 126 def __init__(self):
127 127 self.users = 'user' in self.scopes
128 128 self.usergroups = 'usergroup' in self.scopes
129 129
130 130 def serialize(self, node, appstruct):
131 131 if appstruct is colander.null:
132 132 return colander.null
133 133
134 134 if self.users:
135 135 if isinstance(appstruct, User):
136 136 if self.usergroups:
137 137 return 'user:%s' % appstruct.username
138 138 return appstruct.username
139 139
140 140 if self.usergroups:
141 141 if isinstance(appstruct, UserGroup):
142 142 if self.users:
143 143 return 'usergroup:%s' % appstruct.users_group_name
144 144 return appstruct.users_group_name
145 145
146 146 raise colander.Invalid(
147 147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
148 148
149 149 def deserialize(self, node, cstruct):
150 150 if cstruct is colander.null:
151 151 return colander.null
152 152
153 153 user, usergroup = None, None
154 154 if self.users:
155 155 if cstruct.startswith('user:'):
156 156 user = User.get_by_username(cstruct.split(':')[1])
157 157 else:
158 158 user = User.get_by_username(cstruct)
159 159
160 160 if self.usergroups:
161 161 if cstruct.startswith('usergroup:'):
162 162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
163 163 else:
164 164 usergroup = UserGroup.get_by_group_name(cstruct)
165 165
166 166 if self.users and self.usergroups:
167 167 if user and usergroup:
168 168 raise colander.Invalid(node, (
169 169 '%s is both a user and usergroup, specify which '
170 170 'one was wanted by prepending user: or usergroup: to the '
171 171 'name') % cstruct)
172 172
173 173 if self.users and user:
174 174 return user
175 175
176 176 if self.usergroups and usergroup:
177 177 return usergroup
178 178
179 179 raise colander.Invalid(
180 180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
181 181
182 182
183 183 class UserType(UserOrUserGroupType):
184 184 scopes = ('user',)
185 185
186 186
187 187 class UserGroupType(UserOrUserGroupType):
188 188 scopes = ('usergroup',)
189 189
190 190
191 191 class StrOrIntType(colander.String):
192 192 def deserialize(self, node, cstruct):
193 if isinstance(node, basestring):
193 if isinstance(cstruct, basestring):
194 194 return super(StrOrIntType, self).deserialize(node, cstruct)
195 195 else:
196 196 return colander.Integer().deserialize(node, cstruct)
@@ -1,408 +1,418 b''
1 1
2 2
3 3 //BUTTONS
4 4 button,
5 5 .btn,
6 6 input[type="button"] {
7 7 -webkit-appearance: none;
8 8 display: inline-block;
9 9 margin: 0 @padding/3 0 0;
10 10 padding: @button-padding;
11 11 text-align: center;
12 12 font-size: @basefontsize;
13 13 line-height: 1em;
14 14 font-family: @text-light;
15 15 text-decoration: none;
16 16 text-shadow: none;
17 17 color: @grey4;
18 18 background-color: white;
19 19 background-image: none;
20 20 border: none;
21 21 .border ( @border-thickness-buttons, @grey4 );
22 22 .border-radius (@border-radius);
23 23 cursor: pointer;
24 24 white-space: nowrap;
25 25 -webkit-transition: background .3s,color .3s;
26 26 -moz-transition: background .3s,color .3s;
27 27 -o-transition: background .3s,color .3s;
28 28 transition: background .3s,color .3s;
29 29
30 30 a {
31 31 display: block;
32 32 margin: 0;
33 33 padding: 0;
34 34 color: inherit;
35 35 text-decoration: none;
36 36
37 37 &:hover {
38 38 text-decoration: none;
39 39 }
40 40 }
41 41
42 42 &:focus,
43 43 &:active {
44 44 outline:none;
45 45 }
46 46 &:hover {
47 47 color: white;
48 48 background-color: @grey4;
49 49 }
50 50
51 51 .icon-remove-sign {
52 52 display: none;
53 53 }
54 54
55 55 //disabled buttons
56 56 //last; overrides any other styles
57 57 &:disabled {
58 58 opacity: .7;
59 59 cursor: auto;
60 60 background-color: white;
61 61 color: @grey4;
62 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 72 .btn-default {
69 73 .border ( @border-thickness-buttons, @rcblue );
70 74 background-image: none;
71 75 color: @rcblue;
72 76
73 77 a {
74 78 color: @rcblue;
75 79 }
76 80
77 81 &:hover,
78 82 &.active {
79 83 color: white;
80 84 background-color: @rcdarkblue;
81 85 .border ( @border-thickness, @rcdarkblue );
82 86
83 87 a {
84 88 color: white;
85 89 }
86 90 }
87 91 &:disabled {
88 92 .border ( @border-thickness-buttons, @grey4 );
89 93 background-color: transparent;
90 94 }
91 95 }
92 96
93 97 .btn-primary,
94 98 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
95 99 .btn-success {
96 100 .border ( @border-thickness, @rcblue );
97 101 background-color: @rcblue;
98 102 color: white;
99 103
100 104 a {
101 105 color: white;
102 106 }
103 107
104 108 &:hover,
105 109 &.active {
106 110 .border ( @border-thickness, @rcdarkblue );
107 111 color: white;
108 112 background-color: @rcdarkblue;
109 113
110 114 a {
111 115 color: white;
112 116 }
113 117 }
114 118 &:disabled {
115 119 background-color: @rcblue;
116 120 }
117 121 }
118 122
119 123 .btn-secondary {
120 124 &:extend(.btn-default);
121 125
122 126 background-color: white;
123 127
124 128 &:focus {
125 129 outline: 0;
126 130 }
127 131
128 132 &:hover {
129 133 &:extend(.btn-default:hover);
130 134 }
131 135
132 136 &.btn-link {
133 137 &:extend(.btn-link);
134 138 color: @rcblue;
135 139 }
136 140
137 141 &:disabled {
138 142 color: @rcblue;
139 143 background-color: white;
140 144 }
141 145 }
142 146
143 147 .btn-warning,
144 148 .btn-danger,
145 149 .revoke_perm,
146 150 .btn-x,
147 151 .form .action_button.btn-x {
148 152 .border ( @border-thickness, @alert2 );
149 153 background-color: white;
150 154 color: @alert2;
151 155
152 156 a {
153 157 color: @alert2;
154 158 }
155 159
156 160 &:hover,
157 161 &.active {
158 162 .border ( @border-thickness, @alert2 );
159 163 color: white;
160 164 background-color: @alert2;
161 165
162 166 a {
163 167 color: white;
164 168 }
165 169 }
166 170
167 171 i {
168 172 display:none;
169 173 }
170 174
171 175 &:disabled {
172 176 background-color: white;
173 177 color: @alert2;
174 178 }
175 179 }
176 180
177 181 .btn-approved-status {
178 182 .border ( @border-thickness, @alert1 );
179 183 background-color: white;
180 184 color: @alert1;
181 185
182 186 }
183 187
184 188 .btn-rejected-status {
185 189 .border ( @border-thickness, @alert2 );
186 190 background-color: white;
187 191 color: @alert2;
188 192 }
189 193
190 194 .btn-sm,
191 195 .btn-mini,
192 196 .field-sm .btn {
193 197 padding: @padding/3;
194 198 }
195 199
196 200 .btn-xs {
197 201 padding: @padding/4;
198 202 }
199 203
200 204 .btn-lg {
201 205 padding: @padding * 1.2;
202 206 }
203 207
204 208 .btn-group {
205 209 display: inline-block;
206 210 .btn {
207 211 float: left;
208 212 margin: 0 0 0 -1px;
209 213 }
210 214 }
211 215
212 216 .btn-link {
213 217 background: transparent;
214 218 border: none;
215 219 padding: 0;
216 220 color: @rcblue;
217 221
218 222 &:hover {
219 223 background: transparent;
220 224 border: none;
221 225 color: @rcdarkblue;
222 226 }
223 227
224 228 //disabled buttons
225 229 //last; overrides any other styles
226 230 &:disabled {
227 231 opacity: .7;
228 232 cursor: auto;
229 233 background-color: white;
230 234 color: @grey4;
231 235 text-shadow: none;
232 236 }
233 237
234 238 // TODO: johbo: Check if we can avoid this, indicates that the structure
235 239 // is not yet good.
236 240 // lisa: The button CSS reflects the button HTML; both need a cleanup.
237 241 &.btn-danger {
238 242 color: @alert2;
239 243
240 244 &:hover {
241 245 color: darken(@alert2,30%);
242 246 }
243 247
244 248 &:disabled {
245 249 color: @alert2;
246 250 }
247 251 }
248 252 }
249 253
250 254 .btn-social {
251 255 &:extend(.btn-default);
252 256 margin: 5px 5px 5px 0px;
253 257 min-width: 150px;
254 258 }
255 259
256 260 // TODO: johbo: check these exceptions
257 261
258 262 .links {
259 263
260 264 .btn + .btn {
261 265 margin-top: @padding;
262 266 }
263 267 }
264 268
265 269
266 270 .action_button {
267 271 display:inline;
268 272 margin: 0;
269 273 padding: 0 1em 0 0;
270 274 font-size: inherit;
271 275 color: @rcblue;
272 276 border: none;
273 .border-radius (0);
277 border-radius: 0;
274 278 background-color: transparent;
275 279
280 &.last-item {
281 border: none;
282 padding: 0 0 0 0;
283 }
284
276 285 &:last-child {
277 286 border: none;
287 padding: 0 0 0 0;
278 288 }
279 289
280 290 &:hover {
281 291 color: @rcdarkblue;
282 292 background-color: transparent;
283 293 border: none;
284 294 }
285 295 }
286 296 .grid_delete {
287 297 .action_button {
288 298 border: none;
289 299 }
290 300 }
291 301
292 302
293 303 // TODO: johbo: Form button tweaks, check if we can use the classes instead
294 304 input[type="submit"] {
295 305 &:extend(.btn-primary);
296 306
297 307 &:focus {
298 308 outline: 0;
299 309 }
300 310
301 311 &:hover {
302 312 &:extend(.btn-primary:hover);
303 313 }
304 314
305 315 &.btn-link {
306 316 &:extend(.btn-link);
307 317 color: @rcblue;
308 318
309 319 &:disabled {
310 320 color: @rcblue;
311 321 background-color: transparent;
312 322 }
313 323 }
314 324
315 325 &:disabled {
316 326 .border ( @border-thickness-buttons, @rcblue );
317 327 background-color: @rcblue;
318 328 color: white;
319 329 }
320 330 }
321 331
322 332 input[type="reset"] {
323 333 &:extend(.btn-default);
324 334
325 335 // TODO: johbo: Check if this tweak can be avoided.
326 336 background: transparent;
327 337
328 338 &:focus {
329 339 outline: 0;
330 340 }
331 341
332 342 &:hover {
333 343 &:extend(.btn-default:hover);
334 344 }
335 345
336 346 &.btn-link {
337 347 &:extend(.btn-link);
338 348 color: @rcblue;
339 349
340 350 &:disabled {
341 351 border: none;
342 352 }
343 353 }
344 354
345 355 &:disabled {
346 356 .border ( @border-thickness-buttons, @rcblue );
347 357 background-color: white;
348 358 color: @rcblue;
349 359 }
350 360 }
351 361
352 362 input[type="submit"],
353 363 input[type="reset"] {
354 364 &.btn-danger {
355 365 &:extend(.btn-danger);
356 366
357 367 &:focus {
358 368 outline: 0;
359 369 }
360 370
361 371 &:hover {
362 372 &:extend(.btn-danger:hover);
363 373 }
364 374
365 375 &.btn-link {
366 376 &:extend(.btn-link);
367 377 color: @alert2;
368 378
369 379 &:hover {
370 380 color: darken(@alert2,30%);
371 381 }
372 382 }
373 383
374 384 &:disabled {
375 385 color: @alert2;
376 386 background-color: white;
377 387 }
378 388 }
379 389 &.btn-danger-action {
380 390 .border ( @border-thickness, @alert2 );
381 391 background-color: @alert2;
382 392 color: white;
383 393
384 394 a {
385 395 color: white;
386 396 }
387 397
388 398 &:hover {
389 399 background-color: darken(@alert2,20%);
390 400 }
391 401
392 402 &.active {
393 403 .border ( @border-thickness, @alert2 );
394 404 color: white;
395 405 background-color: @alert2;
396 406
397 407 a {
398 408 color: white;
399 409 }
400 410 }
401 411
402 412 &:disabled {
403 413 background-color: white;
404 414 color: @alert2;
405 415 }
406 416 }
407 417 }
408 418
@@ -1,139 +1,139 b''
1 1 .deform {
2 2
3 3 * {
4 4 box-sizing: border-box;
5 5 }
6 6
7 7 .required:after {
8 8 color: #e32;
9 9 content: '*';
10 10 display:inline;
11 11 }
12 12
13 13 .control-label {
14 14 width: 200px;
15 padding: 10px;
15 padding: 10px 0px;
16 16 float: left;
17 17 }
18 18 .control-inputs {
19 19 width: 400px;
20 20 float: left;
21 21 }
22 22 .form-group .radio, .form-group .checkbox {
23 23 position: relative;
24 24 display: block;
25 25 /* margin-bottom: 10px; */
26 26 }
27 27
28 28 .form-group {
29 29 clear: left;
30 30 margin-bottom: 20px;
31 31
32 32 &:after { /* clear fix */
33 33 content: " ";
34 34 display: block;
35 35 clear: left;
36 36 }
37 37 }
38 38
39 39 .form-control {
40 40 width: 100%;
41 41 padding: 0.9em;
42 42 border: 1px solid #979797;
43 43 border-radius: 2px;
44 44 }
45 45 .form-control.select2-container {
46 46 padding: 0; /* padding already applied in .drop-menu a */
47 47 }
48 48
49 49 .form-control.readonly {
50 50 background: #eeeeee;
51 51 cursor: not-allowed;
52 52 }
53 53
54 54 .error-block {
55 55 color: red;
56 56 margin: 0;
57 57 }
58 58
59 59 .help-block {
60 60 margin: 0;
61 61 }
62 62
63 63 .deform-seq-container .control-inputs {
64 64 width: 100%;
65 65 }
66 66
67 67 .deform-seq-container .deform-seq-item-handle {
68 68 width: 8.3%;
69 69 float: left;
70 70 }
71 71
72 72 .deform-seq-container .deform-seq-item-group {
73 73 width: 91.6%;
74 74 float: left;
75 75 }
76 76
77 77 .form-control {
78 78 input {
79 79 height: 40px;
80 80 }
81 81 input[type=checkbox], input[type=radio] {
82 82 height: auto;
83 83 }
84 84 select {
85 85 height: 40px;
86 86 }
87 87 }
88 88
89 89 .form-control.select2-container {
90 90 height: 40px;
91 91 }
92 92
93 93 .deform-full-field-sequence.control-inputs {
94 94 width: 100%;
95 95 }
96 96
97 97 .deform-table-sequence {
98 98 .deform-seq-container {
99 99 .deform-seq-item {
100 100 margin: 0;
101 101 label {
102 102 display: none;
103 103 }
104 104 .panel-heading {
105 105 display: none;
106 106 }
107 107 .deform-seq-item-group > .panel {
108 108 padding: 0;
109 109 margin: 5px 0;
110 110 border: none;
111 111 &> .panel-body {
112 112 padding: 0;
113 113 }
114 114 }
115 115 &:first-child label {
116 116 display: block;
117 117 }
118 118 }
119 119 }
120 120 }
121 121 .deform-table-2-sequence {
122 122 .deform-seq-container {
123 123 .deform-seq-item {
124 124 .form-group {
125 125 width: 45% !important; padding: 0 2px; float: left; clear: none;
126 126 }
127 127 }
128 128 }
129 129 }
130 130 .deform-table-3-sequence {
131 131 .deform-seq-container {
132 132 .deform-seq-item {
133 133 .form-group {
134 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 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'readme-box';
15 15 @import 'progress-bar';
16 16
17 17 @import 'type';
18 18 @import 'alerts';
19 19 @import 'buttons';
20 20 @import 'tags';
21 21 @import 'code-block';
22 22 @import 'examples';
23 23 @import 'login';
24 24 @import 'main-content';
25 25 @import 'select2';
26 26 @import 'comments';
27 27 @import 'panels-bootstrap';
28 28 @import 'panels';
29 29 @import 'deform';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 ul.simple-list{
104 104 list-style: none;
105 105 margin: 0;
106 106 padding: 0;
107 107 }
108 108
109 109 .main-content {
110 110 padding-bottom: @pagepadding;
111 111 }
112 112
113 113 .wide-mode-wrapper {
114 114 max-width:4000px !important;
115 115 }
116 116
117 117 .wrapper {
118 118 position: relative;
119 119 max-width: @wrapper-maxwidth;
120 120 margin: 0 auto;
121 121 }
122 122
123 123 #content {
124 124 clear: both;
125 125 padding: 0 @contentpadding;
126 126 }
127 127
128 128 .advanced-settings-fields{
129 129 input{
130 130 margin-left: @textmargin;
131 131 margin-right: @padding/2;
132 132 }
133 133 }
134 134
135 135 .cs_files_title {
136 136 margin: @pagepadding 0 0;
137 137 }
138 138
139 139 input.inline[type="file"] {
140 140 display: inline;
141 141 }
142 142
143 143 .error_page {
144 144 margin: 10% auto;
145 145
146 146 h1 {
147 147 color: @grey2;
148 148 }
149 149
150 150 .alert {
151 151 margin: @padding 0;
152 152 }
153 153
154 154 .error-branding {
155 155 font-family: @text-semibold;
156 156 color: @grey4;
157 157 }
158 158
159 159 .error_message {
160 160 font-family: @text-regular;
161 161 }
162 162
163 163 .sidebar {
164 164 min-height: 275px;
165 165 margin: 0;
166 166 padding: 0 0 @sidebarpadding @sidebarpadding;
167 167 border: none;
168 168 }
169 169
170 170 .main-content {
171 171 position: relative;
172 172 margin: 0 @sidebarpadding @sidebarpadding;
173 173 padding: 0 0 0 @sidebarpadding;
174 174 border-left: @border-thickness solid @grey5;
175 175
176 176 @media (max-width:767px) {
177 177 clear: both;
178 178 width: 100%;
179 179 margin: 0;
180 180 border: none;
181 181 }
182 182 }
183 183
184 184 .inner-column {
185 185 float: left;
186 186 width: 29.75%;
187 187 min-height: 150px;
188 188 margin: @sidebarpadding 2% 0 0;
189 189 padding: 0 2% 0 0;
190 190 border-right: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 border: none;
196 196 }
197 197
198 198 ul {
199 199 padding-left: 1.25em;
200 200 }
201 201
202 202 &:last-child {
203 203 margin: @sidebarpadding 0 0;
204 204 border: none;
205 205 }
206 206
207 207 h4 {
208 208 margin: 0 0 @padding;
209 209 font-family: @text-semibold;
210 210 }
211 211 }
212 212 }
213 213 .error-page-logo {
214 214 width: 130px;
215 215 height: 160px;
216 216 }
217 217
218 218 // HEADER
219 219 .header {
220 220
221 221 // TODO: johbo: Fix login pages, so that they work without a min-height
222 222 // for the header and then remove the min-height. I chose a smaller value
223 223 // intentionally here to avoid rendering issues in the main navigation.
224 224 min-height: 49px;
225 225
226 226 position: relative;
227 227 vertical-align: bottom;
228 228 padding: 0 @header-padding;
229 229 background-color: @grey2;
230 230 color: @grey5;
231 231
232 232 .title {
233 233 overflow: visible;
234 234 }
235 235
236 236 &:before,
237 237 &:after {
238 238 content: "";
239 239 clear: both;
240 240 width: 100%;
241 241 }
242 242
243 243 // TODO: johbo: Avoids breaking "Repositories" chooser
244 244 .select2-container .select2-choice .select2-arrow {
245 245 display: none;
246 246 }
247 247 }
248 248
249 249 #header-inner {
250 250 &.title {
251 251 margin: 0;
252 252 }
253 253 &:before,
254 254 &:after {
255 255 content: "";
256 256 clear: both;
257 257 }
258 258 }
259 259
260 260 // Gists
261 261 #files_data {
262 262 clear: both; //for firefox
263 263 }
264 264 #gistid {
265 265 margin-right: @padding;
266 266 }
267 267
268 268 // Global Settings Editor
269 269 .textarea.editor {
270 270 float: left;
271 271 position: relative;
272 272 max-width: @texteditor-width;
273 273
274 274 select {
275 275 position: absolute;
276 276 top:10px;
277 277 right:0;
278 278 }
279 279
280 280 .CodeMirror {
281 281 margin: 0;
282 282 }
283 283
284 284 .help-block {
285 285 margin: 0 0 @padding;
286 286 padding:.5em;
287 287 background-color: @grey6;
288 288 }
289 289 }
290 290
291 291 ul.auth_plugins {
292 292 margin: @padding 0 @padding @legend-width;
293 293 padding: 0;
294 294
295 295 li {
296 296 margin-bottom: @padding;
297 297 line-height: 1em;
298 298 list-style-type: none;
299 299
300 300 .auth_buttons .btn {
301 301 margin-right: @padding;
302 302 }
303 303
304 304 &:before { content: none; }
305 305 }
306 306 }
307 307
308 308
309 309 // My Account PR list
310 310
311 311 #show_closed {
312 312 margin: 0 1em 0 0;
313 313 }
314 314
315 315 .pullrequestlist {
316 316 .closed {
317 317 background-color: @grey6;
318 318 }
319 319 .td-status {
320 320 padding-left: .5em;
321 321 }
322 322 .log-container .truncate {
323 323 height: 2.75em;
324 324 white-space: pre-line;
325 325 }
326 326 table.rctable .user {
327 327 padding-left: 0;
328 328 }
329 329 table.rctable {
330 330 td.td-description,
331 331 .rc-user {
332 332 min-width: auto;
333 333 }
334 334 }
335 335 }
336 336
337 337 // Pull Requests
338 338
339 339 .pullrequests_section_head {
340 340 display: block;
341 341 clear: both;
342 342 margin: @padding 0;
343 343 font-family: @text-bold;
344 344 }
345 345
346 346 .pr-origininfo, .pr-targetinfo {
347 347 position: relative;
348 348
349 349 .tag {
350 350 display: inline-block;
351 351 margin: 0 1em .5em 0;
352 352 }
353 353
354 354 .clone-url {
355 355 display: inline-block;
356 356 margin: 0 0 .5em 0;
357 357 padding: 0;
358 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 373 .pr-pullinfo {
363 374 clear: both;
364 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 384 #pr-title-input {
368 385 width: 72%;
369 386 font-size: 1em;
370 387 font-family: @text-bold;
371 388 margin: 0;
372 389 padding: 0 0 0 @padding/4;
373 390 line-height: 1.7em;
374 391 color: @text-color;
375 392 letter-spacing: .02em;
376 393 }
377 394
378 395 #pullrequest_title {
379 396 width: 100%;
380 397 box-sizing: border-box;
381 398 }
382 399
383 400 #pr_open_message {
384 401 border: @border-thickness solid #fff;
385 402 border-radius: @border-radius;
386 403 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
387 404 text-align: left;
388 405 overflow: hidden;
389 406 }
390 407
391 408 .pr-submit-button {
392 409 float: right;
393 410 margin: 0 0 0 5px;
394 411 }
395 412
396 413 .pr-spacing-container {
397 414 padding: 20px;
398 415 clear: both
399 416 }
400 417
401 418 #pr-description-input {
402 419 margin-bottom: 0;
403 420 }
404 421
405 422 .pr-description-label {
406 423 vertical-align: top;
407 424 }
408 425
409 426 .perms_section_head {
410 427 min-width: 625px;
411 428
412 429 h2 {
413 430 margin-bottom: 0;
414 431 }
415 432
416 433 .label-checkbox {
417 434 float: left;
418 435 }
419 436
420 437 &.field {
421 438 margin: @space 0 @padding;
422 439 }
423 440
424 441 &:first-child.field {
425 442 margin-top: 0;
426 443
427 444 .label {
428 445 margin-top: 0;
429 446 padding-top: 0;
430 447 }
431 448
432 449 .radios {
433 450 padding-top: 0;
434 451 }
435 452 }
436 453
437 454 .radios {
438 455 float: right;
439 456 position: relative;
440 457 width: 405px;
441 458 }
442 459 }
443 460
444 461 //--- MODULES ------------------//
445 462
446 463
447 464 // Server Announcement
448 465 #server-announcement {
449 466 width: 95%;
450 467 margin: @padding auto;
451 468 padding: @padding;
452 469 border-width: 2px;
453 470 border-style: solid;
454 471 .border-radius(2px);
455 472 font-family: @text-bold;
456 473
457 474 &.info { border-color: @alert4; background-color: @alert4-inner; }
458 475 &.warning { border-color: @alert3; background-color: @alert3-inner; }
459 476 &.error { border-color: @alert2; background-color: @alert2-inner; }
460 477 &.success { border-color: @alert1; background-color: @alert1-inner; }
461 478 &.neutral { border-color: @grey3; background-color: @grey6; }
462 479 }
463 480
464 481 // Fixed Sidebar Column
465 482 .sidebar-col-wrapper {
466 483 padding-left: @sidebar-all-width;
467 484
468 485 .sidebar {
469 486 width: @sidebar-width;
470 487 margin-left: -@sidebar-all-width;
471 488 }
472 489 }
473 490
474 491 .sidebar-col-wrapper.scw-small {
475 492 padding-left: @sidebar-small-all-width;
476 493
477 494 .sidebar {
478 495 width: @sidebar-small-width;
479 496 margin-left: -@sidebar-small-all-width;
480 497 }
481 498 }
482 499
483 500
484 501 // FOOTER
485 502 #footer {
486 503 padding: 0;
487 504 text-align: center;
488 505 vertical-align: middle;
489 506 color: @grey2;
490 507 background-color: @grey6;
491 508
492 509 p {
493 510 margin: 0;
494 511 padding: 1em;
495 512 line-height: 1em;
496 513 }
497 514
498 515 .server-instance { //server instance
499 516 display: none;
500 517 }
501 518
502 519 .title {
503 520 float: none;
504 521 margin: 0 auto;
505 522 }
506 523 }
507 524
508 525 button.close {
509 526 padding: 0;
510 527 cursor: pointer;
511 528 background: transparent;
512 529 border: 0;
513 530 .box-shadow(none);
514 531 -webkit-appearance: none;
515 532 }
516 533
517 534 .close {
518 535 float: right;
519 536 font-size: 21px;
520 537 font-family: @text-bootstrap;
521 538 line-height: 1em;
522 539 font-weight: bold;
523 540 color: @grey2;
524 541
525 542 &:hover,
526 543 &:focus {
527 544 color: @grey1;
528 545 text-decoration: none;
529 546 cursor: pointer;
530 547 }
531 548 }
532 549
533 550 // GRID
534 551 .sorting,
535 552 .sorting_desc,
536 553 .sorting_asc {
537 554 cursor: pointer;
538 555 }
539 556 .sorting_desc:after {
540 557 content: "\00A0\25B2";
541 558 font-size: .75em;
542 559 }
543 560 .sorting_asc:after {
544 561 content: "\00A0\25BC";
545 562 font-size: .68em;
546 563 }
547 564
548 565
549 566 .user_auth_tokens {
550 567
551 568 &.truncate {
552 569 white-space: nowrap;
553 570 overflow: hidden;
554 571 text-overflow: ellipsis;
555 572 }
556 573
557 574 .fields .field .input {
558 575 margin: 0;
559 576 }
560 577
561 578 input#description {
562 579 width: 100px;
563 580 margin: 0;
564 581 }
565 582
566 583 .drop-menu {
567 584 // TODO: johbo: Remove this, should work out of the box when
568 585 // having multiple inputs inline
569 586 margin: 0 0 0 5px;
570 587 }
571 588 }
572 589 #user_list_table {
573 590 .closed {
574 591 background-color: @grey6;
575 592 }
576 593 }
577 594
578 595
579 596 input {
580 597 &.disabled {
581 598 opacity: .5;
582 599 }
583 600 }
584 601
585 602 // remove extra padding in firefox
586 603 input::-moz-focus-inner { border:0; padding:0 }
587 604
588 605 .adjacent input {
589 606 margin-bottom: @padding;
590 607 }
591 608
592 609 .permissions_boxes {
593 610 display: block;
594 611 }
595 612
596 613 //TODO: lisa: this should be in tables
597 614 .show_more_col {
598 615 width: 20px;
599 616 }
600 617
601 618 //FORMS
602 619
603 620 .medium-inline,
604 621 input#description.medium-inline {
605 622 display: inline;
606 623 width: @medium-inline-input-width;
607 624 min-width: 100px;
608 625 }
609 626
610 627 select {
611 628 //reset
612 629 -webkit-appearance: none;
613 630 -moz-appearance: none;
614 631
615 632 display: inline-block;
616 633 height: 28px;
617 634 width: auto;
618 635 margin: 0 @padding @padding 0;
619 636 padding: 0 18px 0 8px;
620 637 line-height:1em;
621 638 font-size: @basefontsize;
622 639 border: @border-thickness solid @rcblue;
623 640 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
624 641 color: @rcblue;
625 642
626 643 &:after {
627 644 content: "\00A0\25BE";
628 645 }
629 646
630 647 &:focus {
631 648 outline: none;
632 649 }
633 650 }
634 651
635 652 option {
636 653 &:focus {
637 654 outline: none;
638 655 }
639 656 }
640 657
641 658 input,
642 659 textarea {
643 660 padding: @input-padding;
644 661 border: @input-border-thickness solid @border-highlight-color;
645 662 .border-radius (@border-radius);
646 663 font-family: @text-light;
647 664 font-size: @basefontsize;
648 665
649 666 &.input-sm {
650 667 padding: 5px;
651 668 }
652 669
653 670 &#description {
654 671 min-width: @input-description-minwidth;
655 672 min-height: 1em;
656 673 padding: 10px;
657 674 }
658 675 }
659 676
660 677 .field-sm {
661 678 input,
662 679 textarea {
663 680 padding: 5px;
664 681 }
665 682 }
666 683
667 684 textarea {
668 685 display: block;
669 686 clear: both;
670 687 width: 100%;
671 688 min-height: 100px;
672 689 margin-bottom: @padding;
673 690 .box-sizing(border-box);
674 691 overflow: auto;
675 692 }
676 693
677 694 label {
678 695 font-family: @text-light;
679 696 }
680 697
681 698 // GRAVATARS
682 699 // centers gravatar on username to the right
683 700
684 701 .gravatar {
685 702 display: inline;
686 703 min-width: 16px;
687 704 min-height: 16px;
688 705 margin: -5px 0;
689 706 padding: 0;
690 707 line-height: 1em;
691 708 border: 1px solid @grey4;
692 709 box-sizing: content-box;
693 710
694 711 &.gravatar-large {
695 712 margin: -0.5em .25em -0.5em 0;
696 713 }
697 714
698 715 & + .user {
699 716 display: inline;
700 717 margin: 0;
701 718 padding: 0 0 0 .17em;
702 719 line-height: 1em;
703 720 }
704 721 }
705 722
706 723 .user-inline-data {
707 724 display: inline-block;
708 725 float: left;
709 726 padding-left: .5em;
710 727 line-height: 1.3em;
711 728 }
712 729
713 730 .rc-user { // gravatar + user wrapper
714 731 float: left;
715 732 position: relative;
716 733 min-width: 100px;
717 734 max-width: 200px;
718 735 min-height: (@gravatar-size + @border-thickness * 2); // account for border
719 736 display: block;
720 737 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
721 738
722 739
723 740 .gravatar {
724 741 display: block;
725 742 position: absolute;
726 743 top: 0;
727 744 left: 0;
728 745 min-width: @gravatar-size;
729 746 min-height: @gravatar-size;
730 747 margin: 0;
731 748 }
732 749
733 750 .user {
734 751 display: block;
735 752 max-width: 175px;
736 753 padding-top: 2px;
737 754 overflow: hidden;
738 755 text-overflow: ellipsis;
739 756 }
740 757 }
741 758
742 759 .gist-gravatar,
743 760 .journal_container {
744 761 .gravatar-large {
745 762 margin: 0 .5em -10px 0;
746 763 }
747 764 }
748 765
749 766
750 767 // ADMIN SETTINGS
751 768
752 769 // Tag Patterns
753 770 .tag_patterns {
754 771 .tag_input {
755 772 margin-bottom: @padding;
756 773 }
757 774 }
758 775
759 776 .locked_input {
760 777 position: relative;
761 778
762 779 input {
763 780 display: inline;
764 781 margin: 3px 5px 0px 0px;
765 782 }
766 783
767 784 br {
768 785 display: none;
769 786 }
770 787
771 788 .error-message {
772 789 float: left;
773 790 width: 100%;
774 791 }
775 792
776 793 .lock_input_button {
777 794 display: inline;
778 795 }
779 796
780 797 .help-block {
781 798 clear: both;
782 799 }
783 800 }
784 801
785 802 // Notifications
786 803
787 804 .notifications_buttons {
788 805 margin: 0 0 @space 0;
789 806 padding: 0;
790 807
791 808 .btn {
792 809 display: inline-block;
793 810 }
794 811 }
795 812
796 813 .notification-list {
797 814
798 815 div {
799 816 display: inline-block;
800 817 vertical-align: middle;
801 818 }
802 819
803 820 .container {
804 821 display: block;
805 822 margin: 0 0 @padding 0;
806 823 }
807 824
808 825 .delete-notifications {
809 826 margin-left: @padding;
810 827 text-align: right;
811 828 cursor: pointer;
812 829 }
813 830
814 831 .read-notifications {
815 832 margin-left: @padding/2;
816 833 text-align: right;
817 834 width: 35px;
818 835 cursor: pointer;
819 836 }
820 837
821 838 .icon-minus-sign {
822 839 color: @alert2;
823 840 }
824 841
825 842 .icon-ok-sign {
826 843 color: @alert1;
827 844 }
828 845 }
829 846
830 847 .user_settings {
831 848 float: left;
832 849 clear: both;
833 850 display: block;
834 851 width: 100%;
835 852
836 853 .gravatar_box {
837 854 margin-bottom: @padding;
838 855
839 856 &:after {
840 857 content: " ";
841 858 clear: both;
842 859 width: 100%;
843 860 }
844 861 }
845 862
846 863 .fields .field {
847 864 clear: both;
848 865 }
849 866 }
850 867
851 868 .advanced_settings {
852 869 margin-bottom: @space;
853 870
854 871 .help-block {
855 872 margin-left: 0;
856 873 }
857 874
858 875 button + .help-block {
859 876 margin-top: @padding;
860 877 }
861 878 }
862 879
863 880 // admin settings radio buttons and labels
864 881 .label-2 {
865 882 float: left;
866 883 width: @label2-width;
867 884
868 885 label {
869 886 color: @grey1;
870 887 }
871 888 }
872 889 .checkboxes {
873 890 float: left;
874 891 width: @checkboxes-width;
875 892 margin-bottom: @padding;
876 893
877 894 .checkbox {
878 895 width: 100%;
879 896
880 897 label {
881 898 margin: 0;
882 899 padding: 0;
883 900 }
884 901 }
885 902
886 903 .checkbox + .checkbox {
887 904 display: inline-block;
888 905 }
889 906
890 907 label {
891 908 margin-right: 1em;
892 909 }
893 910 }
894 911
895 912 // CHANGELOG
896 913 .container_header {
897 914 float: left;
898 915 display: block;
899 916 width: 100%;
900 917 margin: @padding 0 @padding;
901 918
902 919 #filter_changelog {
903 920 float: left;
904 921 margin-right: @padding;
905 922 }
906 923
907 924 .breadcrumbs_light {
908 925 display: inline-block;
909 926 }
910 927 }
911 928
912 929 .info_box {
913 930 float: right;
914 931 }
915 932
916 933
917 934 #graph_nodes {
918 935 padding-top: 43px;
919 936 }
920 937
921 938 #graph_content{
922 939
923 940 // adjust for table headers so that graph renders properly
924 941 // #graph_nodes padding - table cell padding
925 942 padding-top: (@space - (@basefontsize * 2.4));
926 943
927 944 &.graph_full_width {
928 945 width: 100%;
929 946 max-width: 100%;
930 947 }
931 948 }
932 949
933 950 #graph {
934 951 .flag_status {
935 952 margin: 0;
936 953 }
937 954
938 955 .pagination-left {
939 956 float: left;
940 957 clear: both;
941 958 }
942 959
943 960 .log-container {
944 961 max-width: 345px;
945 962
946 963 .message{
947 964 max-width: 340px;
948 965 }
949 966 }
950 967
951 968 .graph-col-wrapper {
952 969 padding-left: 110px;
953 970
954 971 #graph_nodes {
955 972 width: 100px;
956 973 margin-left: -110px;
957 974 float: left;
958 975 clear: left;
959 976 }
960 977 }
961 978
962 979 .load-more-commits {
963 980 text-align: center;
964 981 }
965 982 .load-more-commits:hover {
966 983 background-color: @grey7;
967 984 }
968 985 .load-more-commits {
969 986 a {
970 987 display: block;
971 988 }
972 989 }
973 990 }
974 991
975 992 #filter_changelog {
976 993 float: left;
977 994 }
978 995
979 996
980 997 //--- THEME ------------------//
981 998
982 999 #logo {
983 1000 float: left;
984 1001 margin: 9px 0 0 0;
985 1002
986 1003 .header {
987 1004 background-color: transparent;
988 1005 }
989 1006
990 1007 a {
991 1008 display: inline-block;
992 1009 }
993 1010
994 1011 img {
995 1012 height:30px;
996 1013 }
997 1014 }
998 1015
999 1016 .logo-wrapper {
1000 1017 float:left;
1001 1018 }
1002 1019
1003 1020 .branding{
1004 1021 float: left;
1005 1022 padding: 9px 2px;
1006 1023 line-height: 1em;
1007 1024 font-size: @navigation-fontsize;
1008 1025 }
1009 1026
1010 1027 img {
1011 1028 border: none;
1012 1029 outline: none;
1013 1030 }
1014 1031 user-profile-header
1015 1032 label {
1016 1033
1017 1034 input[type="checkbox"] {
1018 1035 margin-right: 1em;
1019 1036 }
1020 1037 input[type="radio"] {
1021 1038 margin-right: 1em;
1022 1039 }
1023 1040 }
1024 1041
1025 1042 .flag_status {
1026 1043 margin: 2px 8px 6px 2px;
1027 1044 &.under_review {
1028 1045 .circle(5px, @alert3);
1029 1046 }
1030 1047 &.approved {
1031 1048 .circle(5px, @alert1);
1032 1049 }
1033 1050 &.rejected,
1034 1051 &.forced_closed{
1035 1052 .circle(5px, @alert2);
1036 1053 }
1037 1054 &.not_reviewed {
1038 1055 .circle(5px, @grey5);
1039 1056 }
1040 1057 }
1041 1058
1042 1059 .flag_status_comment_box {
1043 1060 margin: 5px 6px 0px 2px;
1044 1061 }
1045 1062 .test_pattern_preview {
1046 1063 margin: @space 0;
1047 1064
1048 1065 p {
1049 1066 margin-bottom: 0;
1050 1067 border-bottom: @border-thickness solid @border-default-color;
1051 1068 color: @grey3;
1052 1069 }
1053 1070
1054 1071 .btn {
1055 1072 margin-bottom: @padding;
1056 1073 }
1057 1074 }
1058 1075 #test_pattern_result {
1059 1076 display: none;
1060 1077 &:extend(pre);
1061 1078 padding: .9em;
1062 1079 color: @grey3;
1063 1080 background-color: @grey7;
1064 1081 border-right: @border-thickness solid @border-default-color;
1065 1082 border-bottom: @border-thickness solid @border-default-color;
1066 1083 border-left: @border-thickness solid @border-default-color;
1067 1084 }
1068 1085
1069 1086 #repo_vcs_settings {
1070 1087 #inherit_overlay_vcs_default {
1071 1088 display: none;
1072 1089 }
1073 1090 #inherit_overlay_vcs_custom {
1074 1091 display: custom;
1075 1092 }
1076 1093 &.inherited {
1077 1094 #inherit_overlay_vcs_default {
1078 1095 display: block;
1079 1096 }
1080 1097 #inherit_overlay_vcs_custom {
1081 1098 display: none;
1082 1099 }
1083 1100 }
1084 1101 }
1085 1102
1086 1103 .issue-tracker-link {
1087 1104 color: @rcblue;
1088 1105 }
1089 1106
1090 1107 // Issue Tracker Table Show/Hide
1091 1108 #repo_issue_tracker {
1092 1109 #inherit_overlay {
1093 1110 display: none;
1094 1111 }
1095 1112 #custom_overlay {
1096 1113 display: custom;
1097 1114 }
1098 1115 &.inherited {
1099 1116 #inherit_overlay {
1100 1117 display: block;
1101 1118 }
1102 1119 #custom_overlay {
1103 1120 display: none;
1104 1121 }
1105 1122 }
1106 1123 }
1107 1124 table.issuetracker {
1108 1125 &.readonly {
1109 1126 tr, td {
1110 1127 color: @grey3;
1111 1128 }
1112 1129 }
1113 1130 .edit {
1114 1131 display: none;
1115 1132 }
1116 1133 .editopen {
1117 1134 .edit {
1118 1135 display: inline;
1119 1136 }
1120 1137 .entry {
1121 1138 display: none;
1122 1139 }
1123 1140 }
1124 1141 tr td.td-action {
1125 1142 min-width: 117px;
1126 1143 }
1127 1144 td input {
1128 1145 max-width: none;
1129 1146 min-width: 30px;
1130 1147 width: 80%;
1131 1148 }
1132 1149 .issuetracker_pref input {
1133 1150 width: 40%;
1134 1151 }
1135 1152 input.edit_issuetracker_update {
1136 1153 margin-right: 0;
1137 1154 width: auto;
1138 1155 }
1139 1156 }
1140 1157
1141 1158 table.integrations {
1142 1159 .td-icon {
1143 1160 width: 20px;
1144 1161 .integration-icon {
1145 1162 height: 20px;
1146 1163 width: 20px;
1147 1164 }
1148 1165 }
1149 1166 }
1150 1167
1151 1168 .integrations {
1152 1169 a.integration-box {
1153 1170 color: @text-color;
1154 1171 &:hover {
1155 1172 .panel {
1156 1173 background: #fbfbfb;
1157 1174 }
1158 1175 }
1159 1176 .integration-icon {
1160 1177 width: 30px;
1161 1178 height: 30px;
1162 1179 margin-right: 20px;
1163 1180 float: left;
1164 1181 }
1165 1182
1166 1183 .panel-body {
1167 1184 padding: 10px;
1168 1185 }
1169 1186 .panel {
1170 1187 margin-bottom: 10px;
1171 1188 }
1172 1189 h2 {
1173 1190 display: inline-block;
1174 1191 margin: 0;
1175 1192 min-width: 140px;
1176 1193 }
1177 1194 }
1178 1195 }
1179 1196
1180 1197 //Permissions Settings
1181 1198 #add_perm {
1182 1199 margin: 0 0 @padding;
1183 1200 cursor: pointer;
1184 1201 }
1185 1202
1186 1203 .perm_ac {
1187 1204 input {
1188 1205 width: 95%;
1189 1206 }
1190 1207 }
1191 1208
1192 1209 .autocomplete-suggestions {
1193 1210 width: auto !important; // overrides autocomplete.js
1194 1211 margin: 0;
1195 1212 border: @border-thickness solid @rcblue;
1196 1213 border-radius: @border-radius;
1197 1214 color: @rcblue;
1198 1215 background-color: white;
1199 1216 }
1200 1217 .autocomplete-selected {
1201 1218 background: #F0F0F0;
1202 1219 }
1203 1220 .ac-container-wrap {
1204 1221 margin: 0;
1205 1222 padding: 8px;
1206 1223 border-bottom: @border-thickness solid @rclightblue;
1207 1224 list-style-type: none;
1208 1225 cursor: pointer;
1209 1226
1210 1227 &:hover {
1211 1228 background-color: @rclightblue;
1212 1229 }
1213 1230
1214 1231 img {
1215 1232 height: @gravatar-size;
1216 1233 width: @gravatar-size;
1217 1234 margin-right: 1em;
1218 1235 }
1219 1236
1220 1237 strong {
1221 1238 font-weight: normal;
1222 1239 }
1223 1240 }
1224 1241
1225 1242 // Settings Dropdown
1226 1243 .user-menu .container {
1227 1244 padding: 0 4px;
1228 1245 margin: 0;
1229 1246 }
1230 1247
1231 1248 .user-menu .gravatar {
1232 1249 cursor: pointer;
1233 1250 }
1234 1251
1235 1252 .codeblock {
1236 1253 margin-bottom: @padding;
1237 1254 clear: both;
1238 1255
1239 1256 .stats{
1240 1257 overflow: hidden;
1241 1258 }
1242 1259
1243 1260 .message{
1244 1261 textarea{
1245 1262 margin: 0;
1246 1263 }
1247 1264 }
1248 1265
1249 1266 .code-header {
1250 1267 .stats {
1251 1268 line-height: 2em;
1252 1269
1253 1270 .revision_id {
1254 1271 margin-left: 0;
1255 1272 }
1256 1273 .buttons {
1257 1274 padding-right: 0;
1258 1275 }
1259 1276 }
1260 1277
1261 1278 .item{
1262 1279 margin-right: 0.5em;
1263 1280 }
1264 1281 }
1265 1282
1266 1283 #editor_container{
1267 1284 position: relative;
1268 1285 margin: @padding;
1269 1286 }
1270 1287 }
1271 1288
1272 1289 #file_history_container {
1273 1290 display: none;
1274 1291 }
1275 1292
1276 1293 .file-history-inner {
1277 1294 margin-bottom: 10px;
1278 1295 }
1279 1296
1280 1297 // Pull Requests
1281 1298 .summary-details {
1282 1299 width: 72%;
1283 1300 }
1284 1301 .pr-summary {
1285 1302 border-bottom: @border-thickness solid @grey5;
1286 1303 margin-bottom: @space;
1287 1304 }
1288 1305 .reviewers-title {
1289 1306 width: 25%;
1290 1307 min-width: 200px;
1291 1308 }
1292 1309 .reviewers {
1293 1310 width: 25%;
1294 1311 min-width: 200px;
1295 1312 }
1296 1313 .reviewers ul li {
1297 1314 position: relative;
1298 1315 width: 100%;
1299 1316 margin-bottom: 8px;
1300 1317 }
1318
1319 .reviewer_entry {
1320 min-height: 55px;
1321 }
1322
1301 1323 .reviewers_member {
1302 1324 width: 100%;
1303 1325 overflow: auto;
1304 1326 }
1305 1327 .reviewer_reason {
1306 1328 padding-left: 20px;
1307 1329 }
1308 1330 .reviewer_status {
1309 1331 display: inline-block;
1310 1332 vertical-align: top;
1311 1333 width: 7%;
1312 1334 min-width: 20px;
1313 1335 height: 1.2em;
1314 1336 margin-top: 3px;
1315 1337 line-height: 1em;
1316 1338 }
1317 1339
1318 1340 .reviewer_name {
1319 1341 display: inline-block;
1320 1342 max-width: 83%;
1321 1343 padding-right: 20px;
1322 1344 vertical-align: middle;
1323 1345 line-height: 1;
1324 1346
1325 1347 .rc-user {
1326 1348 min-width: 0;
1327 1349 margin: -2px 1em 0 0;
1328 1350 }
1329 1351
1330 1352 .reviewer {
1331 1353 float: left;
1332 1354 }
1333 1355 }
1334 1356
1357 .reviewer_member_mandatory,
1358 .reviewer_member_mandatory_remove,
1335 1359 .reviewer_member_remove {
1336 1360 position: absolute;
1337 1361 right: 0;
1338 1362 top: 0;
1339 1363 width: 16px;
1340 1364 margin-bottom: 10px;
1341 1365 padding: 0;
1342 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 1377 .reviewer_member_status {
1345 1378 margin-top: 5px;
1346 1379 }
1347 1380 .pr-summary #summary{
1348 1381 width: 100%;
1349 1382 }
1350 1383 .pr-summary .action_button:hover {
1351 1384 border: 0;
1352 1385 cursor: pointer;
1353 1386 }
1354 1387 .pr-details-title {
1355 1388 padding-bottom: 8px;
1356 1389 border-bottom: @border-thickness solid @grey5;
1357 1390
1358 1391 .action_button.disabled {
1359 1392 color: @grey4;
1360 1393 cursor: inherit;
1361 1394 }
1362 1395 .action_button {
1363 1396 color: @rcblue;
1364 1397 }
1365 1398 }
1366 1399 .pr-details-content {
1367 1400 margin-top: @textmargin;
1368 1401 margin-bottom: @textmargin;
1369 1402 }
1370 1403 .pr-description {
1371 1404 white-space:pre-wrap;
1372 1405 }
1406
1407 .pr-reviewer-rules {
1408 padding: 10px 0px 20px 0px;
1409 }
1410
1373 1411 .group_members {
1374 1412 margin-top: 0;
1375 1413 padding: 0;
1376 1414 list-style: outside none none;
1377 1415
1378 1416 img {
1379 1417 height: @gravatar-size;
1380 1418 width: @gravatar-size;
1381 1419 margin-right: .5em;
1382 1420 margin-left: 3px;
1383 1421 }
1384 1422
1385 1423 .to-delete {
1386 1424 .user {
1387 1425 text-decoration: line-through;
1388 1426 }
1389 1427 }
1390 1428 }
1391 1429
1392 1430 .compare_view_commits_title {
1393 1431 .disabled {
1394 1432 cursor: inherit;
1395 1433 &:hover{
1396 1434 background-color: inherit;
1397 1435 color: inherit;
1398 1436 }
1399 1437 }
1400 1438 }
1401 1439
1402 1440 .subtitle-compare {
1403 1441 margin: -15px 0px 0px 0px;
1404 1442 }
1405 1443
1406 1444 .comments-summary-td {
1407 1445 border-top: 1px dashed @grey5;
1408 1446 }
1409 1447
1410 1448 // new entry in group_members
1411 1449 .td-author-new-entry {
1412 1450 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1413 1451 }
1414 1452
1415 1453 .usergroup_member_remove {
1416 1454 width: 16px;
1417 1455 margin-bottom: 10px;
1418 1456 padding: 0;
1419 1457 color: black !important;
1420 1458 cursor: pointer;
1421 1459 }
1422 1460
1423 1461 .reviewer_ac .ac-input {
1424 1462 width: 92%;
1425 1463 margin-bottom: 1em;
1426 1464 }
1427 1465
1428 1466 .compare_view_commits tr{
1429 1467 height: 20px;
1430 1468 }
1431 1469 .compare_view_commits td {
1432 1470 vertical-align: top;
1433 1471 padding-top: 10px;
1434 1472 }
1435 1473 .compare_view_commits .author {
1436 1474 margin-left: 5px;
1437 1475 }
1438 1476
1439 1477 .compare_view_commits {
1440 1478 .color-a {
1441 1479 color: @alert1;
1442 1480 }
1443 1481
1444 1482 .color-c {
1445 1483 color: @color3;
1446 1484 }
1447 1485
1448 1486 .color-r {
1449 1487 color: @color5;
1450 1488 }
1451 1489
1452 1490 .color-a-bg {
1453 1491 background-color: @alert1;
1454 1492 }
1455 1493
1456 1494 .color-c-bg {
1457 1495 background-color: @alert3;
1458 1496 }
1459 1497
1460 1498 .color-r-bg {
1461 1499 background-color: @alert2;
1462 1500 }
1463 1501
1464 1502 .color-a-border {
1465 1503 border: 1px solid @alert1;
1466 1504 }
1467 1505
1468 1506 .color-c-border {
1469 1507 border: 1px solid @alert3;
1470 1508 }
1471 1509
1472 1510 .color-r-border {
1473 1511 border: 1px solid @alert2;
1474 1512 }
1475 1513
1476 1514 .commit-change-indicator {
1477 1515 width: 15px;
1478 1516 height: 15px;
1479 1517 position: relative;
1480 1518 left: 15px;
1481 1519 }
1482 1520
1483 1521 .commit-change-content {
1484 1522 text-align: center;
1485 1523 vertical-align: middle;
1486 1524 line-height: 15px;
1487 1525 }
1488 1526 }
1489 1527
1490 1528 .compare_view_files {
1491 1529 width: 100%;
1492 1530
1493 1531 td {
1494 1532 vertical-align: middle;
1495 1533 }
1496 1534 }
1497 1535
1498 1536 .compare_view_filepath {
1499 1537 color: @grey1;
1500 1538 }
1501 1539
1502 1540 .show_more {
1503 1541 display: inline-block;
1504 1542 position: relative;
1505 1543 vertical-align: middle;
1506 1544 width: 4px;
1507 1545 height: @basefontsize;
1508 1546
1509 1547 &:after {
1510 1548 content: "\00A0\25BE";
1511 1549 display: inline-block;
1512 1550 width:10px;
1513 1551 line-height: 5px;
1514 1552 font-size: 12px;
1515 1553 cursor: pointer;
1516 1554 }
1517 1555 }
1518 1556
1519 1557 .journal_more .show_more {
1520 1558 display: inline;
1521 1559
1522 1560 &:after {
1523 1561 content: none;
1524 1562 }
1525 1563 }
1526 1564
1527 1565 .open .show_more:after,
1528 1566 .select2-dropdown-open .show_more:after {
1529 1567 .rotate(180deg);
1530 1568 margin-left: 4px;
1531 1569 }
1532 1570
1533 1571
1534 1572 .compare_view_commits .collapse_commit:after {
1535 1573 cursor: pointer;
1536 1574 content: "\00A0\25B4";
1537 1575 margin-left: -3px;
1538 1576 font-size: 17px;
1539 1577 color: @grey4;
1540 1578 }
1541 1579
1542 1580 .diff_links {
1543 1581 margin-left: 8px;
1544 1582 }
1545 1583
1546 1584 div.ancestor {
1547 1585 margin: -30px 0px;
1548 1586 }
1549 1587
1550 1588 .cs_icon_td input[type="checkbox"] {
1551 1589 display: none;
1552 1590 }
1553 1591
1554 1592 .cs_icon_td .expand_file_icon:after {
1555 1593 cursor: pointer;
1556 1594 content: "\00A0\25B6";
1557 1595 font-size: 12px;
1558 1596 color: @grey4;
1559 1597 }
1560 1598
1561 1599 .cs_icon_td .collapse_file_icon:after {
1562 1600 cursor: pointer;
1563 1601 content: "\00A0\25BC";
1564 1602 font-size: 12px;
1565 1603 color: @grey4;
1566 1604 }
1567 1605
1568 1606 /*new binary
1569 1607 NEW_FILENODE = 1
1570 1608 DEL_FILENODE = 2
1571 1609 MOD_FILENODE = 3
1572 1610 RENAMED_FILENODE = 4
1573 1611 COPIED_FILENODE = 5
1574 1612 CHMOD_FILENODE = 6
1575 1613 BIN_FILENODE = 7
1576 1614 */
1577 1615 .cs_files_expand {
1578 1616 font-size: @basefontsize + 5px;
1579 1617 line-height: 1.8em;
1580 1618 float: right;
1581 1619 }
1582 1620
1583 1621 .cs_files_expand span{
1584 1622 color: @rcblue;
1585 1623 cursor: pointer;
1586 1624 }
1587 1625 .cs_files {
1588 1626 clear: both;
1589 1627 padding-bottom: @padding;
1590 1628
1591 1629 .cur_cs {
1592 1630 margin: 10px 2px;
1593 1631 font-weight: bold;
1594 1632 }
1595 1633
1596 1634 .node {
1597 1635 float: left;
1598 1636 }
1599 1637
1600 1638 .changes {
1601 1639 float: right;
1602 1640 color: white;
1603 1641 font-size: @basefontsize - 4px;
1604 1642 margin-top: 4px;
1605 1643 opacity: 0.6;
1606 1644 filter: Alpha(opacity=60); /* IE8 and earlier */
1607 1645
1608 1646 .added {
1609 1647 background-color: @alert1;
1610 1648 float: left;
1611 1649 text-align: center;
1612 1650 }
1613 1651
1614 1652 .deleted {
1615 1653 background-color: @alert2;
1616 1654 float: left;
1617 1655 text-align: center;
1618 1656 }
1619 1657
1620 1658 .bin {
1621 1659 background-color: @alert1;
1622 1660 text-align: center;
1623 1661 }
1624 1662
1625 1663 /*new binary*/
1626 1664 .bin.bin1 {
1627 1665 background-color: @alert1;
1628 1666 text-align: center;
1629 1667 }
1630 1668
1631 1669 /*deleted binary*/
1632 1670 .bin.bin2 {
1633 1671 background-color: @alert2;
1634 1672 text-align: center;
1635 1673 }
1636 1674
1637 1675 /*mod binary*/
1638 1676 .bin.bin3 {
1639 1677 background-color: @grey2;
1640 1678 text-align: center;
1641 1679 }
1642 1680
1643 1681 /*rename file*/
1644 1682 .bin.bin4 {
1645 1683 background-color: @alert4;
1646 1684 text-align: center;
1647 1685 }
1648 1686
1649 1687 /*copied file*/
1650 1688 .bin.bin5 {
1651 1689 background-color: @alert4;
1652 1690 text-align: center;
1653 1691 }
1654 1692
1655 1693 /*chmod file*/
1656 1694 .bin.bin6 {
1657 1695 background-color: @grey2;
1658 1696 text-align: center;
1659 1697 }
1660 1698 }
1661 1699 }
1662 1700
1663 1701 .cs_files .cs_added, .cs_files .cs_A,
1664 1702 .cs_files .cs_added, .cs_files .cs_M,
1665 1703 .cs_files .cs_added, .cs_files .cs_D {
1666 1704 height: 16px;
1667 1705 padding-right: 10px;
1668 1706 margin-top: 7px;
1669 1707 text-align: left;
1670 1708 }
1671 1709
1672 1710 .cs_icon_td {
1673 1711 min-width: 16px;
1674 1712 width: 16px;
1675 1713 }
1676 1714
1677 1715 .pull-request-merge {
1678 1716 border: 1px solid @grey5;
1679 1717 padding: 10px 0px 20px;
1680 1718 margin-top: 10px;
1681 1719 margin-bottom: 20px;
1682 1720 }
1683 1721
1684 1722 .pull-request-merge ul {
1685 1723 padding: 0px 0px;
1686 1724 }
1687 1725
1688 1726 .pull-request-merge li:before{
1689 1727 content:none;
1690 1728 }
1691 1729
1692 1730 .pull-request-merge .pull-request-wrap {
1693 1731 height: auto;
1694 1732 padding: 0px 0px;
1695 1733 text-align: right;
1696 1734 }
1697 1735
1698 1736 .pull-request-merge span {
1699 1737 margin-right: 5px;
1700 1738 }
1701 1739
1702 1740 .pull-request-merge-actions {
1703 1741 height: 30px;
1704 1742 padding: 0px 0px;
1705 1743 }
1706 1744
1707 1745 .merge-status {
1708 1746 margin-right: 5px;
1709 1747 }
1710 1748
1711 1749 .merge-message {
1712 1750 font-size: 1.2em
1713 1751 }
1714 1752
1715 1753 .merge-message.success i,
1716 1754 .merge-icon.success i {
1717 1755 color:@alert1;
1718 1756 }
1719 1757
1720 1758 .merge-message.warning i,
1721 1759 .merge-icon.warning i {
1722 1760 color: @alert3;
1723 1761 }
1724 1762
1725 1763 .merge-message.error i,
1726 1764 .merge-icon.error i {
1727 1765 color:@alert2;
1728 1766 }
1729 1767
1730 1768 .pr-versions {
1731 1769 font-size: 1.1em;
1732 1770
1733 1771 table {
1734 1772 padding: 0px 5px;
1735 1773 }
1736 1774
1737 1775 td {
1738 1776 line-height: 15px;
1739 1777 }
1740 1778
1741 1779 .flag_status {
1742 1780 margin: 0;
1743 1781 }
1744 1782
1745 1783 .compare-radio-button {
1746 1784 position: relative;
1747 1785 top: -3px;
1748 1786 }
1749 1787 }
1750 1788
1751 1789
1752 1790 #close_pull_request {
1753 1791 margin-right: 0px;
1754 1792 }
1755 1793
1756 1794 .empty_data {
1757 1795 color: @grey4;
1758 1796 }
1759 1797
1760 1798 #changeset_compare_view_content {
1761 1799 margin-bottom: @space;
1762 1800 clear: both;
1763 1801 width: 100%;
1764 1802 box-sizing: border-box;
1765 1803 .border-radius(@border-radius);
1766 1804
1767 1805 .help-block {
1768 1806 margin: @padding 0;
1769 1807 color: @text-color;
1770 1808 }
1771 1809
1772 1810 .empty_data {
1773 1811 margin: @padding 0;
1774 1812 }
1775 1813
1776 1814 .alert {
1777 1815 margin-bottom: @space;
1778 1816 }
1779 1817 }
1780 1818
1781 1819 .table_disp {
1782 1820 .status {
1783 1821 width: auto;
1784 1822
1785 1823 .flag_status {
1786 1824 float: left;
1787 1825 }
1788 1826 }
1789 1827 }
1790 1828
1791 1829 .status_box_menu {
1792 1830 margin: 0;
1793 1831 }
1794 1832
1795 1833 .notification-table{
1796 1834 margin-bottom: @space;
1797 1835 display: table;
1798 1836 width: 100%;
1799 1837
1800 1838 .container{
1801 1839 display: table-row;
1802 1840
1803 1841 .notification-header{
1804 1842 border-bottom: @border-thickness solid @border-default-color;
1805 1843 }
1806 1844
1807 1845 .notification-subject{
1808 1846 display: table-cell;
1809 1847 }
1810 1848 }
1811 1849 }
1812 1850
1813 1851 // Notifications
1814 1852 .notification-header{
1815 1853 display: table;
1816 1854 width: 100%;
1817 1855 padding: floor(@basefontsize/2) 0;
1818 1856 line-height: 1em;
1819 1857
1820 1858 .desc, .delete-notifications, .read-notifications{
1821 1859 display: table-cell;
1822 1860 text-align: left;
1823 1861 }
1824 1862
1825 1863 .desc{
1826 1864 width: 1163px;
1827 1865 }
1828 1866
1829 1867 .delete-notifications, .read-notifications{
1830 1868 width: 35px;
1831 1869 min-width: 35px; //fixes when only one button is displayed
1832 1870 }
1833 1871 }
1834 1872
1835 1873 .notification-body {
1836 1874 .markdown-block,
1837 1875 .rst-block {
1838 1876 padding: @padding 0;
1839 1877 }
1840 1878
1841 1879 .notification-subject {
1842 1880 padding: @textmargin 0;
1843 1881 border-bottom: @border-thickness solid @border-default-color;
1844 1882 }
1845 1883 }
1846 1884
1847 1885
1848 1886 .notifications_buttons{
1849 1887 float: right;
1850 1888 }
1851 1889
1852 1890 #notification-status{
1853 1891 display: inline;
1854 1892 }
1855 1893
1856 1894 // Repositories
1857 1895
1858 1896 #summary.fields{
1859 1897 display: table;
1860 1898
1861 1899 .field{
1862 1900 display: table-row;
1863 1901
1864 1902 .label-summary{
1865 1903 display: table-cell;
1866 1904 min-width: @label-summary-minwidth;
1867 1905 padding-top: @padding/2;
1868 1906 padding-bottom: @padding/2;
1869 1907 padding-right: @padding/2;
1870 1908 }
1871 1909
1872 1910 .input{
1873 1911 display: table-cell;
1874 1912 padding: @padding/2;
1875 1913
1876 1914 input{
1877 1915 min-width: 29em;
1878 1916 padding: @padding/4;
1879 1917 }
1880 1918 }
1881 1919 .statistics, .downloads{
1882 1920 .disabled{
1883 1921 color: @grey4;
1884 1922 }
1885 1923 }
1886 1924 }
1887 1925 }
1888 1926
1889 1927 #summary{
1890 1928 width: 70%;
1891 1929 }
1892 1930
1893 1931
1894 1932 // Journal
1895 1933 .journal.title {
1896 1934 h5 {
1897 1935 float: left;
1898 1936 margin: 0;
1899 1937 width: 70%;
1900 1938 }
1901 1939
1902 1940 ul {
1903 1941 float: right;
1904 1942 display: inline-block;
1905 1943 margin: 0;
1906 1944 width: 30%;
1907 1945 text-align: right;
1908 1946
1909 1947 li {
1910 1948 display: inline;
1911 1949 font-size: @journal-fontsize;
1912 1950 line-height: 1em;
1913 1951
1914 1952 &:before { content: none; }
1915 1953 }
1916 1954 }
1917 1955 }
1918 1956
1919 1957 .filterexample {
1920 1958 position: absolute;
1921 1959 top: 95px;
1922 1960 left: @contentpadding;
1923 1961 color: @rcblue;
1924 1962 font-size: 11px;
1925 1963 font-family: @text-regular;
1926 1964 cursor: help;
1927 1965
1928 1966 &:hover {
1929 1967 color: @rcdarkblue;
1930 1968 }
1931 1969
1932 1970 @media (max-width:768px) {
1933 1971 position: relative;
1934 1972 top: auto;
1935 1973 left: auto;
1936 1974 display: block;
1937 1975 }
1938 1976 }
1939 1977
1940 1978
1941 1979 #journal{
1942 1980 margin-bottom: @space;
1943 1981
1944 1982 .journal_day{
1945 1983 margin-bottom: @textmargin/2;
1946 1984 padding-bottom: @textmargin/2;
1947 1985 font-size: @journal-fontsize;
1948 1986 border-bottom: @border-thickness solid @border-default-color;
1949 1987 }
1950 1988
1951 1989 .journal_container{
1952 1990 margin-bottom: @space;
1953 1991
1954 1992 .journal_user{
1955 1993 display: inline-block;
1956 1994 }
1957 1995 .journal_action_container{
1958 1996 display: block;
1959 1997 margin-top: @textmargin;
1960 1998
1961 1999 div{
1962 2000 display: inline;
1963 2001 }
1964 2002
1965 2003 div.journal_action_params{
1966 2004 display: block;
1967 2005 }
1968 2006
1969 2007 div.journal_repo:after{
1970 2008 content: "\A";
1971 2009 white-space: pre;
1972 2010 }
1973 2011
1974 2012 div.date{
1975 2013 display: block;
1976 2014 margin-bottom: @textmargin;
1977 2015 }
1978 2016 }
1979 2017 }
1980 2018 }
1981 2019
1982 2020 // Files
1983 2021 .edit-file-title {
1984 2022 border-bottom: @border-thickness solid @border-default-color;
1985 2023
1986 2024 .breadcrumbs {
1987 2025 margin-bottom: 0;
1988 2026 }
1989 2027 }
1990 2028
1991 2029 .edit-file-fieldset {
1992 2030 margin-top: @sidebarpadding;
1993 2031
1994 2032 .fieldset {
1995 2033 .left-label {
1996 2034 width: 13%;
1997 2035 }
1998 2036 .right-content {
1999 2037 width: 87%;
2000 2038 max-width: 100%;
2001 2039 }
2002 2040 .filename-label {
2003 2041 margin-top: 13px;
2004 2042 }
2005 2043 .commit-message-label {
2006 2044 margin-top: 4px;
2007 2045 }
2008 2046 .file-upload-input {
2009 2047 input {
2010 2048 display: none;
2011 2049 }
2012 2050 margin-top: 10px;
2013 2051 }
2014 2052 .file-upload-label {
2015 2053 margin-top: 10px;
2016 2054 }
2017 2055 p {
2018 2056 margin-top: 5px;
2019 2057 }
2020 2058
2021 2059 }
2022 2060 .custom-path-link {
2023 2061 margin-left: 5px;
2024 2062 }
2025 2063 #commit {
2026 2064 resize: vertical;
2027 2065 }
2028 2066 }
2029 2067
2030 2068 .delete-file-preview {
2031 2069 max-height: 250px;
2032 2070 }
2033 2071
2034 2072 .new-file,
2035 2073 #filter_activate,
2036 2074 #filter_deactivate {
2037 2075 float: left;
2038 2076 margin: 0 0 0 15px;
2039 2077 }
2040 2078
2041 2079 h3.files_location{
2042 2080 line-height: 2.4em;
2043 2081 }
2044 2082
2045 2083 .browser-nav {
2046 2084 display: table;
2047 2085 margin-bottom: @space;
2048 2086
2049 2087
2050 2088 .info_box {
2051 2089 display: inline-table;
2052 2090 height: 2.5em;
2053 2091
2054 2092 .browser-cur-rev, .info_box_elem {
2055 2093 display: table-cell;
2056 2094 vertical-align: middle;
2057 2095 }
2058 2096
2059 2097 .info_box_elem {
2060 2098 border-top: @border-thickness solid @rcblue;
2061 2099 border-bottom: @border-thickness solid @rcblue;
2062 2100
2063 2101 #at_rev, a {
2064 2102 padding: 0.6em 0.9em;
2065 2103 margin: 0;
2066 2104 .box-shadow(none);
2067 2105 border: 0;
2068 2106 height: 12px;
2069 2107 }
2070 2108
2071 2109 input#at_rev {
2072 2110 max-width: 50px;
2073 2111 text-align: right;
2074 2112 }
2075 2113
2076 2114 &.previous {
2077 2115 border: @border-thickness solid @rcblue;
2078 2116 .disabled {
2079 2117 color: @grey4;
2080 2118 cursor: not-allowed;
2081 2119 }
2082 2120 }
2083 2121
2084 2122 &.next {
2085 2123 border: @border-thickness solid @rcblue;
2086 2124 .disabled {
2087 2125 color: @grey4;
2088 2126 cursor: not-allowed;
2089 2127 }
2090 2128 }
2091 2129 }
2092 2130
2093 2131 .browser-cur-rev {
2094 2132
2095 2133 span{
2096 2134 margin: 0;
2097 2135 color: @rcblue;
2098 2136 height: 12px;
2099 2137 display: inline-block;
2100 2138 padding: 0.7em 1em ;
2101 2139 border: @border-thickness solid @rcblue;
2102 2140 margin-right: @padding;
2103 2141 }
2104 2142 }
2105 2143 }
2106 2144
2107 2145 .search_activate {
2108 2146 display: table-cell;
2109 2147 vertical-align: middle;
2110 2148
2111 2149 input, label{
2112 2150 margin: 0;
2113 2151 padding: 0;
2114 2152 }
2115 2153
2116 2154 input{
2117 2155 margin-left: @textmargin;
2118 2156 }
2119 2157
2120 2158 }
2121 2159 }
2122 2160
2123 2161 .browser-cur-rev{
2124 2162 margin-bottom: @textmargin;
2125 2163 }
2126 2164
2127 2165 #node_filter_box_loading{
2128 2166 .info_text;
2129 2167 }
2130 2168
2131 2169 .browser-search {
2132 2170 margin: -25px 0px 5px 0px;
2133 2171 }
2134 2172
2135 2173 .node-filter {
2136 2174 font-size: @repo-title-fontsize;
2137 2175 padding: 4px 0px 0px 0px;
2138 2176
2139 2177 .node-filter-path {
2140 2178 float: left;
2141 2179 color: @grey4;
2142 2180 }
2143 2181 .node-filter-input {
2144 2182 float: left;
2145 2183 margin: -2px 0px 0px 2px;
2146 2184 input {
2147 2185 padding: 2px;
2148 2186 border: none;
2149 2187 font-size: @repo-title-fontsize;
2150 2188 }
2151 2189 }
2152 2190 }
2153 2191
2154 2192
2155 2193 .browser-result{
2156 2194 td a{
2157 2195 margin-left: 0.5em;
2158 2196 display: inline-block;
2159 2197
2160 2198 em{
2161 2199 font-family: @text-bold;
2162 2200 }
2163 2201 }
2164 2202 }
2165 2203
2166 2204 .browser-highlight{
2167 2205 background-color: @grey5-alpha;
2168 2206 }
2169 2207
2170 2208
2171 2209 // Search
2172 2210
2173 2211 .search-form{
2174 2212 #q {
2175 2213 width: @search-form-width;
2176 2214 }
2177 2215 .fields{
2178 2216 margin: 0 0 @space;
2179 2217 }
2180 2218
2181 2219 label{
2182 2220 display: inline-block;
2183 2221 margin-right: @textmargin;
2184 2222 padding-top: 0.25em;
2185 2223 }
2186 2224
2187 2225
2188 2226 .results{
2189 2227 clear: both;
2190 2228 margin: 0 0 @padding;
2191 2229 }
2192 2230 }
2193 2231
2194 2232 div.search-feedback-items {
2195 2233 display: inline-block;
2196 2234 padding:0px 0px 0px 96px;
2197 2235 }
2198 2236
2199 2237 div.search-code-body {
2200 2238 background-color: #ffffff; padding: 5px 0 5px 10px;
2201 2239 pre {
2202 2240 .match { background-color: #faffa6;}
2203 2241 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2204 2242 }
2205 2243 }
2206 2244
2207 2245 .expand_commit.search {
2208 2246 .show_more.open {
2209 2247 height: auto;
2210 2248 max-height: none;
2211 2249 }
2212 2250 }
2213 2251
2214 2252 .search-results {
2215 2253
2216 2254 h2 {
2217 2255 margin-bottom: 0;
2218 2256 }
2219 2257 .codeblock {
2220 2258 border: none;
2221 2259 background: transparent;
2222 2260 }
2223 2261
2224 2262 .codeblock-header {
2225 2263 border: none;
2226 2264 background: transparent;
2227 2265 }
2228 2266
2229 2267 .code-body {
2230 2268 border: @border-thickness solid @border-default-color;
2231 2269 .border-radius(@border-radius);
2232 2270 }
2233 2271
2234 2272 .td-commit {
2235 2273 &:extend(pre);
2236 2274 border-bottom: @border-thickness solid @border-default-color;
2237 2275 }
2238 2276
2239 2277 .message {
2240 2278 height: auto;
2241 2279 max-width: 350px;
2242 2280 white-space: normal;
2243 2281 text-overflow: initial;
2244 2282 overflow: visible;
2245 2283
2246 2284 .match { background-color: #faffa6;}
2247 2285 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2248 2286 }
2249 2287
2250 2288 }
2251 2289
2252 2290 table.rctable td.td-search-results div {
2253 2291 max-width: 100%;
2254 2292 }
2255 2293
2256 2294 #tip-box, .tip-box{
2257 2295 padding: @menupadding/2;
2258 2296 display: block;
2259 2297 border: @border-thickness solid @border-highlight-color;
2260 2298 .border-radius(@border-radius);
2261 2299 background-color: white;
2262 2300 z-index: 99;
2263 2301 white-space: pre-wrap;
2264 2302 }
2265 2303
2266 2304 #linktt {
2267 2305 width: 79px;
2268 2306 }
2269 2307
2270 2308 #help_kb .modal-content{
2271 2309 max-width: 750px;
2272 2310 margin: 10% auto;
2273 2311
2274 2312 table{
2275 2313 td,th{
2276 2314 border-bottom: none;
2277 2315 line-height: 2.5em;
2278 2316 }
2279 2317 th{
2280 2318 padding-bottom: @textmargin/2;
2281 2319 }
2282 2320 td.keys{
2283 2321 text-align: center;
2284 2322 }
2285 2323 }
2286 2324
2287 2325 .block-left{
2288 2326 width: 45%;
2289 2327 margin-right: 5%;
2290 2328 }
2291 2329 .modal-footer{
2292 2330 clear: both;
2293 2331 }
2294 2332 .key.tag{
2295 2333 padding: 0.5em;
2296 2334 background-color: @rcblue;
2297 2335 color: white;
2298 2336 border-color: @rcblue;
2299 2337 .box-shadow(none);
2300 2338 }
2301 2339 }
2302 2340
2303 2341
2304 2342
2305 2343 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2306 2344
2307 2345 @import 'statistics-graph';
2308 2346 @import 'tables';
2309 2347 @import 'forms';
2310 2348 @import 'diff';
2311 2349 @import 'summary';
2312 2350 @import 'navigation';
2313 2351
2314 2352 //--- SHOW/HIDE SECTIONS --//
2315 2353
2316 2354 .btn-collapse {
2317 2355 float: right;
2318 2356 text-align: right;
2319 2357 font-family: @text-light;
2320 2358 font-size: @basefontsize;
2321 2359 cursor: pointer;
2322 2360 border: none;
2323 2361 color: @rcblue;
2324 2362 }
2325 2363
2326 2364 table.rctable,
2327 2365 table.dataTable {
2328 2366 .btn-collapse {
2329 2367 float: right;
2330 2368 text-align: right;
2331 2369 }
2332 2370 }
2333 2371
2334 2372
2335 2373 // TODO: johbo: Fix for IE10, this avoids that we see a border
2336 2374 // and padding around checkboxes and radio boxes. Move to the right place,
2337 2375 // or better: Remove this once we did the form refactoring.
2338 2376 input[type=checkbox],
2339 2377 input[type=radio] {
2340 2378 padding: 0;
2341 2379 border: none;
2342 2380 }
2343 2381
2344 2382 .toggle-ajax-spinner{
2345 2383 height: 16px;
2346 2384 width: 16px;
2347 2385 }
@@ -1,355 +1,600 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 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
21 31 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
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 }
24 49
25 if(typeof(mark_delete) === undefined){
26 mark_delete = false;
27 }
50 if (msg) {
51 $('#pr_open_message').html(msg);
52 }
53 };
28 54
29 if(mark_delete === true){
30 if (reviewer){
31 // now delete the input
32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
33 // mark as to-delete
34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
35 obj.addClass('to-delete');
36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
37 }
38 }
39 else{
40 $('#reviewer_{0}'.format(reviewer_id)).remove();
41 }
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 {*[]}
72 */
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]
42 91 };
43 92
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
45 var members = $('#review_members').get(0);
46 var reasons_html = '';
47 var reasons_inputs = '';
48 var reasons = reasons || [];
49 if (reasons) {
50 for (var i = 0; i < reasons.length; i++) {
51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
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();
53 187 }
54 }
55 var tmpl = '<li id="reviewer_{2}">'+
56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 '<div class="reviewer_status">'+
58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
59 '</div>'+
60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
61 '<span class="reviewer_name user">{1}</span>'+
62 reasons_html +
63 '<input type="hidden" name="user_id" value="{2}">'+
64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 '{3}'+
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
68 '<i class="icon-remove-sign"></i>'+
69 '</div>'+
70 '</div>'+
71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 '</li>' ;
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) {
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
229
230 if(typeof(mark_delete) === undefined){
231 mark_delete = false;
232 }
233
234 if(mark_delete === true){
235 if (reviewer){
236 // now delete the input
237 $('#reviewer_{0} input'.format(reviewer_id)).remove();
238 // mark as to-delete
239 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
240 obj.addClass('to-delete');
241 obj.css({"text-decoration":"line-through", "opacity": 0.5});
242 }
243 }
244 else{
245 $('#reviewer_{0}'.format(reviewer_id)).remove();
246 }
247 };
248
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
250 var members = self.$reviewMembers.get(0);
251 var reasons_html = '';
252 var reasons_inputs = '';
253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
73 255
74 var displayname = "{0} ({1} {2})".format(
75 nname, escapeHtml(fname), escapeHtml(lname));
76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 // check if we don't have this ID already in
78 var ids = [];
79 var _els = $('#review_members li').toArray();
80 for (el in _els){
81 ids.push(_els[el].id)
82 }
83 if(ids.indexOf('reviewer_'+id) == -1){
84 // only add if it's not there
85 members.innerHTML += element;
86 }
256 if (reasons) {
257 for (var i = 0; i < reasons.length; i++) {
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
260 }
261 }
262 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
265 '<div class="reviewer_status">'+
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
267 '</div>'+
268 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
269 '<span class="reviewer_name user">{1}</span>'+
270 reasons_html +
271 '<input type="hidden" name="user_id" value="{2}">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
273 '{3}'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
279 '<i class="icon-remove-sign"></i>'+
280 '</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 += ''+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
296 '</li>' ;
297
298 var displayname = "{0} ({1} {2})".format(
299 nname, escapeHtml(fname), escapeHtml(lname));
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
301 // check if we don't have this ID already in
302 var ids = [];
303 var _els = self.$reviewMembers.find('li').toArray();
304 for (el in _els){
305 ids.push(_els[el].id)
306 }
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) {
326 // only add if it's not there
327 members.innerHTML += element;
328 }
329
330 };
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 };
87 336
88 337 };
89 338
339
90 340 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
91 341 var url = pyroutes.url(
92 342 'pullrequest_update',
93 343 {"repo_name": repo_name, "pull_request_id": pull_request_id});
94 344 if (typeof postData === 'string' ) {
95 345 postData += '&csrf_token=' + CSRF_TOKEN;
96 346 } else {
97 347 postData.csrf_token = CSRF_TOKEN;
98 348 }
99 349 var success = function(o) {
100 350 window.location.reload();
101 351 };
102 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 356 * PULL REQUEST reject & close
114 357 */
115 358 var closePullRequest = function(repo_name, pull_request_id) {
116 359 var postData = {
117 360 '_method': 'put',
118 361 'close_pull_request': true};
119 362 _updatePullRequest(repo_name, pull_request_id, postData);
120 363 };
121 364
122 365 /**
123 366 * PULL REQUEST update commits
124 367 */
125 368 var updateCommits = function(repo_name, pull_request_id) {
126 369 var postData = {
127 370 '_method': 'put',
128 371 'update_commits': true};
129 372 _updatePullRequest(repo_name, pull_request_id, postData);
130 373 };
131 374
132 375
133 376 /**
134 377 * PULL REQUEST edit info
135 378 */
136 379 var editPullRequest = function(repo_name, pull_request_id, title, description) {
137 380 var url = pyroutes.url(
138 381 'pullrequest_update',
139 382 {"repo_name": repo_name, "pull_request_id": pull_request_id});
140 383
141 384 var postData = {
142 385 '_method': 'put',
143 386 'title': title,
144 387 'description': description,
145 388 'edit_pull_request': true,
146 389 'csrf_token': CSRF_TOKEN
147 390 };
148 391 var success = function(o) {
149 392 window.location.reload();
150 393 };
151 394 ajaxPOST(url, postData, success);
152 395 };
153 396
154 397 var initPullRequestsCodeMirror = function (textAreaId) {
155 398 var ta = $(textAreaId).get(0);
156 399 var initialHeight = '100px';
157 400
158 401 // default options
159 402 var codeMirrorOptions = {
160 403 mode: "text",
161 404 lineNumbers: false,
162 405 indentUnit: 4,
163 406 theme: 'rc-input'
164 407 };
165 408
166 409 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
167 410 // marker for manually set description
168 411 codeMirrorInstance._userDefinedDesc = false;
169 412 codeMirrorInstance.setSize(null, initialHeight);
170 413 codeMirrorInstance.on("change", function(instance, changeObj) {
171 414 var height = initialHeight;
172 415 var lines = instance.lineCount();
173 416 if (lines > 6 && lines < 20) {
174 417 height = "auto"
175 418 }
176 419 else if (lines >= 20) {
177 420 height = 20 * 15;
178 421 }
179 422 instance.setSize(null, height);
180 423
181 424 // detect if the change was trigger by auto desc, or user input
182 425 changeOrigin = changeObj.origin;
183 426
184 427 if (changeOrigin === "setValue") {
185 428 cmLog.debug('Change triggered by setValue');
186 429 }
187 430 else {
188 431 cmLog.debug('user triggered change !');
189 432 // set special marker to indicate user has created an input.
190 433 instance._userDefinedDesc = true;
191 434 }
192 435
193 436 });
194 437
195 438 return codeMirrorInstance
196 439 };
197 440
198 441 /**
199 442 * Reviewer autocomplete
200 443 */
201 444 var ReviewerAutoComplete = function(inputId) {
202 445 $(inputId).autocomplete({
203 446 serviceUrl: pyroutes.url('user_autocomplete_data'),
204 447 minChars:2,
205 448 maxHeight:400,
206 449 deferRequestBy: 300, //miliseconds
207 450 showNoSuggestionNotice: true,
208 451 tabDisabled: true,
209 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 454 formatResult: autocompleteFormatResult,
212 455 lookupFilter: autocompleteFilterResult,
213 456 onSelect: function(element, data) {
214 457
215 458 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
216 459 if (data.value_type == 'user_group') {
217 460 reasons.push(_gettext('member of "{0}"').format(data.value_display));
218 461
219 462 $.each(data.members, function(index, member_data) {
220 addReviewMember(member_data.id, member_data.first_name, member_data.last_name,
221 member_data.username, member_data.icon_link, reasons);
463 reviewersController.addReviewMember(
464 member_data.id, member_data.first_name, member_data.last_name,
465 member_data.username, member_data.icon_link, reasons);
222 466 })
223 467
224 468 } else {
225 addReviewMember(data.id, data.first_name, data.last_name,
226 data.username, data.icon_link, reasons);
469 reviewersController.addReviewMember(
470 data.id, data.first_name, data.last_name,
471 data.username, data.icon_link, reasons);
227 472 }
228 473
229 474 $(inputId).val('');
230 475 }
231 476 });
232 477 };
233 478
234 479
235 480 VersionController = function () {
236 481 var self = this;
237 482 this.$verSource = $('input[name=ver_source]');
238 483 this.$verTarget = $('input[name=ver_target]');
239 484 this.$showVersionDiff = $('#show-version-diff');
240 485
241 486 this.adjustRadioSelectors = function (curNode) {
242 487 var getVal = function (item) {
243 488 if (item == 'latest') {
244 489 return Number.MAX_SAFE_INTEGER
245 490 }
246 491 else {
247 492 return parseInt(item)
248 493 }
249 494 };
250 495
251 496 var curVal = getVal($(curNode).val());
252 497 var cleared = false;
253 498
254 499 $.each(self.$verSource, function (index, value) {
255 500 var elVal = getVal($(value).val());
256 501
257 502 if (elVal > curVal) {
258 503 if ($(value).is(':checked')) {
259 504 cleared = true;
260 505 }
261 506 $(value).attr('disabled', 'disabled');
262 507 $(value).removeAttr('checked');
263 508 $(value).css({'opacity': 0.1});
264 509 }
265 510 else {
266 511 $(value).css({'opacity': 1});
267 512 $(value).removeAttr('disabled');
268 513 }
269 514 });
270 515
271 516 if (cleared) {
272 517 // if we unchecked an active, set the next one to same loc.
273 518 $(this.$verSource).filter('[value={0}]'.format(
274 519 curVal)).attr('checked', 'checked');
275 520 }
276 521
277 522 self.setLockAction(false,
278 523 $(curNode).data('verPos'),
279 524 $(this.$verSource).filter(':checked').data('verPos')
280 525 );
281 526 };
282 527
283 528
284 529 this.attachVersionListener = function () {
285 530 self.$verTarget.change(function (e) {
286 531 self.adjustRadioSelectors(this)
287 532 });
288 533 self.$verSource.change(function (e) {
289 534 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
290 535 });
291 536 };
292 537
293 538 this.init = function () {
294 539
295 540 var curNode = self.$verTarget.filter(':checked');
296 541 self.adjustRadioSelectors(curNode);
297 542 self.setLockAction(true);
298 543 self.attachVersionListener();
299 544
300 545 };
301 546
302 547 this.setLockAction = function (state, selectedVersion, otherVersion) {
303 548 var $showVersionDiff = this.$showVersionDiff;
304 549
305 550 if (state) {
306 551 $showVersionDiff.attr('disabled', 'disabled');
307 552 $showVersionDiff.addClass('disabled');
308 553 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
309 554 }
310 555 else {
311 556 $showVersionDiff.removeAttr('disabled');
312 557 $showVersionDiff.removeClass('disabled');
313 558
314 559 if (selectedVersion == otherVersion) {
315 560 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
316 561 } else {
317 562 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
318 563 }
319 564 }
320 565
321 566 };
322 567
323 568 this.showVersionDiff = function () {
324 569 var target = self.$verTarget.filter(':checked');
325 570 var source = self.$verSource.filter(':checked');
326 571
327 572 if (target.val() && source.val()) {
328 573 var params = {
329 574 'pull_request_id': templateContext.pull_request_data.pull_request_id,
330 575 'repo_name': templateContext.repo_name,
331 576 'version': target.val(),
332 577 'from_version': source.val()
333 578 };
334 579 window.location = pyroutes.url('pullrequest_show', params)
335 580 }
336 581
337 582 return false;
338 583 };
339 584
340 585 this.toggleVersionView = function (elem) {
341 586
342 587 if (this.$showVersionDiff.is(':visible')) {
343 588 $('.version-pr').hide();
344 589 this.$showVersionDiff.hide();
345 590 $(elem).html($(elem).data('toggleOn'))
346 591 } else {
347 592 $('.version-pr').show();
348 593 this.$showVersionDiff.show();
349 594 $(elem).html($(elem).data('toggleOff'))
350 595 }
351 596
352 597 return false
353 598 }
354 599
355 600 }; No newline at end of file
@@ -1,95 +1,99 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##
3 3 ## See also repo_settings.html
4 4 ##
5 5 <%inherit file="/base/base.mako"/>
6 6
7 7 <%def name="title()">
8 8 ${_('%s repository settings') % c.repo_info.repo_name}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Settings')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='options')}
24 24 </%def>
25 25
26 26 <%def name="main_content()">
27 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
30 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
31 % endif
28 32 </%def>
29 33
30 34
31 35 <%def name="main()">
32 36 <div class="box">
33 37 <div class="title">
34 38 ${self.repo_page_title(c.rhodecode_db_repo)}
35 39 ${self.breadcrumbs()}
36 40 </div>
37 41
38 42 <div class="sidebar-col-wrapper scw-small">
39 43 <div class="sidebar">
40 44 <ul class="nav nav-pills nav-stacked">
41 45 <li class="${'active' if c.active=='settings' else ''}">
42 46 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
43 47 </li>
44 48 <li class="${'active' if c.active=='permissions' else ''}">
45 49 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
46 50 </li>
47 51 <li class="${'active' if c.active=='advanced' else ''}">
48 52 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
49 53 </li>
50 54 <li class="${'active' if c.active=='vcs' else ''}">
51 55 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
52 56 </li>
53 57 <li class="${'active' if c.active=='fields' else ''}">
54 58 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
55 59 </li>
56 60 <li class="${'active' if c.active=='issuetracker' else ''}">
57 61 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
58 62 </li>
59 63 <li class="${'active' if c.active=='caches' else ''}">
60 64 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
61 65 </li>
62 66 %if c.repo_info.repo_type != 'svn':
63 67 <li class="${'active' if c.active=='remote' else ''}">
64 68 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
65 69 </li>
66 70 %endif
67 71 <li class="${'active' if c.active=='statistics' else ''}">
68 72 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
69 73 </li>
70 74 <li class="${'active' if c.active=='integrations' else ''}">
71 75 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
72 76 </li>
73 77 %if c.repo_info.repo_type != 'svn':
74 78 <li class="${'active' if c.active=='reviewers' else ''}">
75 79 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
76 80 </li>
77 81 %endif
78 82 <li class="${'active' if c.active=='maintenance' else ''}">
79 83 <a href="${h.route_path('repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
80 84 </li>
81 85 <li class="${'active' if c.active=='strip' else ''}">
82 86 <a href="${h.route_path('strip', repo_name=c.repo_name)}">${_('Strip')}</a>
83 87 </li>
84 88
85 89 </ul>
86 90 </div>
87 91
88 92 <div class="main-content-full-width">
89 93 ${self.main_content()}
90 94 </div>
91 95
92 96 </div>
93 97 </div>
94 98
95 99 </%def> No newline at end of file
@@ -1,113 +1,114 b''
1 1 ## Changesets table !
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 %if c.ancestor:
5 5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
7 7 ${h.short_id(c.ancestor)}
8 8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
9 10 </div>
10 11 %endif
11 12
12 13 <div class="container">
13 14 <input type="hidden" name="__start__" value="revisions:sequence">
14 15 <table class="rctable compare_view_commits">
15 16 <tr>
16 17 <th>${_('Time')}</th>
17 18 <th>${_('Author')}</th>
18 19 <th>${_('Commit')}</th>
19 20 <th></th>
20 21 <th>${_('Description')}</th>
21 22 </tr>
22 23 %for commit in c.commit_ranges:
23 24 <tr id="row-${commit.raw_id}"
24 25 commit_id="${commit.raw_id}"
25 26 class="compare_select"
26 27 style="${'display: none' if c.collapse_all_commits else ''}"
27 28 >
28 29 <td class="td-time">
29 30 ${h.age_component(commit.date)}
30 31 </td>
31 32 <td class="td-user">
32 33 ${base.gravatar_with_user(commit.author, 16)}
33 34 </td>
34 35 <td class="td-hash">
35 36 <code>
36 37 <a href="${h.url('changeset_home',
37 38 repo_name=c.target_repo.repo_name,
38 39 revision=commit.raw_id)}">
39 40 r${commit.revision}:${h.short_id(commit.raw_id)}
40 41 </a>
41 42 ${h.hidden('revisions',commit.raw_id)}
42 43 </code>
43 44 </td>
44 45 <td class="expand_commit"
45 46 data-commit-id="${commit.raw_id}"
46 47 title="${_( 'Expand commit message')}"
47 48 >
48 49 <div class="show_more_col">
49 50 <i class="show_more"></i>
50 51 </div>
51 52 </td>
52 53 <td class="mid td-description">
53 54 <div class="log-container truncate-wrap">
54 55 <div
55 56 id="c-${commit.raw_id}"
56 57 class="message truncate"
57 58 data-message-raw="${commit.message}"
58 59 >
59 60 ${h.urlify_commit_message(commit.message, c.repo_name)}
60 61 </div>
61 62 </div>
62 63 </td>
63 64 </tr>
64 65 %endfor
65 66 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
66 67 <td colspan="5">
67 68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
68 69 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 70 </td>
70 71 </tr>
71 72 % if not c.commit_ranges:
72 73 <tr class="compare_select">
73 74 <td colspan="5">
74 75 ${_('No commits in this compare')}
75 76 </td>
76 77 </tr>
77 78 % endif
78 79 </table>
79 80 <input type="hidden" name="__end__" value="revisions:sequence">
80 81
81 82 </div>
82 83
83 84 <script>
84 85 $('.expand_commit').on('click',function(e){
85 86 var target_expand = $(this);
86 87 var cid = target_expand.data('commitId');
87 88
88 89 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 90 if (target_expand.hasClass('open')){
90 91 $('#c-'+cid).css({
91 92 'height': '1.5em',
92 93 'white-space': 'nowrap',
93 94 'text-overflow': 'ellipsis',
94 95 'overflow':'hidden'
95 96 });
96 97 target_expand.removeClass('open');
97 98 }
98 99 else {
99 100 $('#c-'+cid).css({
100 101 'height': 'auto',
101 102 'white-space': 'pre-line',
102 103 'text-overflow': 'initial',
103 104 'overflow':'visible'
104 105 });
105 106 target_expand.addClass('open');
106 107 }
107 108 });
108 109
109 110 $('.compare_select').on('click',function(e){
110 111 var cid = $(this).attr('commit_id');
111 112 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 113 });
113 114 </script>
@@ -1,105 +1,105 b''
1 1 <div tal:define="item_tmpl item_template|field.widget.item_template;
2 2 oid oid|field.oid;
3 3 name name|field.name;
4 4 min_len min_len|field.widget.min_len;
5 5 min_len min_len or 0;
6 6 max_len max_len|field.widget.max_len;
7 7 max_len max_len or 100000;
8 8 now_len len(subfields);
9 9 orderable orderable|field.widget.orderable;
10 10 orderable orderable and 1 or 0;
11 11 prototype field.widget.prototype(field);
12 12 title title|field.title;"
13 13 class="deform-seq"
14 14 id="${oid}">
15 15
16 16 <style>
17 17 body.dragging, body.dragging * {
18 18 cursor: move !important;
19 19 }
20 20
21 21 .dragged {
22 22 position: absolute;
23 23 opacity: 0.5;
24 24 z-index: 2000;
25 25 }
26 26 </style>
27 27
28 28 <!-- sequence -->
29 29 <input type="hidden" name="__start__"
30 30 value="${field.name}:sequence"
31 31 class="deform-proto"
32 32 tal:attributes="prototype prototype"/>
33 33
34 34 <div class="panel panel-default">
35 <div class="panel-heading">${title}</div>
36 35 <div class="panel-body">
37 36
38 37 <div class="deform-seq-container"
39 38 id="${oid}-orderable">
40 39 <div tal:define="subfields [ x[1] for x in subfields ]"
41 40 tal:repeat="subfield subfields"
42 tal:replace="structure subfield.render_template(item_tmpl,
43 parent=field)" />
41 tal:replace="structure subfield.render_template(item_tmpl,parent=field)" />
44 42 <span class="deform-insert-before"
45 43 tal:attributes="
46 44 min_len min_len;
47 45 max_len max_len;
48 46 now_len now_len;
49 orderable orderable;"></span>
47 orderable orderable;">
48
49 </span>
50 50 </div>
51 51
52 52 <div style="clear: both"></div>
53 53 </div>
54 54
55 55 <div class="panel-footer">
56 56 <a href="#"
57 57 class="btn deform-seq-add"
58 58 id="${field.oid}-seqAdd"
59 59 onclick="javascript: return deform.appendSequenceItem(this);">
60 60 <small id="${field.oid}-addtext">${add_subitem_text}</small>
61 61 </a>
62 62
63 63 <script type="text/javascript">
64 64 deform.addCallback(
65 65 '${field.oid}',
66 66 function(oid) {
67 67 oid_node = $('#'+ oid);
68 68 deform.processSequenceButtons(oid_node, ${min_len},
69 69 ${max_len}, ${now_len},
70 70 ${orderable});
71 71 }
72 72 )
73 73 <tal:block condition="orderable">
74 74 $( "#${oid}-orderable" ).sortable({
75 75 handle: ".deform-order-button, .panel-heading",
76 76 containerSelector: "#${oid}-orderable",
77 77 itemSelector: ".deform-seq-item",
78 78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
79 79 onDragStart: function ($item, container, _super) {
80 80 var offset = $item.offset(),
81 pointer = container.rootGroup.pointer
81 pointer = container.rootGroup.pointer;
82 82
83 83 adjustment = {
84 84 left: pointer.left - offset.left,
85 85 top: pointer.top - offset.top
86 }
86 };
87 87
88 88 _super($item, container)
89 89 },
90 90 onDrag: function ($item, position) {
91 91 $item.css({
92 92 left: position.left - adjustment.left,
93 93 top: position.top - adjustment.top
94 94 })
95 95 }
96 96 });
97 97 </tal:block>
98 98 </script>
99 99
100 100 <input type="hidden" name="__end__" value="${field.name}:sequence"/>
101 101 <!-- /sequence -->
102 102 </div>
103 103
104 104 </div>
105 105 </div> No newline at end of file
@@ -1,35 +1,31 b''
1 1 <div tal:omit-tag="field.widget.hidden"
2 2 tal:define="hidden hidden|field.widget.hidden;
3 3 error_class error_class|field.widget.error_class;
4 4 description description|field.description;
5 5 title title|field.title;
6 6 oid oid|field.oid"
7 7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
8 8 i18n:domain="deform">
9
9 10 <div class="deform-seq-item-group">
10 11 <span tal:replace="structure field.serialize(cstruct)"/>
11 12 <tal:errors condition="field.error and not hidden"
12 13 define="errstr 'error-%s' % oid"
13 14 repeat="msg field.error.messages()">
14 15 <p tal:condition="msg"
15 16 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 17 class="${error_class} help-block error-block"
17 18 i18n:translate="">${msg}</p>
18 19 </tal:errors>
19 20 </div>
21
20 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 23 <a class="deform-close-button close"
28 24 id="${oid}-close"
29 25 tal:condition="not field.widget.hidden"
30 26 title="Remove"
31 27 i18n:attributes="title"
32 28 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
33 29 </div>
34 <!-- /sequence_item -->
30
35 31 </div>
@@ -1,591 +1,505 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 27 <div class="box pr-summary">
28 28
29 29 <div class="summary-details block-left">
30 30
31 31 <div class="form">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 43 </div>
44 44
45 45 <div class="field">
46 46 <div class="label label-textarea">
47 47 <label for="pullrequest_desc">${_('Description')}:</label>
48 48 </div>
49 49 <div class="textarea text-area editor">
50 50 ${h.textarea('pullrequest_desc',size=30, )}
51 51 <span class="help-block">${_('Write a short description on this pull request')}</span>
52 52 </div>
53 53 </div>
54 54
55 55 <div class="field">
56 56 <div class="label label-textarea">
57 57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
58 58 </div>
59 59
60 60 ## TODO: johbo: Abusing the "content" class here to get the
61 61 ## desired effect. Should be replaced by a proper solution.
62 62
63 63 ##ORG
64 64 <div class="content">
65 <strong>${_('Origin repository')}:</strong>
65 <strong>${_('Source repository')}:</strong>
66 66 ${c.rhodecode_db_repo.description}
67 67 </div>
68 68 <div class="content">
69 69 ${h.hidden('source_repo')}
70 70 ${h.hidden('source_ref')}
71 71 </div>
72 72
73 73 ##OTHER, most Probably the PARENT OF THIS FORK
74 74 <div class="content">
75 75 ## filled with JS
76 76 <div id="target_repo_desc"></div>
77 77 </div>
78 78
79 79 <div class="content">
80 80 ${h.hidden('target_repo')}
81 81 ${h.hidden('target_ref')}
82 82 <span id="target_ref_loading" style="display: none">
83 83 ${_('Loading refs...')}
84 84 </span>
85 85 </div>
86 86 </div>
87 87
88 88 <div class="field">
89 89 <div class="label label-textarea">
90 90 <label for="pullrequest_submit"></label>
91 91 </div>
92 92 <div class="input">
93 93 <div class="pr-submit-button">
94 94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
95 95 </div>
96 96 <div id="pr_open_message"></div>
97 97 </div>
98 98 </div>
99 99
100 100 <div class="pr-spacing-container"></div>
101 101 </div>
102 102 </div>
103 103 </div>
104 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 116 <div class="reviewers-title block-right">
106 117 <div class="pr-details-title">
107 118 ${_('Pull request reviewers')}
108 119 <span class="calculate-reviewers"> - ${_('loading...')}</span>
109 120 </div>
110 121 </div>
111 122 <div id="reviewers" class="block-right pr-details-content reviewers">
112 123 ## members goes here, filled via JS based on initial selection !
113 124 <input type="hidden" name="__start__" value="review_members:sequence">
114 125 <ul id="review_members" class="group_members"></ul>
115 126 <input type="hidden" name="__end__" value="review_members:sequence">
116 127 <div id="add_reviewer_input" class='ac'>
117 128 <div class="reviewer_ac">
118 129 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
119 130 <div id="reviewers_container"></div>
120 131 </div>
121 132 </div>
122 133 </div>
123 134 </div>
124 135 </div>
125 136 <div class="box">
126 137 <div>
127 138 ## overview pulled by ajax
128 139 <div id="pull_request_overview"></div>
129 140 </div>
130 141 </div>
131 142 ${h.end_form()}
132 143 </div>
133 144
134 145 <script type="text/javascript">
135 146 $(function(){
136 147 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
137 148 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 149 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 150 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 var targetRepoName = '${c.repo_name}';
141 151
142 152 var $pullRequestForm = $('#pull_request_form');
143 153 var $sourceRepo = $('#source_repo', $pullRequestForm);
144 154 var $targetRepo = $('#target_repo', $pullRequestForm);
145 155 var $sourceRef = $('#source_ref', $pullRequestForm);
146 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 164 var calculateContainerWidth = function() {
149 165 var maxWidth = 0;
150 166 var repoSelect2Containers = ['#source_repo', '#target_repo'];
151 167 $.each(repoSelect2Containers, function(idx, value) {
152 168 $(value).select2('container').width('auto');
153 169 var curWidth = $(value).select2('container').width();
154 170 if (maxWidth <= curWidth) {
155 171 maxWidth = curWidth;
156 172 }
157 173 $.each(repoSelect2Containers, function(idx, value) {
158 174 $(value).select2('container').width(maxWidth + 10);
159 175 });
160 176 });
161 177 };
162 178
163 179 var initRefSelection = function(selectedRef) {
164 180 return function(element, callback) {
165 181 // translate our select2 id into a text, it's a mapping to show
166 182 // simple label when selecting by internal ID.
167 183 var id, refData;
168 184 if (selectedRef === undefined) {
169 185 id = element.val();
170 186 refData = element.val().split(':');
171 187 } else {
172 188 id = selectedRef;
173 189 refData = selectedRef.split(':');
174 190 }
175 191
176 192 var text = refData[1];
177 193 if (refData[0] === 'rev') {
178 194 text = text.substring(0, 12);
179 195 }
180 196
181 197 var data = {id: id, text: text};
182 198
183 199 callback(data);
184 200 };
185 201 };
186 202
187 203 var formatRefSelection = function(item) {
188 204 var prefix = '';
189 205 var refData = item.id.split(':');
190 206 if (refData[0] === 'branch') {
191 207 prefix = '<i class="icon-branch"></i>';
192 208 }
193 209 else if (refData[0] === 'book') {
194 210 prefix = '<i class="icon-bookmark"></i>';
195 211 }
196 212 else if (refData[0] === 'tag') {
197 213 prefix = '<i class="icon-tag"></i>';
198 214 }
199 215
200 216 var originalOption = item.element;
201 217 return prefix + item.text;
202 218 };
203 219
204 220 // custom code mirror
205 221 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206 222
223 reviewersController = new ReviewersController();
224
207 225 var queryTargetRepo = function(self, query) {
208 226 // cache ALL results if query is empty
209 227 var cacheKey = query.term || '__';
210 228 var cachedData = self.cachedDataSource[cacheKey];
211 229
212 230 if (cachedData) {
213 231 query.callback({results: cachedData.results});
214 232 } else {
215 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 235 data: {query: query.term},
218 236 dataType: 'json',
219 237 type: 'GET',
220 238 success: function(data) {
221 239 self.cachedDataSource[cacheKey] = data;
222 240 query.callback({results: data.results});
223 241 },
224 242 error: function(data, textStatus, errorThrown) {
225 243 alert(
226 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 250 var queryTargetRefs = function(initialData, query) {
233 251 var data = {results: []};
234 252 // filter initialData
235 253 $.each(initialData, function() {
236 254 var section = this.text;
237 255 var children = [];
238 256 $.each(this.children, function() {
239 257 if (query.term.length === 0 ||
240 258 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
241 259 children.push({'id': this.id, 'text': this.text})
242 260 }
243 261 });
244 262 data.results.push({'text': section, 'children': children})
245 263 });
246 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 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 269 var url_data = {
286 'repo_name': targetRepo,
287 'target_repo': sourceRepo,
288 'source_ref': targetRef[2],
270 'repo_name': targetRepo(),
271 'target_repo': sourceRepo(),
272 'source_ref': targetRef()[2],
289 273 'source_ref_type': 'rev',
290 'target_ref': sourceRef[2],
274 'target_ref': sourceRef()[2],
291 275 'target_ref_type': 'rev',
292 276 'merge': true,
293 277 '_': Date.now() // bypass browser caching
294 278 }; // gather the source/target ref and repo here
295 279
296 if (sourceRef.length !== 3 || targetRef.length !== 3) {
297 prButtonLock(true, "${_('Please select origin and destination')}");
280 if (sourceRef().length !== 3 || targetRef().length !== 3) {
281 prButtonLock(true, "${_('Please select source and target')}");
298 282 return;
299 283 }
300 284 var url = pyroutes.url('compare_url', url_data);
301 285
302 286 // lock PR button, so we cannot send PR before it's calculated
303 287 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
304 288
305 289 if (loadRepoRefDiffPreview._currentRequest) {
306 290 loadRepoRefDiffPreview._currentRequest.abort();
307 291 }
308 292
309 293 loadRepoRefDiffPreview._currentRequest = $.get(url)
310 294 .error(function(data, textStatus, errorThrown) {
311 295 alert(
312 296 "Error while processing request.\nError code {0} ({1}).".format(
313 297 data.status, data.statusText));
314 298 })
315 299 .done(function(data) {
316 300 loadRepoRefDiffPreview._currentRequest = null;
317 301 $('#pull_request_overview').html(data);
302
318 303 var commitElements = $(data).find('tr[commit_id]');
319 304
320 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
321 commitElements, 5);
305 var prTitleAndDesc = getTitleAndDescription(
306 sourceRef()[1], commitElements, 5);
322 307
323 308 var title = prTitleAndDesc[0];
324 309 var proposedDescription = prTitleAndDesc[1];
325 310
326 311 var useGeneratedTitle = (
327 312 $('#pullrequest_title').hasClass('autogenerated-title') ||
328 313 $('#pullrequest_title').val() === "");
329 314
330 315 if (title && useGeneratedTitle) {
331 316 // use generated title if we haven't specified our own
332 317 $('#pullrequest_title').val(title);
333 318 $('#pullrequest_title').addClass('autogenerated-title');
334 319
335 320 }
336 321
337 322 var useGeneratedDescription = (
338 323 !codeMirrorInstance._userDefinedDesc ||
339 324 codeMirrorInstance.getValue() === "");
340 325
341 326 if (proposedDescription && useGeneratedDescription) {
342 327 // set proposed content, if we haven't defined our own,
343 328 // or we don't have description written
344 329 codeMirrorInstance._userDefinedDesc = false; // reset state
345 330 codeMirrorInstance.setValue(proposedDescription);
346 331 }
347 332
348 333 var msg = '';
349 334 if (commitElements.length === 1) {
350 335 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
351 336 } else {
352 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 340 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
356 341
357 342 if (commitElements.length) {
358 343 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
359 344 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
360 345 }
361 346 else {
362 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 354 var Select2Box = function(element, overrides) {
407 355 var globalDefaults = {
408 356 dropdownAutoWidth: true,
409 357 containerCssClass: "drop-menu",
410 358 dropdownCssClass: "drop-menu-dropdown"
411 359 };
412 360
413 361 var initSelect2 = function(defaultOptions) {
414 362 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
415 363 element.select2(options);
416 364 };
417 365
418 366 return {
419 367 initRef: function() {
420 368 var defaultOptions = {
421 369 minimumResultsForSearch: 5,
422 370 formatSelection: formatRefSelection
423 371 };
424 372
425 373 initSelect2(defaultOptions);
426 374 },
427 375
428 376 initRepo: function(defaultValue, readOnly) {
429 377 var defaultOptions = {
430 378 initSelection : function (element, callback) {
431 379 var data = {id: defaultValue, text: defaultValue};
432 380 callback(data);
433 381 }
434 382 };
435 383
436 384 initSelect2(defaultOptions);
437 385
438 386 element.select2('val', defaultSourceRepo);
439 387 if (readOnly === true) {
440 388 element.select2('readonly', true);
441 389 }
442 390 }
443 391 };
444 392 };
445 393
446 394 var initTargetRefs = function(refsData, selectedRef){
447 395 Select2Box($targetRef, {
448 396 query: function(query) {
449 397 queryTargetRefs(refsData, query);
450 398 },
451 399 initSelection : initRefSelection(selectedRef)
452 400 }).initRef();
453 401
454 402 if (!(selectedRef === undefined)) {
455 403 $targetRef.select2('val', selectedRef);
456 404 }
457 405 };
458 406
459 407 var targetRepoChanged = function(repoData) {
460 408 // generate new DESC of target repo displayed next to select
461 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 413 // generate dynamic select2 for refs.
466 414 initTargetRefs(repoData['refs']['select2_refs'],
467 415 repoData['refs']['selected_ref']);
468 416
469 417 };
470 418
471 var sourceRefSelect2 = Select2Box(
472 $sourceRef, {
419 var sourceRefSelect2 = Select2Box($sourceRef, {
473 420 placeholder: "${_('Select commit reference')}",
474 421 query: function(query) {
475 422 var initialData = defaultSourceRepoData['refs']['select2_refs'];
476 423 queryTargetRefs(initialData, query)
477 424 },
478 425 initSelection: initRefSelection()
479 426 }
480 427 );
481 428
482 429 var sourceRepoSelect2 = Select2Box($sourceRepo, {
483 430 query: function(query) {}
484 431 });
485 432
486 433 var targetRepoSelect2 = Select2Box($targetRepo, {
487 434 cachedDataSource: {},
488 435 query: $.debounce(250, function(query) {
489 436 queryTargetRepo(this, query);
490 437 }),
491 438 formatResult: formatResult
492 439 });
493 440
494 441 sourceRefSelect2.initRef();
495 442
496 443 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
497 444
498 445 targetRepoSelect2.initRepo(defaultTargetRepo, false);
499 446
500 447 $sourceRef.on('change', function(e){
501 448 loadRepoRefDiffPreview();
502 loadDefaultReviewers();
449 reviewersController.loadDefaultReviewers(
450 sourceRepo(), sourceRef(), targetRepo(), targetRef());
503 451 });
504 452
505 453 $targetRef.on('change', function(e){
506 454 loadRepoRefDiffPreview();
507 loadDefaultReviewers();
455 reviewersController.loadDefaultReviewers(
456 sourceRepo(), sourceRef(), targetRepo(), targetRef());
508 457 });
509 458
510 459 $targetRepo.on('change', function(e){
511 460 var repoName = $(this).val();
512 461 calculateContainerWidth();
513 462 $targetRef.select2('destroy');
514 463 $('#target_ref_loading').show();
515 464
516 465 $.ajax({
517 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 468 data: {},
520 469 dataType: 'json',
521 470 type: 'GET',
522 471 success: function(data) {
523 472 $('#target_ref_loading').hide();
524 473 targetRepoChanged(data);
525 474 loadRepoRefDiffPreview();
526 475 },
527 476 error: function(data, textStatus, errorThrown) {
528 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() {
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');
483 prButtonLock(true, "${_('Please select source and target')}", 'all');
571 484
572 485 // auto-load on init, the target refs select2
573 486 calculateContainerWidth();
574 487 targetRepoChanged(defaultTargetRepoData);
575 488
576 489 $('#pullrequest_title').on('keyup', function(e){
577 490 $(this).removeClass('autogenerated-title');
578 491 });
579 492
580 493 % if c.default_source_ref:
581 // in case we have a pre-selected value, use it now
582 $sourceRef.select2('val', '${c.default_source_ref}');
583 loadRepoRefDiffPreview();
584 loadDefaultReviewers();
494 // in case we have a pre-selected value, use it now
495 $sourceRef.select2('val', '${c.default_source_ref}');
496 loadRepoRefDiffPreview();
497 reviewersController.loadDefaultReviewers(
498 sourceRepo(), sourceRef(), targetRepo(), targetRef());
585 499 % endif
586 500
587 501 ReviewerAutoComplete('#user');
588 502 });
589 503 </script>
590 504
591 505 </%def>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now