##// END OF EJS Templates
api: creation of pull_request now honor additional reviewers when using reviewer rules.
marcink -
r2881:00ce3d87 default
parent child Browse files
Show More
@@ -1,311 +1,337 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User
23 from rhodecode.model.db import User
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCreatePullRequestApi(object):
32 class TestCreatePullRequestApi(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 def test_create_with_wrong_data(self):
41 def test_create_with_wrong_data(self):
42 required_data = {
42 required_data = {
43 'source_repo': 'tests/source_repo',
43 'source_repo': 'tests/source_repo',
44 'target_repo': 'tests/target_repo',
44 'target_repo': 'tests/target_repo',
45 'source_ref': 'branch:default:initial',
45 'source_ref': 'branch:default:initial',
46 'target_ref': 'branch:default:new-feature',
46 'target_ref': 'branch:default:new-feature',
47 }
47 }
48 for key in required_data:
48 for key in required_data:
49 data = required_data.copy()
49 data = required_data.copy()
50 data.pop(key)
50 data.pop(key)
51 id_, params = build_data(
51 id_, params = build_data(
52 self.apikey, 'create_pull_request', **data)
52 self.apikey, 'create_pull_request', **data)
53 response = api_call(self.app, params)
53 response = api_call(self.app, params)
54
54
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 assert_error(id_, expected, given=response.body)
56 assert_error(id_, expected, given=response.body)
57
57
58 @pytest.mark.backends("git", "hg")
58 @pytest.mark.backends("git", "hg")
59 def test_create_with_correct_data(self, backend):
59 def test_create_with_correct_data(self, backend):
60 data = self._prepare_data(backend)
60 data = self._prepare_data(backend)
61 RepoModel().revoke_user_permission(
61 RepoModel().revoke_user_permission(
62 self.source.repo_name, User.DEFAULT_USER)
62 self.source.repo_name, User.DEFAULT_USER)
63 id_, params = build_data(
63 id_, params = build_data(
64 self.apikey_regular, 'create_pull_request', **data)
64 self.apikey_regular, 'create_pull_request', **data)
65 response = api_call(self.app, params)
65 response = api_call(self.app, params)
66 expected_message = "Created new pull request `{title}`".format(
66 expected_message = "Created new pull request `{title}`".format(
67 title=data['title'])
67 title=data['title'])
68 result = response.json
68 result = response.json
69 assert result['result']['msg'] == expected_message
69 assert result['result']['msg'] == expected_message
70 pull_request_id = result['result']['pull_request_id']
70 pull_request_id = result['result']['pull_request_id']
71 pull_request = PullRequestModel().get(pull_request_id)
71 pull_request = PullRequestModel().get(pull_request_id)
72 assert pull_request.title == data['title']
72 assert pull_request.title == data['title']
73 assert pull_request.description == data['description']
73 assert pull_request.description == data['description']
74 assert pull_request.source_ref == data['source_ref']
74 assert pull_request.source_ref == data['source_ref']
75 assert pull_request.target_ref == data['target_ref']
75 assert pull_request.target_ref == data['target_ref']
76 assert pull_request.source_repo.repo_name == data['source_repo']
76 assert pull_request.source_repo.repo_name == data['source_repo']
77 assert pull_request.target_repo.repo_name == data['target_repo']
77 assert pull_request.target_repo.repo_name == data['target_repo']
78 assert pull_request.revisions == [self.commit_ids['change']]
78 assert pull_request.revisions == [self.commit_ids['change']]
79 assert len(pull_request.reviewers) == 1
79 assert len(pull_request.reviewers) == 1
80
80
81 @pytest.mark.backends("git", "hg")
81 @pytest.mark.backends("git", "hg")
82 def test_create_with_empty_description(self, backend):
82 def test_create_with_empty_description(self, backend):
83 data = self._prepare_data(backend)
83 data = self._prepare_data(backend)
84 data.pop('description')
84 data.pop('description')
85 id_, params = build_data(
85 id_, params = build_data(
86 self.apikey_regular, 'create_pull_request', **data)
86 self.apikey_regular, 'create_pull_request', **data)
87 response = api_call(self.app, params)
87 response = api_call(self.app, params)
88 expected_message = "Created new pull request `{title}`".format(
88 expected_message = "Created new pull request `{title}`".format(
89 title=data['title'])
89 title=data['title'])
90 result = response.json
90 result = response.json
91 assert result['result']['msg'] == expected_message
91 assert result['result']['msg'] == expected_message
92 pull_request_id = result['result']['pull_request_id']
92 pull_request_id = result['result']['pull_request_id']
93 pull_request = PullRequestModel().get(pull_request_id)
93 pull_request = PullRequestModel().get(pull_request_id)
94 assert pull_request.description == ''
94 assert pull_request.description == ''
95
95
96 @pytest.mark.backends("git", "hg")
96 @pytest.mark.backends("git", "hg")
97 def test_create_with_empty_title(self, backend):
97 def test_create_with_empty_title(self, backend):
98 data = self._prepare_data(backend)
98 data = self._prepare_data(backend)
99 data.pop('title')
99 data.pop('title')
100 id_, params = build_data(
100 id_, params = build_data(
101 self.apikey_regular, 'create_pull_request', **data)
101 self.apikey_regular, 'create_pull_request', **data)
102 response = api_call(self.app, params)
102 response = api_call(self.app, params)
103 result = response.json
103 result = response.json
104 pull_request_id = result['result']['pull_request_id']
104 pull_request_id = result['result']['pull_request_id']
105 pull_request = PullRequestModel().get(pull_request_id)
105 pull_request = PullRequestModel().get(pull_request_id)
106 data['ref'] = backend.default_branch_name
106 data['ref'] = backend.default_branch_name
107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
108 assert pull_request.title == title
108 assert pull_request.title == title
109
109
110 @pytest.mark.backends("git", "hg")
110 @pytest.mark.backends("git", "hg")
111 def test_create_with_reviewers_specified_by_names(
111 def test_create_with_reviewers_specified_by_names(
112 self, backend, no_notifications):
112 self, backend, no_notifications):
113 data = self._prepare_data(backend)
113 data = self._prepare_data(backend)
114 reviewers = [
114 reviewers = [
115 {'username': TEST_USER_REGULAR_LOGIN,
115 {'username': TEST_USER_REGULAR_LOGIN,
116 'reasons': ['added manually']},
116 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
117 {'username': TEST_USER_ADMIN_LOGIN,
117 {'username': TEST_USER_ADMIN_LOGIN,
118 'reasons': ['added manually']},
118 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
119 'mandatory': True},
119 ]
120 ]
120 data['reviewers'] = reviewers
121 data['reviewers'] = reviewers
122
121 id_, params = build_data(
123 id_, params = build_data(
122 self.apikey_regular, 'create_pull_request', **data)
124 self.apikey_regular, 'create_pull_request', **data)
123 response = api_call(self.app, params)
125 response = api_call(self.app, params)
124
126
125 expected_message = "Created new pull request `{title}`".format(
127 expected_message = "Created new pull request `{title}`".format(
126 title=data['title'])
128 title=data['title'])
127 result = response.json
129 result = response.json
128 assert result['result']['msg'] == expected_message
130 assert result['result']['msg'] == expected_message
129 pull_request_id = result['result']['pull_request_id']
131 pull_request_id = result['result']['pull_request_id']
130 pull_request = PullRequestModel().get(pull_request_id)
132 pull_request = PullRequestModel().get(pull_request_id)
131 actual_reviewers = [
133
132 {'username': r.user.username,
134 actual_reviewers = []
133 'reasons': ['added manually'],
135 for rev in pull_request.reviewers:
134 } for r in pull_request.reviewers
136 entry = {
135 ]
137 'username': rev.user.username,
136 assert sorted(actual_reviewers) == sorted(reviewers)
138 'reasons': rev.reasons,
139 }
140 if rev.mandatory:
141 entry['mandatory'] = rev.mandatory
142 actual_reviewers.append(entry)
143
144 # default reviewer will be added who is an owner of the repo
145 reviewers.append(
146 {'username': pull_request.author.username,
147 'reasons': [u'Default reviewer', u'Repository owner']},
148 )
149 assert sorted(actual_reviewers, key=lambda e: e['username']) \
150 == sorted(reviewers, key=lambda e: e['username'])
137
151
138 @pytest.mark.backends("git", "hg")
152 @pytest.mark.backends("git", "hg")
139 def test_create_with_reviewers_specified_by_ids(
153 def test_create_with_reviewers_specified_by_ids(
140 self, backend, no_notifications):
154 self, backend, no_notifications):
141 data = self._prepare_data(backend)
155 data = self._prepare_data(backend)
142 reviewers = [
156 reviewers = [
143 {'username': UserModel().get_by_username(
157 {'username': UserModel().get_by_username(
144 TEST_USER_REGULAR_LOGIN).user_id,
158 TEST_USER_REGULAR_LOGIN).user_id,
145 'reasons': ['added manually']},
159 'reasons': ['added manually']},
146 {'username': UserModel().get_by_username(
160 {'username': UserModel().get_by_username(
147 TEST_USER_ADMIN_LOGIN).user_id,
161 TEST_USER_ADMIN_LOGIN).user_id,
148 'reasons': ['added manually']},
162 'reasons': ['added manually']},
149 ]
163 ]
150
164
151 data['reviewers'] = reviewers
165 data['reviewers'] = reviewers
152 id_, params = build_data(
166 id_, params = build_data(
153 self.apikey_regular, 'create_pull_request', **data)
167 self.apikey_regular, 'create_pull_request', **data)
154 response = api_call(self.app, params)
168 response = api_call(self.app, params)
155
169
156 expected_message = "Created new pull request `{title}`".format(
170 expected_message = "Created new pull request `{title}`".format(
157 title=data['title'])
171 title=data['title'])
158 result = response.json
172 result = response.json
159 assert result['result']['msg'] == expected_message
173 assert result['result']['msg'] == expected_message
160 pull_request_id = result['result']['pull_request_id']
174 pull_request_id = result['result']['pull_request_id']
161 pull_request = PullRequestModel().get(pull_request_id)
175 pull_request = PullRequestModel().get(pull_request_id)
162 actual_reviewers = [
176
163 {'username': r.user.user_id,
177 actual_reviewers = []
164 'reasons': ['added manually'],
178 for rev in pull_request.reviewers:
165 } for r in pull_request.reviewers
179 entry = {
166 ]
180 'username': rev.user.user_id,
167 assert sorted(actual_reviewers) == sorted(reviewers)
181 'reasons': rev.reasons,
182 }
183 if rev.mandatory:
184 entry['mandatory'] = rev.mandatory
185 actual_reviewers.append(entry)
186 # default reviewer will be added who is an owner of the repo
187 reviewers.append(
188 {'username': pull_request.author.user_id,
189 'reasons': [u'Default reviewer', u'Repository owner']},
190 )
191 assert sorted(actual_reviewers, key=lambda e: e['username']) \
192 == sorted(reviewers, key=lambda e: e['username'])
168
193
169 @pytest.mark.backends("git", "hg")
194 @pytest.mark.backends("git", "hg")
170 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
195 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
171 data = self._prepare_data(backend)
196 data = self._prepare_data(backend)
172 data['reviewers'] = [{'username': 'somebody'}]
197 data['reviewers'] = [{'username': 'somebody'}]
173 id_, params = build_data(
198 id_, params = build_data(
174 self.apikey_regular, 'create_pull_request', **data)
199 self.apikey_regular, 'create_pull_request', **data)
175 response = api_call(self.app, params)
200 response = api_call(self.app, params)
176 expected_message = 'user `somebody` does not exist'
201 expected_message = 'user `somebody` does not exist'
177 assert_error(id_, expected_message, given=response.body)
202 assert_error(id_, expected_message, given=response.body)
178
203
179 @pytest.mark.backends("git", "hg")
204 @pytest.mark.backends("git", "hg")
180 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
205 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
181 data = self._prepare_data(backend)
206 data = self._prepare_data(backend)
182 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
207 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
183 data['reviewers'] = reviewers
208 data['reviewers'] = reviewers
184 id_, params = build_data(
209 id_, params = build_data(
185 self.apikey_regular, 'create_pull_request', **data)
210 self.apikey_regular, 'create_pull_request', **data)
186 response = api_call(self.app, params)
211 response = api_call(self.app, params)
187 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
212 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
188 assert_error(id_, expected_message, given=response.body)
213 assert_error(id_, expected_message, given=response.body)
189
214
190 @pytest.mark.backends("git", "hg")
215 @pytest.mark.backends("git", "hg")
191 def test_create_with_no_commit_hashes(self, backend):
216 def test_create_with_no_commit_hashes(self, backend):
192 data = self._prepare_data(backend)
217 data = self._prepare_data(backend)
193 expected_source_ref = data['source_ref']
218 expected_source_ref = data['source_ref']
194 expected_target_ref = data['target_ref']
219 expected_target_ref = data['target_ref']
195 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
220 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
196 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
221 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
197 id_, params = build_data(
222 id_, params = build_data(
198 self.apikey_regular, 'create_pull_request', **data)
223 self.apikey_regular, 'create_pull_request', **data)
199 response = api_call(self.app, params)
224 response = api_call(self.app, params)
200 expected_message = "Created new pull request `{title}`".format(
225 expected_message = "Created new pull request `{title}`".format(
201 title=data['title'])
226 title=data['title'])
202 result = response.json
227 result = response.json
203 assert result['result']['msg'] == expected_message
228 assert result['result']['msg'] == expected_message
204 pull_request_id = result['result']['pull_request_id']
229 pull_request_id = result['result']['pull_request_id']
205 pull_request = PullRequestModel().get(pull_request_id)
230 pull_request = PullRequestModel().get(pull_request_id)
206 assert pull_request.source_ref == expected_source_ref
231 assert pull_request.source_ref == expected_source_ref
207 assert pull_request.target_ref == expected_target_ref
232 assert pull_request.target_ref == expected_target_ref
208
233
209 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
210 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
235 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
211 def test_create_fails_with_wrong_repo(self, backend, data_key):
236 def test_create_fails_with_wrong_repo(self, backend, data_key):
212 repo_name = 'fake-repo'
237 repo_name = 'fake-repo'
213 data = self._prepare_data(backend)
238 data = self._prepare_data(backend)
214 data[data_key] = repo_name
239 data[data_key] = repo_name
215 id_, params = build_data(
240 id_, params = build_data(
216 self.apikey_regular, 'create_pull_request', **data)
241 self.apikey_regular, 'create_pull_request', **data)
217 response = api_call(self.app, params)
242 response = api_call(self.app, params)
218 expected_message = 'repository `{}` does not exist'.format(repo_name)
243 expected_message = 'repository `{}` does not exist'.format(repo_name)
219 assert_error(id_, expected_message, given=response.body)
244 assert_error(id_, expected_message, given=response.body)
220
245
221 @pytest.mark.backends("git", "hg")
246 @pytest.mark.backends("git", "hg")
222 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
247 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
223 def test_create_fails_with_non_existing_branch(self, backend, data_key):
248 def test_create_fails_with_non_existing_branch(self, backend, data_key):
224 branch_name = 'test-branch'
249 branch_name = 'test-branch'
225 data = self._prepare_data(backend)
250 data = self._prepare_data(backend)
226 data[data_key] = "branch:{}".format(branch_name)
251 data[data_key] = "branch:{}".format(branch_name)
227 id_, params = build_data(
252 id_, params = build_data(
228 self.apikey_regular, 'create_pull_request', **data)
253 self.apikey_regular, 'create_pull_request', **data)
229 response = api_call(self.app, params)
254 response = api_call(self.app, params)
230 expected_message = 'The specified branch `{}` does not exist'.format(
255 expected_message = 'The specified branch `{}` does not exist'.format(
231 branch_name)
256 branch_name)
232 assert_error(id_, expected_message, given=response.body)
257 assert_error(id_, expected_message, given=response.body)
233
258
234 @pytest.mark.backends("git", "hg")
259 @pytest.mark.backends("git", "hg")
235 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
260 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
236 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
261 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
237 data = self._prepare_data(backend)
262 data = self._prepare_data(backend)
238 ref = 'stange-ref'
263 ref = 'stange-ref'
239 data[data_key] = ref
264 data[data_key] = ref
240 id_, params = build_data(
265 id_, params = build_data(
241 self.apikey_regular, 'create_pull_request', **data)
266 self.apikey_regular, 'create_pull_request', **data)
242 response = api_call(self.app, params)
267 response = api_call(self.app, params)
243 expected_message = (
268 expected_message = (
244 'Ref `{ref}` given in a wrong format. Please check the API'
269 'Ref `{ref}` given in a wrong format. Please check the API'
245 ' documentation for more details'.format(ref=ref))
270 ' documentation for more details'.format(ref=ref))
246 assert_error(id_, expected_message, given=response.body)
271 assert_error(id_, expected_message, given=response.body)
247
272
248 @pytest.mark.backends("git", "hg")
273 @pytest.mark.backends("git", "hg")
249 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
274 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
250 def test_create_fails_with_non_existing_ref(self, backend, data_key):
275 def test_create_fails_with_non_existing_ref(self, backend, data_key):
251 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
276 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
252 ref = self._get_full_ref(backend, commit_id)
277 ref = self._get_full_ref(backend, commit_id)
253 data = self._prepare_data(backend)
278 data = self._prepare_data(backend)
254 data[data_key] = ref
279 data[data_key] = ref
255 id_, params = build_data(
280 id_, params = build_data(
256 self.apikey_regular, 'create_pull_request', **data)
281 self.apikey_regular, 'create_pull_request', **data)
257 response = api_call(self.app, params)
282 response = api_call(self.app, params)
258 expected_message = 'Ref `{}` does not exist'.format(ref)
283 expected_message = 'Ref `{}` does not exist'.format(ref)
259 assert_error(id_, expected_message, given=response.body)
284 assert_error(id_, expected_message, given=response.body)
260
285
261 @pytest.mark.backends("git", "hg")
286 @pytest.mark.backends("git", "hg")
262 def test_create_fails_when_no_revisions(self, backend):
287 def test_create_fails_when_no_revisions(self, backend):
263 data = self._prepare_data(backend, source_head='initial')
288 data = self._prepare_data(backend, source_head='initial')
264 id_, params = build_data(
289 id_, params = build_data(
265 self.apikey_regular, 'create_pull_request', **data)
290 self.apikey_regular, 'create_pull_request', **data)
266 response = api_call(self.app, params)
291 response = api_call(self.app, params)
267 expected_message = 'no commits found'
292 expected_message = 'no commits found'
268 assert_error(id_, expected_message, given=response.body)
293 assert_error(id_, expected_message, given=response.body)
269
294
270 @pytest.mark.backends("git", "hg")
295 @pytest.mark.backends("git", "hg")
271 def test_create_fails_when_no_permissions(self, backend):
296 def test_create_fails_when_no_permissions(self, backend):
272 data = self._prepare_data(backend)
297 data = self._prepare_data(backend)
273 RepoModel().revoke_user_permission(
298 RepoModel().revoke_user_permission(
274 self.source.repo_name, User.DEFAULT_USER)
299 self.source.repo_name, self.test_user)
275 RepoModel().revoke_user_permission(
300 RepoModel().revoke_user_permission(
276 self.source.repo_name, self.test_user)
301 self.source.repo_name, User.DEFAULT_USER)
302
277 id_, params = build_data(
303 id_, params = build_data(
278 self.apikey_regular, 'create_pull_request', **data)
304 self.apikey_regular, 'create_pull_request', **data)
279 response = api_call(self.app, params)
305 response = api_call(self.app, params)
280 expected_message = 'repository `{}` does not exist'.format(
306 expected_message = 'repository `{}` does not exist'.format(
281 self.source.repo_name)
307 self.source.repo_name)
282 assert_error(id_, expected_message, given=response.body)
308 assert_error(id_, expected_message, given=response.body)
283
309
284 def _prepare_data(
310 def _prepare_data(
285 self, backend, source_head='change', target_head='initial'):
311 self, backend, source_head='change', target_head='initial'):
286 commits = [
312 commits = [
287 {'message': 'initial'},
313 {'message': 'initial'},
288 {'message': 'change'},
314 {'message': 'change'},
289 {'message': 'new-feature', 'parents': ['initial']},
315 {'message': 'new-feature', 'parents': ['initial']},
290 ]
316 ]
291 self.commit_ids = backend.create_master_repo(commits)
317 self.commit_ids = backend.create_master_repo(commits)
292 self.source = backend.create_repo(heads=[source_head])
318 self.source = backend.create_repo(heads=[source_head])
293 self.target = backend.create_repo(heads=[target_head])
319 self.target = backend.create_repo(heads=[target_head])
294
320
295 data = {
321 data = {
296 'source_repo': self.source.repo_name,
322 'source_repo': self.source.repo_name,
297 'target_repo': self.target.repo_name,
323 'target_repo': self.target.repo_name,
298 'source_ref': self._get_full_ref(
324 'source_ref': self._get_full_ref(
299 backend, self.commit_ids[source_head]),
325 backend, self.commit_ids[source_head]),
300 'target_ref': self._get_full_ref(
326 'target_ref': self._get_full_ref(
301 backend, self.commit_ids[target_head]),
327 backend, self.commit_ids[target_head]),
302 'title': 'Test PR 1',
328 'title': 'Test PR 1',
303 'description': 'Test'
329 'description': 'Test'
304 }
330 }
305 RepoModel().grant_user_permission(
331 RepoModel().grant_user_permission(
306 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
332 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
307 return data
333 return data
308
334
309 def _get_full_ref(self, backend, commit_id):
335 def _get_full_ref(self, backend, commit_id):
310 return 'branch:{branch}:{commit_id}'.format(
336 return 'branch:{branch}:{commit_id}'.format(
311 branch=backend.default_branch_name, commit_id=commit_id)
337 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,919 +1,919 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(
131 if not PullRequestModel().check_user_read(
132 pull_request, apiuser, api=True):
132 pull_request, apiuser, api=True):
133 raise JSONRPCError('repository `%s` or pull request `%s` '
133 raise JSONRPCError('repository `%s` or pull request `%s` '
134 'does not exist' % (repoid, pullrequestid))
134 'does not exist' % (repoid, pullrequestid))
135 data = pull_request.get_api_data()
135 data = pull_request.get_api_data()
136 return data
136 return data
137
137
138
138
139 @jsonrpc_method()
139 @jsonrpc_method()
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
141 """
141 """
142 Get all pull requests from the repository specified in `repoid`.
142 Get all pull requests from the repository specified in `repoid`.
143
143
144 :param apiuser: This is filled automatically from the |authtoken|.
144 :param apiuser: This is filled automatically from the |authtoken|.
145 :type apiuser: AuthUser
145 :type apiuser: AuthUser
146 :param repoid: Optional repository name or repository ID.
146 :param repoid: Optional repository name or repository ID.
147 :type repoid: str or int
147 :type repoid: str or int
148 :param status: Only return pull requests with the specified status.
148 :param status: Only return pull requests with the specified status.
149 Valid options are.
149 Valid options are.
150 * ``new`` (default)
150 * ``new`` (default)
151 * ``open``
151 * ``open``
152 * ``closed``
152 * ``closed``
153 :type status: str
153 :type status: str
154
154
155 Example output:
155 Example output:
156
156
157 .. code-block:: bash
157 .. code-block:: bash
158
158
159 "id": <id_given_in_input>,
159 "id": <id_given_in_input>,
160 "result":
160 "result":
161 [
161 [
162 ...
162 ...
163 {
163 {
164 "pull_request_id": "<pull_request_id>",
164 "pull_request_id": "<pull_request_id>",
165 "url": "<url>",
165 "url": "<url>",
166 "title" : "<title>",
166 "title" : "<title>",
167 "description": "<description>",
167 "description": "<description>",
168 "status": "<status>",
168 "status": "<status>",
169 "created_on": "<date_time_created>",
169 "created_on": "<date_time_created>",
170 "updated_on": "<date_time_updated>",
170 "updated_on": "<date_time_updated>",
171 "commit_ids": [
171 "commit_ids": [
172 ...
172 ...
173 "<commit_id>",
173 "<commit_id>",
174 "<commit_id>",
174 "<commit_id>",
175 ...
175 ...
176 ],
176 ],
177 "review_status": "<review_status>",
177 "review_status": "<review_status>",
178 "mergeable": {
178 "mergeable": {
179 "status": "<bool>",
179 "status": "<bool>",
180 "message: "<message>",
180 "message: "<message>",
181 },
181 },
182 "source": {
182 "source": {
183 "clone_url": "<clone_url>",
183 "clone_url": "<clone_url>",
184 "reference":
184 "reference":
185 {
185 {
186 "name": "<name>",
186 "name": "<name>",
187 "type": "<type>",
187 "type": "<type>",
188 "commit_id": "<commit_id>",
188 "commit_id": "<commit_id>",
189 }
189 }
190 },
190 },
191 "target": {
191 "target": {
192 "clone_url": "<clone_url>",
192 "clone_url": "<clone_url>",
193 "reference":
193 "reference":
194 {
194 {
195 "name": "<name>",
195 "name": "<name>",
196 "type": "<type>",
196 "type": "<type>",
197 "commit_id": "<commit_id>",
197 "commit_id": "<commit_id>",
198 }
198 }
199 },
199 },
200 "merge": {
200 "merge": {
201 "clone_url": "<clone_url>",
201 "clone_url": "<clone_url>",
202 "reference":
202 "reference":
203 {
203 {
204 "name": "<name>",
204 "name": "<name>",
205 "type": "<type>",
205 "type": "<type>",
206 "commit_id": "<commit_id>",
206 "commit_id": "<commit_id>",
207 }
207 }
208 },
208 },
209 "author": <user_obj>,
209 "author": <user_obj>,
210 "reviewers": [
210 "reviewers": [
211 ...
211 ...
212 {
212 {
213 "user": "<user_obj>",
213 "user": "<user_obj>",
214 "review_status": "<review_status>",
214 "review_status": "<review_status>",
215 }
215 }
216 ...
216 ...
217 ]
217 ]
218 }
218 }
219 ...
219 ...
220 ],
220 ],
221 "error": null
221 "error": null
222
222
223 """
223 """
224 repo = get_repo_or_error(repoid)
224 repo = get_repo_or_error(repoid)
225 if not has_superadmin_permission(apiuser):
225 if not has_superadmin_permission(apiuser):
226 _perms = (
226 _perms = (
227 'repository.admin', 'repository.write', 'repository.read',)
227 'repository.admin', 'repository.write', 'repository.read',)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
229
229
230 status = Optional.extract(status)
230 status = Optional.extract(status)
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 data = [pr.get_api_data() for pr in pull_requests]
232 data = [pr.get_api_data() for pr in pull_requests]
233 return data
233 return data
234
234
235
235
236 @jsonrpc_method()
236 @jsonrpc_method()
237 def merge_pull_request(
237 def merge_pull_request(
238 request, apiuser, pullrequestid, repoid=Optional(None),
238 request, apiuser, pullrequestid, repoid=Optional(None),
239 userid=Optional(OAttr('apiuser'))):
239 userid=Optional(OAttr('apiuser'))):
240 """
240 """
241 Merge the pull request specified by `pullrequestid` into its target
241 Merge the pull request specified by `pullrequestid` into its target
242 repository.
242 repository.
243
243
244 :param apiuser: This is filled automatically from the |authtoken|.
244 :param apiuser: This is filled automatically from the |authtoken|.
245 :type apiuser: AuthUser
245 :type apiuser: AuthUser
246 :param repoid: Optional, repository name or repository ID of the
246 :param repoid: Optional, repository name or repository ID of the
247 target repository to which the |pr| is to be merged.
247 target repository to which the |pr| is to be merged.
248 :type repoid: str or int
248 :type repoid: str or int
249 :param pullrequestid: ID of the pull request which shall be merged.
249 :param pullrequestid: ID of the pull request which shall be merged.
250 :type pullrequestid: int
250 :type pullrequestid: int
251 :param userid: Merge the pull request as this user.
251 :param userid: Merge the pull request as this user.
252 :type userid: Optional(str or int)
252 :type userid: Optional(str or int)
253
253
254 Example output:
254 Example output:
255
255
256 .. code-block:: bash
256 .. code-block:: bash
257
257
258 "id": <id_given_in_input>,
258 "id": <id_given_in_input>,
259 "result": {
259 "result": {
260 "executed": "<bool>",
260 "executed": "<bool>",
261 "failure_reason": "<int>",
261 "failure_reason": "<int>",
262 "merge_commit_id": "<merge_commit_id>",
262 "merge_commit_id": "<merge_commit_id>",
263 "possible": "<bool>",
263 "possible": "<bool>",
264 "merge_ref": {
264 "merge_ref": {
265 "commit_id": "<commit_id>",
265 "commit_id": "<commit_id>",
266 "type": "<type>",
266 "type": "<type>",
267 "name": "<name>"
267 "name": "<name>"
268 }
268 }
269 },
269 },
270 "error": null
270 "error": null
271 """
271 """
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if Optional.extract(repoid):
273 if Optional.extract(repoid):
274 repo = get_repo_or_error(repoid)
274 repo = get_repo_or_error(repoid)
275 else:
275 else:
276 repo = pull_request.target_repo
276 repo = pull_request.target_repo
277
277
278 if not isinstance(userid, Optional):
278 if not isinstance(userid, Optional):
279 if (has_superadmin_permission(apiuser) or
279 if (has_superadmin_permission(apiuser) or
280 HasRepoPermissionAnyApi('repository.admin')(
280 HasRepoPermissionAnyApi('repository.admin')(
281 user=apiuser, repo_name=repo.repo_name)):
281 user=apiuser, repo_name=repo.repo_name)):
282 apiuser = get_user_or_error(userid)
282 apiuser = get_user_or_error(userid)
283 else:
283 else:
284 raise JSONRPCError('userid is not the same as your user')
284 raise JSONRPCError('userid is not the same as your user')
285
285
286 check = MergeCheck.validate(
286 check = MergeCheck.validate(
287 pull_request, user=apiuser, translator=request.translate)
287 pull_request, user=apiuser, translator=request.translate)
288 merge_possible = not check.failed
288 merge_possible = not check.failed
289
289
290 if not merge_possible:
290 if not merge_possible:
291 error_messages = []
291 error_messages = []
292 for err_type, error_msg in check.errors:
292 for err_type, error_msg in check.errors:
293 error_msg = request.translate(error_msg)
293 error_msg = request.translate(error_msg)
294 error_messages.append(error_msg)
294 error_messages.append(error_msg)
295
295
296 reasons = ','.join(error_messages)
296 reasons = ','.join(error_messages)
297 raise JSONRPCError(
297 raise JSONRPCError(
298 'merge not possible for following reasons: {}'.format(reasons))
298 'merge not possible for following reasons: {}'.format(reasons))
299
299
300 target_repo = pull_request.target_repo
300 target_repo = pull_request.target_repo
301 extras = vcs_operation_context(
301 extras = vcs_operation_context(
302 request.environ, repo_name=target_repo.repo_name,
302 request.environ, repo_name=target_repo.repo_name,
303 username=apiuser.username, action='push',
303 username=apiuser.username, action='push',
304 scm=target_repo.repo_type)
304 scm=target_repo.repo_type)
305 merge_response = PullRequestModel().merge_repo(
305 merge_response = PullRequestModel().merge_repo(
306 pull_request, apiuser, extras=extras)
306 pull_request, apiuser, extras=extras)
307 if merge_response.executed:
307 if merge_response.executed:
308 PullRequestModel().close_pull_request(
308 PullRequestModel().close_pull_request(
309 pull_request.pull_request_id, apiuser)
309 pull_request.pull_request_id, apiuser)
310
310
311 Session().commit()
311 Session().commit()
312
312
313 # In previous versions the merge response directly contained the merge
313 # In previous versions the merge response directly contained the merge
314 # commit id. It is now contained in the merge reference object. To be
314 # commit id. It is now contained in the merge reference object. To be
315 # backwards compatible we have to extract it again.
315 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
316 merge_response = merge_response._asdict()
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318
318
319 return merge_response
319 return merge_response
320
320
321
321
322 @jsonrpc_method()
322 @jsonrpc_method()
323 def get_pull_request_comments(
323 def get_pull_request_comments(
324 request, apiuser, pullrequestid, repoid=Optional(None)):
324 request, apiuser, pullrequestid, repoid=Optional(None)):
325 """
325 """
326 Get all comments of pull request specified with the `pullrequestid`
326 Get all comments of pull request specified with the `pullrequestid`
327
327
328 :param apiuser: This is filled automatically from the |authtoken|.
328 :param apiuser: This is filled automatically from the |authtoken|.
329 :type apiuser: AuthUser
329 :type apiuser: AuthUser
330 :param repoid: Optional repository name or repository ID.
330 :param repoid: Optional repository name or repository ID.
331 :type repoid: str or int
331 :type repoid: str or int
332 :param pullrequestid: The pull request ID.
332 :param pullrequestid: The pull request ID.
333 :type pullrequestid: int
333 :type pullrequestid: int
334
334
335 Example output:
335 Example output:
336
336
337 .. code-block:: bash
337 .. code-block:: bash
338
338
339 id : <id_given_in_input>
339 id : <id_given_in_input>
340 result : [
340 result : [
341 {
341 {
342 "comment_author": {
342 "comment_author": {
343 "active": true,
343 "active": true,
344 "full_name_or_username": "Tom Gore",
344 "full_name_or_username": "Tom Gore",
345 "username": "admin"
345 "username": "admin"
346 },
346 },
347 "comment_created_on": "2017-01-02T18:43:45.533",
347 "comment_created_on": "2017-01-02T18:43:45.533",
348 "comment_f_path": null,
348 "comment_f_path": null,
349 "comment_id": 25,
349 "comment_id": 25,
350 "comment_lineno": null,
350 "comment_lineno": null,
351 "comment_status": {
351 "comment_status": {
352 "status": "under_review",
352 "status": "under_review",
353 "status_lbl": "Under Review"
353 "status_lbl": "Under Review"
354 },
354 },
355 "comment_text": "Example text",
355 "comment_text": "Example text",
356 "comment_type": null,
356 "comment_type": null,
357 "pull_request_version": null
357 "pull_request_version": null
358 }
358 }
359 ],
359 ],
360 error : null
360 error : null
361 """
361 """
362
362
363 pull_request = get_pull_request_or_error(pullrequestid)
363 pull_request = get_pull_request_or_error(pullrequestid)
364 if Optional.extract(repoid):
364 if Optional.extract(repoid):
365 repo = get_repo_or_error(repoid)
365 repo = get_repo_or_error(repoid)
366 else:
366 else:
367 repo = pull_request.target_repo
367 repo = pull_request.target_repo
368
368
369 if not PullRequestModel().check_user_read(
369 if not PullRequestModel().check_user_read(
370 pull_request, apiuser, api=True):
370 pull_request, apiuser, api=True):
371 raise JSONRPCError('repository `%s` or pull request `%s` '
371 raise JSONRPCError('repository `%s` or pull request `%s` '
372 'does not exist' % (repoid, pullrequestid))
372 'does not exist' % (repoid, pullrequestid))
373
373
374 (pull_request_latest,
374 (pull_request_latest,
375 pull_request_at_ver,
375 pull_request_at_ver,
376 pull_request_display_obj,
376 pull_request_display_obj,
377 at_version) = PullRequestModel().get_pr_version(
377 at_version) = PullRequestModel().get_pr_version(
378 pull_request.pull_request_id, version=None)
378 pull_request.pull_request_id, version=None)
379
379
380 versions = pull_request_display_obj.versions()
380 versions = pull_request_display_obj.versions()
381 ver_map = {
381 ver_map = {
382 ver.pull_request_version_id: cnt
382 ver.pull_request_version_id: cnt
383 for cnt, ver in enumerate(versions, 1)
383 for cnt, ver in enumerate(versions, 1)
384 }
384 }
385
385
386 # GENERAL COMMENTS with versions #
386 # GENERAL COMMENTS with versions #
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
388 q = q.order_by(ChangesetComment.comment_id.asc())
388 q = q.order_by(ChangesetComment.comment_id.asc())
389 general_comments = q.all()
389 general_comments = q.all()
390
390
391 # INLINE COMMENTS with versions #
391 # INLINE COMMENTS with versions #
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
393 q = q.order_by(ChangesetComment.comment_id.asc())
393 q = q.order_by(ChangesetComment.comment_id.asc())
394 inline_comments = q.all()
394 inline_comments = q.all()
395
395
396 data = []
396 data = []
397 for comment in inline_comments + general_comments:
397 for comment in inline_comments + general_comments:
398 full_data = comment.get_api_data()
398 full_data = comment.get_api_data()
399 pr_version_id = None
399 pr_version_id = None
400 if comment.pull_request_version_id:
400 if comment.pull_request_version_id:
401 pr_version_id = 'v{}'.format(
401 pr_version_id = 'v{}'.format(
402 ver_map[comment.pull_request_version_id])
402 ver_map[comment.pull_request_version_id])
403
403
404 # sanitize some entries
404 # sanitize some entries
405
405
406 full_data['pull_request_version'] = pr_version_id
406 full_data['pull_request_version'] = pr_version_id
407 full_data['comment_author'] = {
407 full_data['comment_author'] = {
408 'username': full_data['comment_author'].username,
408 'username': full_data['comment_author'].username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
410 'active': full_data['comment_author'].active,
410 'active': full_data['comment_author'].active,
411 }
411 }
412
412
413 if full_data['comment_status']:
413 if full_data['comment_status']:
414 full_data['comment_status'] = {
414 full_data['comment_status'] = {
415 'status': full_data['comment_status'][0].status,
415 'status': full_data['comment_status'][0].status,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
417 }
417 }
418 else:
418 else:
419 full_data['comment_status'] = {}
419 full_data['comment_status'] = {}
420
420
421 data.append(full_data)
421 data.append(full_data)
422 return data
422 return data
423
423
424
424
425 @jsonrpc_method()
425 @jsonrpc_method()
426 def comment_pull_request(
426 def comment_pull_request(
427 request, apiuser, pullrequestid, repoid=Optional(None),
427 request, apiuser, pullrequestid, repoid=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
430 resolves_comment_id=Optional(None),
430 resolves_comment_id=Optional(None),
431 userid=Optional(OAttr('apiuser'))):
431 userid=Optional(OAttr('apiuser'))):
432 """
432 """
433 Comment on the pull request specified with the `pullrequestid`,
433 Comment on the pull request specified with the `pullrequestid`,
434 in the |repo| specified by the `repoid`, and optionally change the
434 in the |repo| specified by the `repoid`, and optionally change the
435 review status.
435 review status.
436
436
437 :param apiuser: This is filled automatically from the |authtoken|.
437 :param apiuser: This is filled automatically from the |authtoken|.
438 :type apiuser: AuthUser
438 :type apiuser: AuthUser
439 :param repoid: Optional repository name or repository ID.
439 :param repoid: Optional repository name or repository ID.
440 :type repoid: str or int
440 :type repoid: str or int
441 :param pullrequestid: The pull request ID.
441 :param pullrequestid: The pull request ID.
442 :type pullrequestid: int
442 :type pullrequestid: int
443 :param commit_id: Specify the commit_id for which to set a comment. If
443 :param commit_id: Specify the commit_id for which to set a comment. If
444 given commit_id is different than latest in the PR status
444 given commit_id is different than latest in the PR status
445 change won't be performed.
445 change won't be performed.
446 :type commit_id: str
446 :type commit_id: str
447 :param message: The text content of the comment.
447 :param message: The text content of the comment.
448 :type message: str
448 :type message: str
449 :param status: (**Optional**) Set the approval status of the pull
449 :param status: (**Optional**) Set the approval status of the pull
450 request. One of: 'not_reviewed', 'approved', 'rejected',
450 request. One of: 'not_reviewed', 'approved', 'rejected',
451 'under_review'
451 'under_review'
452 :type status: str
452 :type status: str
453 :param comment_type: Comment type, one of: 'note', 'todo'
453 :param comment_type: Comment type, one of: 'note', 'todo'
454 :type comment_type: Optional(str), default: 'note'
454 :type comment_type: Optional(str), default: 'note'
455 :param userid: Comment on the pull request as this user
455 :param userid: Comment on the pull request as this user
456 :type userid: Optional(str or int)
456 :type userid: Optional(str or int)
457
457
458 Example output:
458 Example output:
459
459
460 .. code-block:: bash
460 .. code-block:: bash
461
461
462 id : <id_given_in_input>
462 id : <id_given_in_input>
463 result : {
463 result : {
464 "pull_request_id": "<Integer>",
464 "pull_request_id": "<Integer>",
465 "comment_id": "<Integer>",
465 "comment_id": "<Integer>",
466 "status": {"given": <given_status>,
466 "status": {"given": <given_status>,
467 "was_changed": <bool status_was_actually_changed> },
467 "was_changed": <bool status_was_actually_changed> },
468 },
468 },
469 error : null
469 error : null
470 """
470 """
471 pull_request = get_pull_request_or_error(pullrequestid)
471 pull_request = get_pull_request_or_error(pullrequestid)
472 if Optional.extract(repoid):
472 if Optional.extract(repoid):
473 repo = get_repo_or_error(repoid)
473 repo = get_repo_or_error(repoid)
474 else:
474 else:
475 repo = pull_request.target_repo
475 repo = pull_request.target_repo
476
476
477 if not isinstance(userid, Optional):
477 if not isinstance(userid, Optional):
478 if (has_superadmin_permission(apiuser) or
478 if (has_superadmin_permission(apiuser) or
479 HasRepoPermissionAnyApi('repository.admin')(
479 HasRepoPermissionAnyApi('repository.admin')(
480 user=apiuser, repo_name=repo.repo_name)):
480 user=apiuser, repo_name=repo.repo_name)):
481 apiuser = get_user_or_error(userid)
481 apiuser = get_user_or_error(userid)
482 else:
482 else:
483 raise JSONRPCError('userid is not the same as your user')
483 raise JSONRPCError('userid is not the same as your user')
484
484
485 if not PullRequestModel().check_user_read(
485 if not PullRequestModel().check_user_read(
486 pull_request, apiuser, api=True):
486 pull_request, apiuser, api=True):
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
488 message = Optional.extract(message)
488 message = Optional.extract(message)
489 status = Optional.extract(status)
489 status = Optional.extract(status)
490 commit_id = Optional.extract(commit_id)
490 commit_id = Optional.extract(commit_id)
491 comment_type = Optional.extract(comment_type)
491 comment_type = Optional.extract(comment_type)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
493
493
494 if not message and not status:
494 if not message and not status:
495 raise JSONRPCError(
495 raise JSONRPCError(
496 'Both message and status parameters are missing. '
496 'Both message and status parameters are missing. '
497 'At least one is required.')
497 'At least one is required.')
498
498
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
500 status is not None):
500 status is not None):
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
502
502
503 if commit_id and commit_id not in pull_request.revisions:
503 if commit_id and commit_id not in pull_request.revisions:
504 raise JSONRPCError(
504 raise JSONRPCError(
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
506
506
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
508 pull_request, apiuser)
508 pull_request, apiuser)
509
509
510 # if commit_id is passed re-validated if user is allowed to change status
510 # if commit_id is passed re-validated if user is allowed to change status
511 # based on latest commit_id from the PR
511 # based on latest commit_id from the PR
512 if commit_id:
512 if commit_id:
513 commit_idx = pull_request.revisions.index(commit_id)
513 commit_idx = pull_request.revisions.index(commit_id)
514 if commit_idx != 0:
514 if commit_idx != 0:
515 allowed_to_change_status = False
515 allowed_to_change_status = False
516
516
517 if resolves_comment_id:
517 if resolves_comment_id:
518 comment = ChangesetComment.get(resolves_comment_id)
518 comment = ChangesetComment.get(resolves_comment_id)
519 if not comment:
519 if not comment:
520 raise JSONRPCError(
520 raise JSONRPCError(
521 'Invalid resolves_comment_id `%s` for this pull request.'
521 'Invalid resolves_comment_id `%s` for this pull request.'
522 % resolves_comment_id)
522 % resolves_comment_id)
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
524 raise JSONRPCError(
524 raise JSONRPCError(
525 'Comment `%s` is wrong type for setting status to resolved.'
525 'Comment `%s` is wrong type for setting status to resolved.'
526 % resolves_comment_id)
526 % resolves_comment_id)
527
527
528 text = message
528 text = message
529 status_label = ChangesetStatus.get_status_lbl(status)
529 status_label = ChangesetStatus.get_status_lbl(status)
530 if status and allowed_to_change_status:
530 if status and allowed_to_change_status:
531 st_message = ('Status change %(transition_icon)s %(status)s'
531 st_message = ('Status change %(transition_icon)s %(status)s'
532 % {'transition_icon': '>', 'status': status_label})
532 % {'transition_icon': '>', 'status': status_label})
533 text = message or st_message
533 text = message or st_message
534
534
535 rc_config = SettingsModel().get_all_settings()
535 rc_config = SettingsModel().get_all_settings()
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
537
537
538 status_change = status and allowed_to_change_status
538 status_change = status and allowed_to_change_status
539 comment = CommentsModel().create(
539 comment = CommentsModel().create(
540 text=text,
540 text=text,
541 repo=pull_request.target_repo.repo_id,
541 repo=pull_request.target_repo.repo_id,
542 user=apiuser.user_id,
542 user=apiuser.user_id,
543 pull_request=pull_request.pull_request_id,
543 pull_request=pull_request.pull_request_id,
544 f_path=None,
544 f_path=None,
545 line_no=None,
545 line_no=None,
546 status_change=(status_label if status_change else None),
546 status_change=(status_label if status_change else None),
547 status_change_type=(status if status_change else None),
547 status_change_type=(status if status_change else None),
548 closing_pr=False,
548 closing_pr=False,
549 renderer=renderer,
549 renderer=renderer,
550 comment_type=comment_type,
550 comment_type=comment_type,
551 resolves_comment_id=resolves_comment_id,
551 resolves_comment_id=resolves_comment_id,
552 auth_user=apiuser
552 auth_user=apiuser
553 )
553 )
554
554
555 if allowed_to_change_status and status:
555 if allowed_to_change_status and status:
556 ChangesetStatusModel().set_status(
556 ChangesetStatusModel().set_status(
557 pull_request.target_repo.repo_id,
557 pull_request.target_repo.repo_id,
558 status,
558 status,
559 apiuser.user_id,
559 apiuser.user_id,
560 comment,
560 comment,
561 pull_request=pull_request.pull_request_id
561 pull_request=pull_request.pull_request_id
562 )
562 )
563 Session().flush()
563 Session().flush()
564
564
565 Session().commit()
565 Session().commit()
566 data = {
566 data = {
567 'pull_request_id': pull_request.pull_request_id,
567 'pull_request_id': pull_request.pull_request_id,
568 'comment_id': comment.comment_id if comment else None,
568 'comment_id': comment.comment_id if comment else None,
569 'status': {'given': status, 'was_changed': status_change},
569 'status': {'given': status, 'was_changed': status_change},
570 }
570 }
571 return data
571 return data
572
572
573
573
574 @jsonrpc_method()
574 @jsonrpc_method()
575 def create_pull_request(
575 def create_pull_request(
576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
577 title=Optional(''), description=Optional(''), reviewers=Optional(None)):
577 title=Optional(''), description=Optional(''), reviewers=Optional(None)):
578 """
578 """
579 Creates a new pull request.
579 Creates a new pull request.
580
580
581 Accepts refs in the following formats:
581 Accepts refs in the following formats:
582
582
583 * branch:<branch_name>:<sha>
583 * branch:<branch_name>:<sha>
584 * branch:<branch_name>
584 * branch:<branch_name>
585 * bookmark:<bookmark_name>:<sha> (Mercurial only)
585 * bookmark:<bookmark_name>:<sha> (Mercurial only)
586 * bookmark:<bookmark_name> (Mercurial only)
586 * bookmark:<bookmark_name> (Mercurial only)
587
587
588 :param apiuser: This is filled automatically from the |authtoken|.
588 :param apiuser: This is filled automatically from the |authtoken|.
589 :type apiuser: AuthUser
589 :type apiuser: AuthUser
590 :param source_repo: Set the source repository name.
590 :param source_repo: Set the source repository name.
591 :type source_repo: str
591 :type source_repo: str
592 :param target_repo: Set the target repository name.
592 :param target_repo: Set the target repository name.
593 :type target_repo: str
593 :type target_repo: str
594 :param source_ref: Set the source ref name.
594 :param source_ref: Set the source ref name.
595 :type source_ref: str
595 :type source_ref: str
596 :param target_ref: Set the target ref name.
596 :param target_ref: Set the target ref name.
597 :type target_ref: str
597 :type target_ref: str
598 :param title: Optionally Set the pull request title, it's generated otherwise
598 :param title: Optionally Set the pull request title, it's generated otherwise
599 :type title: str
599 :type title: str
600 :param description: Set the pull request description.
600 :param description: Set the pull request description.
601 :type description: Optional(str)
601 :type description: Optional(str)
602 :param reviewers: Set the new pull request reviewers list.
602 :param reviewers: Set the new pull request reviewers list.
603 Reviewer defined by review rules will be added automatically to the
603 Reviewer defined by review rules will be added automatically to the
604 defined list.
604 defined list.
605 :type reviewers: Optional(list)
605 :type reviewers: Optional(list)
606 Accepts username strings or objects of the format:
606 Accepts username strings or objects of the format:
607
607
608 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
608 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
609 """
609 """
610
610
611 source_db_repo = get_repo_or_error(source_repo)
611 source_db_repo = get_repo_or_error(source_repo)
612 target_db_repo = get_repo_or_error(target_repo)
612 target_db_repo = get_repo_or_error(target_repo)
613 if not has_superadmin_permission(apiuser):
613 if not has_superadmin_permission(apiuser):
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
615 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
615 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
616
616
617 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
617 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
618 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
618 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
619
619
620 source_scm = source_db_repo.scm_instance()
620 source_scm = source_db_repo.scm_instance()
621 target_scm = target_db_repo.scm_instance()
621 target_scm = target_db_repo.scm_instance()
622
622
623 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
623 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
624 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
624 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
625
625
626 ancestor = source_scm.get_common_ancestor(
626 ancestor = source_scm.get_common_ancestor(
627 source_commit.raw_id, target_commit.raw_id, target_scm)
627 source_commit.raw_id, target_commit.raw_id, target_scm)
628 if not ancestor:
628 if not ancestor:
629 raise JSONRPCError('no common ancestor found')
629 raise JSONRPCError('no common ancestor found')
630
630
631 # recalculate target ref based on ancestor
631 # recalculate target ref based on ancestor
632 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
632 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
633 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
633 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
634
634
635 commit_ranges = target_scm.compare(
635 commit_ranges = target_scm.compare(
636 target_commit.raw_id, source_commit.raw_id, source_scm,
636 target_commit.raw_id, source_commit.raw_id, source_scm,
637 merge=True, pre_load=[])
637 merge=True, pre_load=[])
638
638
639 if not commit_ranges:
639 if not commit_ranges:
640 raise JSONRPCError('no commits found')
640 raise JSONRPCError('no commits found')
641
641
642 reviewer_objects = Optional.extract(reviewers) or []
642 reviewer_objects = Optional.extract(reviewers) or []
643
643
644 # serialize and validate passed in given reviewers
644 if reviewer_objects:
645 if reviewer_objects:
645 schema = ReviewerListSchema()
646 schema = ReviewerListSchema()
646 try:
647 try:
647 reviewer_objects = schema.deserialize(reviewer_objects)
648 reviewer_objects = schema.deserialize(reviewer_objects)
648 except Invalid as err:
649 except Invalid as err:
649 raise JSONRPCValidationError(colander_exc=err)
650 raise JSONRPCValidationError(colander_exc=err)
650
651
651 # validate users
652 # validate users
652 for reviewer_object in reviewer_objects:
653 for reviewer_object in reviewer_objects:
653 user = get_user_or_error(reviewer_object['username'])
654 user = get_user_or_error(reviewer_object['username'])
654 reviewer_object['user_id'] = user.user_id
655 reviewer_object['user_id'] = user.user_id
655
656
656 get_default_reviewers_data, get_validated_reviewers = \
657 get_default_reviewers_data, validate_default_reviewers = \
657 PullRequestModel().get_reviewer_functions()
658 PullRequestModel().get_reviewer_functions()
658
659
660 # recalculate reviewers logic, to make sure we can validate this
659 reviewer_rules = get_default_reviewers_data(
661 reviewer_rules = get_default_reviewers_data(
660 apiuser.get_instance(), source_db_repo,
662 apiuser.get_instance(), source_db_repo,
661 source_commit, target_db_repo, target_commit)
663 source_commit, target_db_repo, target_commit)
662
664
663 # specified rules are later re-validated, thus we can assume users will
665 # now MERGE our given with the calculated
664 # eventually provide those that meet the reviewer criteria.
666 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
665 if not reviewer_objects:
666 reviewer_objects = reviewer_rules['reviewers']
667
667
668 try:
668 try:
669 reviewers = get_validated_reviewers(
669 reviewers = validate_default_reviewers(
670 reviewer_objects, reviewer_rules)
670 reviewer_objects, reviewer_rules)
671 except ValueError as e:
671 except ValueError as e:
672 raise JSONRPCError('Reviewers Validation: {}'.format(e))
672 raise JSONRPCError('Reviewers Validation: {}'.format(e))
673
673
674 title = Optional.extract(title)
674 title = Optional.extract(title)
675 if not title:
675 if not title:
676 title_source_ref = source_ref.split(':', 2)[1]
676 title_source_ref = source_ref.split(':', 2)[1]
677 title = PullRequestModel().generate_pullrequest_title(
677 title = PullRequestModel().generate_pullrequest_title(
678 source=source_repo,
678 source=source_repo,
679 source_ref=title_source_ref,
679 source_ref=title_source_ref,
680 target=target_repo
680 target=target_repo
681 )
681 )
682 description = Optional.extract(description)
682 description = Optional.extract(description)
683
683
684 pull_request = PullRequestModel().create(
684 pull_request = PullRequestModel().create(
685 created_by=apiuser.user_id,
685 created_by=apiuser.user_id,
686 source_repo=source_repo,
686 source_repo=source_repo,
687 source_ref=full_source_ref,
687 source_ref=full_source_ref,
688 target_repo=target_repo,
688 target_repo=target_repo,
689 target_ref=full_target_ref,
689 target_ref=full_target_ref,
690 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
690 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
691 reviewers=reviewers,
691 reviewers=reviewers,
692 title=title,
692 title=title,
693 description=description,
693 description=description,
694 reviewer_data=reviewer_rules,
694 reviewer_data=reviewer_rules,
695 auth_user=apiuser
695 auth_user=apiuser
696 )
696 )
697
697
698 Session().commit()
698 Session().commit()
699 data = {
699 data = {
700 'msg': 'Created new pull request `{}`'.format(title),
700 'msg': 'Created new pull request `{}`'.format(title),
701 'pull_request_id': pull_request.pull_request_id,
701 'pull_request_id': pull_request.pull_request_id,
702 }
702 }
703 return data
703 return data
704
704
705
705
706 @jsonrpc_method()
706 @jsonrpc_method()
707 def update_pull_request(
707 def update_pull_request(
708 request, apiuser, pullrequestid, repoid=Optional(None),
708 request, apiuser, pullrequestid, repoid=Optional(None),
709 title=Optional(''), description=Optional(''), reviewers=Optional(None),
709 title=Optional(''), description=Optional(''), reviewers=Optional(None),
710 update_commits=Optional(None)):
710 update_commits=Optional(None)):
711 """
711 """
712 Updates a pull request.
712 Updates a pull request.
713
713
714 :param apiuser: This is filled automatically from the |authtoken|.
714 :param apiuser: This is filled automatically from the |authtoken|.
715 :type apiuser: AuthUser
715 :type apiuser: AuthUser
716 :param repoid: Optional repository name or repository ID.
716 :param repoid: Optional repository name or repository ID.
717 :type repoid: str or int
717 :type repoid: str or int
718 :param pullrequestid: The pull request ID.
718 :param pullrequestid: The pull request ID.
719 :type pullrequestid: int
719 :type pullrequestid: int
720 :param title: Set the pull request title.
720 :param title: Set the pull request title.
721 :type title: str
721 :type title: str
722 :param description: Update pull request description.
722 :param description: Update pull request description.
723 :type description: Optional(str)
723 :type description: Optional(str)
724 :param reviewers: Update pull request reviewers list with new value.
724 :param reviewers: Update pull request reviewers list with new value.
725 :type reviewers: Optional(list)
725 :type reviewers: Optional(list)
726 Accepts username strings or objects of the format:
726 Accepts username strings or objects of the format:
727
727
728 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
728 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
729
729
730 :param update_commits: Trigger update of commits for this pull request
730 :param update_commits: Trigger update of commits for this pull request
731 :type: update_commits: Optional(bool)
731 :type: update_commits: Optional(bool)
732
732
733 Example output:
733 Example output:
734
734
735 .. code-block:: bash
735 .. code-block:: bash
736
736
737 id : <id_given_in_input>
737 id : <id_given_in_input>
738 result : {
738 result : {
739 "msg": "Updated pull request `63`",
739 "msg": "Updated pull request `63`",
740 "pull_request": <pull_request_object>,
740 "pull_request": <pull_request_object>,
741 "updated_reviewers": {
741 "updated_reviewers": {
742 "added": [
742 "added": [
743 "username"
743 "username"
744 ],
744 ],
745 "removed": []
745 "removed": []
746 },
746 },
747 "updated_commits": {
747 "updated_commits": {
748 "added": [
748 "added": [
749 "<sha1_hash>"
749 "<sha1_hash>"
750 ],
750 ],
751 "common": [
751 "common": [
752 "<sha1_hash>",
752 "<sha1_hash>",
753 "<sha1_hash>",
753 "<sha1_hash>",
754 ],
754 ],
755 "removed": []
755 "removed": []
756 }
756 }
757 }
757 }
758 error : null
758 error : null
759 """
759 """
760
760
761 pull_request = get_pull_request_or_error(pullrequestid)
761 pull_request = get_pull_request_or_error(pullrequestid)
762 if Optional.extract(repoid):
762 if Optional.extract(repoid):
763 repo = get_repo_or_error(repoid)
763 repo = get_repo_or_error(repoid)
764 else:
764 else:
765 repo = pull_request.target_repo
765 repo = pull_request.target_repo
766
766
767 if not PullRequestModel().check_user_update(
767 if not PullRequestModel().check_user_update(
768 pull_request, apiuser, api=True):
768 pull_request, apiuser, api=True):
769 raise JSONRPCError(
769 raise JSONRPCError(
770 'pull request `%s` update failed, no permission to update.' % (
770 'pull request `%s` update failed, no permission to update.' % (
771 pullrequestid,))
771 pullrequestid,))
772 if pull_request.is_closed():
772 if pull_request.is_closed():
773 raise JSONRPCError(
773 raise JSONRPCError(
774 'pull request `%s` update failed, pull request is closed' % (
774 'pull request `%s` update failed, pull request is closed' % (
775 pullrequestid,))
775 pullrequestid,))
776
776
777 reviewer_objects = Optional.extract(reviewers) or []
777 reviewer_objects = Optional.extract(reviewers) or []
778
778
779 if reviewer_objects:
779 if reviewer_objects:
780 schema = ReviewerListSchema()
780 schema = ReviewerListSchema()
781 try:
781 try:
782 reviewer_objects = schema.deserialize(reviewer_objects)
782 reviewer_objects = schema.deserialize(reviewer_objects)
783 except Invalid as err:
783 except Invalid as err:
784 raise JSONRPCValidationError(colander_exc=err)
784 raise JSONRPCValidationError(colander_exc=err)
785
785
786 # validate users
786 # validate users
787 for reviewer_object in reviewer_objects:
787 for reviewer_object in reviewer_objects:
788 user = get_user_or_error(reviewer_object['username'])
788 user = get_user_or_error(reviewer_object['username'])
789 reviewer_object['user_id'] = user.user_id
789 reviewer_object['user_id'] = user.user_id
790
790
791 get_default_reviewers_data, get_validated_reviewers = \
791 get_default_reviewers_data, get_validated_reviewers = \
792 PullRequestModel().get_reviewer_functions()
792 PullRequestModel().get_reviewer_functions()
793
793
794 # re-use stored rules
794 # re-use stored rules
795 reviewer_rules = pull_request.reviewer_data
795 reviewer_rules = pull_request.reviewer_data
796 try:
796 try:
797 reviewers = get_validated_reviewers(
797 reviewers = get_validated_reviewers(
798 reviewer_objects, reviewer_rules)
798 reviewer_objects, reviewer_rules)
799 except ValueError as e:
799 except ValueError as e:
800 raise JSONRPCError('Reviewers Validation: {}'.format(e))
800 raise JSONRPCError('Reviewers Validation: {}'.format(e))
801 else:
801 else:
802 reviewers = []
802 reviewers = []
803
803
804 title = Optional.extract(title)
804 title = Optional.extract(title)
805 description = Optional.extract(description)
805 description = Optional.extract(description)
806 if title or description:
806 if title or description:
807 PullRequestModel().edit(
807 PullRequestModel().edit(
808 pull_request, title or pull_request.title,
808 pull_request, title or pull_request.title,
809 description or pull_request.description, apiuser)
809 description or pull_request.description, apiuser)
810 Session().commit()
810 Session().commit()
811
811
812 commit_changes = {"added": [], "common": [], "removed": []}
812 commit_changes = {"added": [], "common": [], "removed": []}
813 if str2bool(Optional.extract(update_commits)):
813 if str2bool(Optional.extract(update_commits)):
814 if PullRequestModel().has_valid_update_type(pull_request):
814 if PullRequestModel().has_valid_update_type(pull_request):
815 update_response = PullRequestModel().update_commits(
815 update_response = PullRequestModel().update_commits(
816 pull_request)
816 pull_request)
817 commit_changes = update_response.changes or commit_changes
817 commit_changes = update_response.changes or commit_changes
818 Session().commit()
818 Session().commit()
819
819
820 reviewers_changes = {"added": [], "removed": []}
820 reviewers_changes = {"added": [], "removed": []}
821 if reviewers:
821 if reviewers:
822 added_reviewers, removed_reviewers = \
822 added_reviewers, removed_reviewers = \
823 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
823 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
824
824
825 reviewers_changes['added'] = sorted(
825 reviewers_changes['added'] = sorted(
826 [get_user_or_error(n).username for n in added_reviewers])
826 [get_user_or_error(n).username for n in added_reviewers])
827 reviewers_changes['removed'] = sorted(
827 reviewers_changes['removed'] = sorted(
828 [get_user_or_error(n).username for n in removed_reviewers])
828 [get_user_or_error(n).username for n in removed_reviewers])
829 Session().commit()
829 Session().commit()
830
830
831 data = {
831 data = {
832 'msg': 'Updated pull request `{}`'.format(
832 'msg': 'Updated pull request `{}`'.format(
833 pull_request.pull_request_id),
833 pull_request.pull_request_id),
834 'pull_request': pull_request.get_api_data(),
834 'pull_request': pull_request.get_api_data(),
835 'updated_commits': commit_changes,
835 'updated_commits': commit_changes,
836 'updated_reviewers': reviewers_changes
836 'updated_reviewers': reviewers_changes
837 }
837 }
838
838
839 return data
839 return data
840
840
841
841
842 @jsonrpc_method()
842 @jsonrpc_method()
843 def close_pull_request(
843 def close_pull_request(
844 request, apiuser, pullrequestid, repoid=Optional(None),
844 request, apiuser, pullrequestid, repoid=Optional(None),
845 userid=Optional(OAttr('apiuser')), message=Optional('')):
845 userid=Optional(OAttr('apiuser')), message=Optional('')):
846 """
846 """
847 Close the pull request specified by `pullrequestid`.
847 Close the pull request specified by `pullrequestid`.
848
848
849 :param apiuser: This is filled automatically from the |authtoken|.
849 :param apiuser: This is filled automatically from the |authtoken|.
850 :type apiuser: AuthUser
850 :type apiuser: AuthUser
851 :param repoid: Repository name or repository ID to which the pull
851 :param repoid: Repository name or repository ID to which the pull
852 request belongs.
852 request belongs.
853 :type repoid: str or int
853 :type repoid: str or int
854 :param pullrequestid: ID of the pull request to be closed.
854 :param pullrequestid: ID of the pull request to be closed.
855 :type pullrequestid: int
855 :type pullrequestid: int
856 :param userid: Close the pull request as this user.
856 :param userid: Close the pull request as this user.
857 :type userid: Optional(str or int)
857 :type userid: Optional(str or int)
858 :param message: Optional message to close the Pull Request with. If not
858 :param message: Optional message to close the Pull Request with. If not
859 specified it will be generated automatically.
859 specified it will be generated automatically.
860 :type message: Optional(str)
860 :type message: Optional(str)
861
861
862 Example output:
862 Example output:
863
863
864 .. code-block:: bash
864 .. code-block:: bash
865
865
866 "id": <id_given_in_input>,
866 "id": <id_given_in_input>,
867 "result": {
867 "result": {
868 "pull_request_id": "<int>",
868 "pull_request_id": "<int>",
869 "close_status": "<str:status_lbl>,
869 "close_status": "<str:status_lbl>,
870 "closed": "<bool>"
870 "closed": "<bool>"
871 },
871 },
872 "error": null
872 "error": null
873
873
874 """
874 """
875 _ = request.translate
875 _ = request.translate
876
876
877 pull_request = get_pull_request_or_error(pullrequestid)
877 pull_request = get_pull_request_or_error(pullrequestid)
878 if Optional.extract(repoid):
878 if Optional.extract(repoid):
879 repo = get_repo_or_error(repoid)
879 repo = get_repo_or_error(repoid)
880 else:
880 else:
881 repo = pull_request.target_repo
881 repo = pull_request.target_repo
882
882
883 if not isinstance(userid, Optional):
883 if not isinstance(userid, Optional):
884 if (has_superadmin_permission(apiuser) or
884 if (has_superadmin_permission(apiuser) or
885 HasRepoPermissionAnyApi('repository.admin')(
885 HasRepoPermissionAnyApi('repository.admin')(
886 user=apiuser, repo_name=repo.repo_name)):
886 user=apiuser, repo_name=repo.repo_name)):
887 apiuser = get_user_or_error(userid)
887 apiuser = get_user_or_error(userid)
888 else:
888 else:
889 raise JSONRPCError('userid is not the same as your user')
889 raise JSONRPCError('userid is not the same as your user')
890
890
891 if pull_request.is_closed():
891 if pull_request.is_closed():
892 raise JSONRPCError(
892 raise JSONRPCError(
893 'pull request `%s` is already closed' % (pullrequestid,))
893 'pull request `%s` is already closed' % (pullrequestid,))
894
894
895 # only owner or admin or person with write permissions
895 # only owner or admin or person with write permissions
896 allowed_to_close = PullRequestModel().check_user_update(
896 allowed_to_close = PullRequestModel().check_user_update(
897 pull_request, apiuser, api=True)
897 pull_request, apiuser, api=True)
898
898
899 if not allowed_to_close:
899 if not allowed_to_close:
900 raise JSONRPCError(
900 raise JSONRPCError(
901 'pull request `%s` close failed, no permission to close.' % (
901 'pull request `%s` close failed, no permission to close.' % (
902 pullrequestid,))
902 pullrequestid,))
903
903
904 # message we're using to close the PR, else it's automatically generated
904 # message we're using to close the PR, else it's automatically generated
905 message = Optional.extract(message)
905 message = Optional.extract(message)
906
906
907 # finally close the PR, with proper message comment
907 # finally close the PR, with proper message comment
908 comment, status = PullRequestModel().close_pull_request_with_comment(
908 comment, status = PullRequestModel().close_pull_request_with_comment(
909 pull_request, apiuser, repo, message=message)
909 pull_request, apiuser, repo, message=message)
910 status_lbl = ChangesetStatus.get_status_lbl(status)
910 status_lbl = ChangesetStatus.get_status_lbl(status)
911
911
912 Session().commit()
912 Session().commit()
913
913
914 data = {
914 data = {
915 'pull_request_id': pull_request.pull_request_id,
915 'pull_request_id': pull_request.pull_request_id,
916 'close_status': status_lbl,
916 'close_status': status_lbl,
917 'closed': True,
917 'closed': True,
918 }
918 }
919 return data
919 return data
@@ -1,1315 +1,1316 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 RepositoryRequirementError, EmptyRepositoryError)
45 RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 ChangesetComment, ChangesetStatus, Repository)
49 ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64
64
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, source=source, opened_by=opened_by,
79 repo_name, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, source=source, statuses=statuses,
83 repo_name, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, source=source, opened_by=opened_by,
87 repo_name, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, source=source, opened_by=opened_by,
96 repo_name, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, source=source, statuses=statuses,
100 repo_name, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.target_repo.repo_name),
112 'name_raw': pr.pull_request_id,
112 'name_raw': pr.pull_request_id,
113 'status': _render('pullrequest_status',
113 'status': _render('pullrequest_status',
114 pr.calculated_review_status()),
114 pr.calculated_review_status()),
115 'title': _render(
115 'title': _render(
116 'pullrequest_title', pr.title, pr.description),
116 'pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'author': _render('pullrequest_author',
124 'author': _render('pullrequest_author',
125 pr.author.full_contact, ),
125 pr.author.full_contact, ),
126 'author_raw': pr.author.full_name,
126 'author_raw': pr.author.full_name,
127 'comments': _render('pullrequest_comments', len(comments)),
127 'comments': _render('pullrequest_comments', len(comments)),
128 'comments_raw': len(comments),
128 'comments_raw': len(comments),
129 'closed': pr.is_closed(),
129 'closed': pr.is_closed(),
130 })
130 })
131
131
132 data = ({
132 data = ({
133 'draw': draw,
133 'draw': draw,
134 'data': data,
134 'data': data,
135 'recordsTotal': pull_requests_total_count,
135 'recordsTotal': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
137 })
137 })
138 return data
138 return data
139
139
140 @LoginRequired()
140 @LoginRequired()
141 @HasRepoPermissionAnyDecorator(
141 @HasRepoPermissionAnyDecorator(
142 'repository.read', 'repository.write', 'repository.admin')
142 'repository.read', 'repository.write', 'repository.admin')
143 @view_config(
143 @view_config(
144 route_name='pullrequest_show_all', request_method='GET',
144 route_name='pullrequest_show_all', request_method='GET',
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 def pull_request_list(self):
146 def pull_request_list(self):
147 c = self.load_default_context()
147 c = self.load_default_context()
148
148
149 req_get = self.request.GET
149 req_get = self.request.GET
150 c.source = str2bool(req_get.get('source'))
150 c.source = str2bool(req_get.get('source'))
151 c.closed = str2bool(req_get.get('closed'))
151 c.closed = str2bool(req_get.get('closed'))
152 c.my = str2bool(req_get.get('my'))
152 c.my = str2bool(req_get.get('my'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155
155
156 c.active = 'open'
156 c.active = 'open'
157 if c.my:
157 if c.my:
158 c.active = 'my'
158 c.active = 'my'
159 if c.closed:
159 if c.closed:
160 c.active = 'closed'
160 c.active = 'closed'
161 if c.awaiting_review and not c.source:
161 if c.awaiting_review and not c.source:
162 c.active = 'awaiting'
162 c.active = 'awaiting'
163 if c.source and not c.awaiting_review:
163 if c.source and not c.awaiting_review:
164 c.active = 'source'
164 c.active = 'source'
165 if c.awaiting_my_review:
165 if c.awaiting_my_review:
166 c.active = 'awaiting_my'
166 c.active = 'awaiting_my'
167
167
168 return self._get_template_context(c)
168 return self._get_template_context(c)
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='pullrequest_show_all_data', request_method='GET',
174 route_name='pullrequest_show_all_data', request_method='GET',
175 renderer='json_ext', xhr=True)
175 renderer='json_ext', xhr=True)
176 def pull_request_list_data(self):
176 def pull_request_list_data(self):
177 self.load_default_context()
177 self.load_default_context()
178
178
179 # additional filters
179 # additional filters
180 req_get = self.request.GET
180 req_get = self.request.GET
181 source = str2bool(req_get.get('source'))
181 source = str2bool(req_get.get('source'))
182 closed = str2bool(req_get.get('closed'))
182 closed = str2bool(req_get.get('closed'))
183 my = str2bool(req_get.get('my'))
183 my = str2bool(req_get.get('my'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186
186
187 filter_type = 'awaiting_review' if awaiting_review \
187 filter_type = 'awaiting_review' if awaiting_review \
188 else 'awaiting_my_review' if awaiting_my_review \
188 else 'awaiting_my_review' if awaiting_my_review \
189 else None
189 else None
190
190
191 opened_by = None
191 opened_by = None
192 if my:
192 if my:
193 opened_by = [self._rhodecode_user.user_id]
193 opened_by = [self._rhodecode_user.user_id]
194
194
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 if closed:
196 if closed:
197 statuses = [PullRequest.STATUS_CLOSED]
197 statuses = [PullRequest.STATUS_CLOSED]
198
198
199 data = self._get_pull_requests_list(
199 data = self._get_pull_requests_list(
200 repo_name=self.db_repo_name, source=source,
200 repo_name=self.db_repo_name, source=source,
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202
202
203 return data
203 return data
204
204
205 def _is_diff_cache_enabled(self, target_repo):
205 def _is_diff_cache_enabled(self, target_repo):
206 caching_enabled = self._get_general_setting(
206 caching_enabled = self._get_general_setting(
207 target_repo, 'rhodecode_diff_cache')
207 target_repo, 'rhodecode_diff_cache')
208 log.debug('Diff caching enabled: %s', caching_enabled)
208 log.debug('Diff caching enabled: %s', caching_enabled)
209 return caching_enabled
209 return caching_enabled
210
210
211 def _get_diffset(self, source_repo_name, source_repo,
211 def _get_diffset(self, source_repo_name, source_repo,
212 source_ref_id, target_ref_id,
212 source_ref_id, target_ref_id,
213 target_commit, source_commit, diff_limit, file_limit,
213 target_commit, source_commit, diff_limit, file_limit,
214 fulldiff):
214 fulldiff):
215
215
216 vcs_diff = PullRequestModel().get_diff(
216 vcs_diff = PullRequestModel().get_diff(
217 source_repo, source_ref_id, target_ref_id)
217 source_repo, source_ref_id, target_ref_id)
218
218
219 diff_processor = diffs.DiffProcessor(
219 diff_processor = diffs.DiffProcessor(
220 vcs_diff, format='newdiff', diff_limit=diff_limit,
220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 file_limit=file_limit, show_full_diff=fulldiff)
221 file_limit=file_limit, show_full_diff=fulldiff)
222
222
223 _parsed = diff_processor.prepare()
223 _parsed = diff_processor.prepare()
224
224
225 diffset = codeblocks.DiffSet(
225 diffset = codeblocks.DiffSet(
226 repo_name=self.db_repo_name,
226 repo_name=self.db_repo_name,
227 source_repo_name=source_repo_name,
227 source_repo_name=source_repo_name,
228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 )
230 )
231 diffset = self.path_filter.render_patchset_filtered(
231 diffset = self.path_filter.render_patchset_filtered(
232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233
233
234 return diffset
234 return diffset
235
235
236 @LoginRequired()
236 @LoginRequired()
237 @HasRepoPermissionAnyDecorator(
237 @HasRepoPermissionAnyDecorator(
238 'repository.read', 'repository.write', 'repository.admin')
238 'repository.read', 'repository.write', 'repository.admin')
239 @view_config(
239 @view_config(
240 route_name='pullrequest_show', request_method='GET',
240 route_name='pullrequest_show', request_method='GET',
241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 def pull_request_show(self):
242 def pull_request_show(self):
243 pull_request_id = self.request.matchdict['pull_request_id']
243 pull_request_id = self.request.matchdict['pull_request_id']
244
244
245 c = self.load_default_context()
245 c = self.load_default_context()
246
246
247 version = self.request.GET.get('version')
247 version = self.request.GET.get('version')
248 from_version = self.request.GET.get('from_version') or version
248 from_version = self.request.GET.get('from_version') or version
249 merge_checks = self.request.GET.get('merge_checks')
249 merge_checks = self.request.GET.get('merge_checks')
250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 force_refresh = str2bool(self.request.GET.get('force_refresh'))
251 force_refresh = str2bool(self.request.GET.get('force_refresh'))
252
252
253 (pull_request_latest,
253 (pull_request_latest,
254 pull_request_at_ver,
254 pull_request_at_ver,
255 pull_request_display_obj,
255 pull_request_display_obj,
256 at_version) = PullRequestModel().get_pr_version(
256 at_version) = PullRequestModel().get_pr_version(
257 pull_request_id, version=version)
257 pull_request_id, version=version)
258 pr_closed = pull_request_latest.is_closed()
258 pr_closed = pull_request_latest.is_closed()
259
259
260 if pr_closed and (version or from_version):
260 if pr_closed and (version or from_version):
261 # not allow to browse versions
261 # not allow to browse versions
262 raise HTTPFound(h.route_path(
262 raise HTTPFound(h.route_path(
263 'pullrequest_show', repo_name=self.db_repo_name,
263 'pullrequest_show', repo_name=self.db_repo_name,
264 pull_request_id=pull_request_id))
264 pull_request_id=pull_request_id))
265
265
266 versions = pull_request_display_obj.versions()
266 versions = pull_request_display_obj.versions()
267
267
268 c.at_version = at_version
268 c.at_version = at_version
269 c.at_version_num = (at_version
269 c.at_version_num = (at_version
270 if at_version and at_version != 'latest'
270 if at_version and at_version != 'latest'
271 else None)
271 else None)
272 c.at_version_pos = ChangesetComment.get_index_from_version(
272 c.at_version_pos = ChangesetComment.get_index_from_version(
273 c.at_version_num, versions)
273 c.at_version_num, versions)
274
274
275 (prev_pull_request_latest,
275 (prev_pull_request_latest,
276 prev_pull_request_at_ver,
276 prev_pull_request_at_ver,
277 prev_pull_request_display_obj,
277 prev_pull_request_display_obj,
278 prev_at_version) = PullRequestModel().get_pr_version(
278 prev_at_version) = PullRequestModel().get_pr_version(
279 pull_request_id, version=from_version)
279 pull_request_id, version=from_version)
280
280
281 c.from_version = prev_at_version
281 c.from_version = prev_at_version
282 c.from_version_num = (prev_at_version
282 c.from_version_num = (prev_at_version
283 if prev_at_version and prev_at_version != 'latest'
283 if prev_at_version and prev_at_version != 'latest'
284 else None)
284 else None)
285 c.from_version_pos = ChangesetComment.get_index_from_version(
285 c.from_version_pos = ChangesetComment.get_index_from_version(
286 c.from_version_num, versions)
286 c.from_version_num, versions)
287
287
288 # define if we're in COMPARE mode or VIEW at version mode
288 # define if we're in COMPARE mode or VIEW at version mode
289 compare = at_version != prev_at_version
289 compare = at_version != prev_at_version
290
290
291 # pull_requests repo_name we opened it against
291 # pull_requests repo_name we opened it against
292 # ie. target_repo must match
292 # ie. target_repo must match
293 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
293 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
294 raise HTTPNotFound()
294 raise HTTPNotFound()
295
295
296 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
296 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
297 pull_request_at_ver)
297 pull_request_at_ver)
298
298
299 c.pull_request = pull_request_display_obj
299 c.pull_request = pull_request_display_obj
300 c.pull_request_latest = pull_request_latest
300 c.pull_request_latest = pull_request_latest
301
301
302 if compare or (at_version and not at_version == 'latest'):
302 if compare or (at_version and not at_version == 'latest'):
303 c.allowed_to_change_status = False
303 c.allowed_to_change_status = False
304 c.allowed_to_update = False
304 c.allowed_to_update = False
305 c.allowed_to_merge = False
305 c.allowed_to_merge = False
306 c.allowed_to_delete = False
306 c.allowed_to_delete = False
307 c.allowed_to_comment = False
307 c.allowed_to_comment = False
308 c.allowed_to_close = False
308 c.allowed_to_close = False
309 else:
309 else:
310 can_change_status = PullRequestModel().check_user_change_status(
310 can_change_status = PullRequestModel().check_user_change_status(
311 pull_request_at_ver, self._rhodecode_user)
311 pull_request_at_ver, self._rhodecode_user)
312 c.allowed_to_change_status = can_change_status and not pr_closed
312 c.allowed_to_change_status = can_change_status and not pr_closed
313
313
314 c.allowed_to_update = PullRequestModel().check_user_update(
314 c.allowed_to_update = PullRequestModel().check_user_update(
315 pull_request_latest, self._rhodecode_user) and not pr_closed
315 pull_request_latest, self._rhodecode_user) and not pr_closed
316 c.allowed_to_merge = PullRequestModel().check_user_merge(
316 c.allowed_to_merge = PullRequestModel().check_user_merge(
317 pull_request_latest, self._rhodecode_user) and not pr_closed
317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 c.allowed_to_delete = PullRequestModel().check_user_delete(
318 c.allowed_to_delete = PullRequestModel().check_user_delete(
319 pull_request_latest, self._rhodecode_user) and not pr_closed
319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 c.allowed_to_comment = not pr_closed
320 c.allowed_to_comment = not pr_closed
321 c.allowed_to_close = c.allowed_to_merge and not pr_closed
321 c.allowed_to_close = c.allowed_to_merge and not pr_closed
322
322
323 c.forbid_adding_reviewers = False
323 c.forbid_adding_reviewers = False
324 c.forbid_author_to_review = False
324 c.forbid_author_to_review = False
325 c.forbid_commit_author_to_review = False
325 c.forbid_commit_author_to_review = False
326
326
327 if pull_request_latest.reviewer_data and \
327 if pull_request_latest.reviewer_data and \
328 'rules' in pull_request_latest.reviewer_data:
328 'rules' in pull_request_latest.reviewer_data:
329 rules = pull_request_latest.reviewer_data['rules'] or {}
329 rules = pull_request_latest.reviewer_data['rules'] or {}
330 try:
330 try:
331 c.forbid_adding_reviewers = rules.get(
331 c.forbid_adding_reviewers = rules.get(
332 'forbid_adding_reviewers')
332 'forbid_adding_reviewers')
333 c.forbid_author_to_review = rules.get(
333 c.forbid_author_to_review = rules.get(
334 'forbid_author_to_review')
334 'forbid_author_to_review')
335 c.forbid_commit_author_to_review = rules.get(
335 c.forbid_commit_author_to_review = rules.get(
336 'forbid_commit_author_to_review')
336 'forbid_commit_author_to_review')
337 except Exception:
337 except Exception:
338 pass
338 pass
339
339
340 # check merge capabilities
340 # check merge capabilities
341 _merge_check = MergeCheck.validate(
341 _merge_check = MergeCheck.validate(
342 pull_request_latest, user=self._rhodecode_user,
342 pull_request_latest, user=self._rhodecode_user,
343 translator=self.request.translate,
343 translator=self.request.translate,
344 force_shadow_repo_refresh=force_refresh)
344 force_shadow_repo_refresh=force_refresh)
345 c.pr_merge_errors = _merge_check.error_details
345 c.pr_merge_errors = _merge_check.error_details
346 c.pr_merge_possible = not _merge_check.failed
346 c.pr_merge_possible = not _merge_check.failed
347 c.pr_merge_message = _merge_check.merge_msg
347 c.pr_merge_message = _merge_check.merge_msg
348
348
349 c.pr_merge_info = MergeCheck.get_merge_conditions(
349 c.pr_merge_info = MergeCheck.get_merge_conditions(
350 pull_request_latest, translator=self.request.translate)
350 pull_request_latest, translator=self.request.translate)
351
351
352 c.pull_request_review_status = _merge_check.review_status
352 c.pull_request_review_status = _merge_check.review_status
353 if merge_checks:
353 if merge_checks:
354 self.request.override_renderer = \
354 self.request.override_renderer = \
355 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
355 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
356 return self._get_template_context(c)
356 return self._get_template_context(c)
357
357
358 comments_model = CommentsModel()
358 comments_model = CommentsModel()
359
359
360 # reviewers and statuses
360 # reviewers and statuses
361 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
361 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
362 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
362 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
363
363
364 # GENERAL COMMENTS with versions #
364 # GENERAL COMMENTS with versions #
365 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
365 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
366 q = q.order_by(ChangesetComment.comment_id.asc())
366 q = q.order_by(ChangesetComment.comment_id.asc())
367 general_comments = q
367 general_comments = q
368
368
369 # pick comments we want to render at current version
369 # pick comments we want to render at current version
370 c.comment_versions = comments_model.aggregate_comments(
370 c.comment_versions = comments_model.aggregate_comments(
371 general_comments, versions, c.at_version_num)
371 general_comments, versions, c.at_version_num)
372 c.comments = c.comment_versions[c.at_version_num]['until']
372 c.comments = c.comment_versions[c.at_version_num]['until']
373
373
374 # INLINE COMMENTS with versions #
374 # INLINE COMMENTS with versions #
375 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
375 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
376 q = q.order_by(ChangesetComment.comment_id.asc())
376 q = q.order_by(ChangesetComment.comment_id.asc())
377 inline_comments = q
377 inline_comments = q
378
378
379 c.inline_versions = comments_model.aggregate_comments(
379 c.inline_versions = comments_model.aggregate_comments(
380 inline_comments, versions, c.at_version_num, inline=True)
380 inline_comments, versions, c.at_version_num, inline=True)
381
381
382 # inject latest version
382 # inject latest version
383 latest_ver = PullRequest.get_pr_display_object(
383 latest_ver = PullRequest.get_pr_display_object(
384 pull_request_latest, pull_request_latest)
384 pull_request_latest, pull_request_latest)
385
385
386 c.versions = versions + [latest_ver]
386 c.versions = versions + [latest_ver]
387
387
388 # if we use version, then do not show later comments
388 # if we use version, then do not show later comments
389 # than current version
389 # than current version
390 display_inline_comments = collections.defaultdict(
390 display_inline_comments = collections.defaultdict(
391 lambda: collections.defaultdict(list))
391 lambda: collections.defaultdict(list))
392 for co in inline_comments:
392 for co in inline_comments:
393 if c.at_version_num:
393 if c.at_version_num:
394 # pick comments that are at least UPTO given version, so we
394 # pick comments that are at least UPTO given version, so we
395 # don't render comments for higher version
395 # don't render comments for higher version
396 should_render = co.pull_request_version_id and \
396 should_render = co.pull_request_version_id and \
397 co.pull_request_version_id <= c.at_version_num
397 co.pull_request_version_id <= c.at_version_num
398 else:
398 else:
399 # showing all, for 'latest'
399 # showing all, for 'latest'
400 should_render = True
400 should_render = True
401
401
402 if should_render:
402 if should_render:
403 display_inline_comments[co.f_path][co.line_no].append(co)
403 display_inline_comments[co.f_path][co.line_no].append(co)
404
404
405 # load diff data into template context, if we use compare mode then
405 # load diff data into template context, if we use compare mode then
406 # diff is calculated based on changes between versions of PR
406 # diff is calculated based on changes between versions of PR
407
407
408 source_repo = pull_request_at_ver.source_repo
408 source_repo = pull_request_at_ver.source_repo
409 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
409 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
410
410
411 target_repo = pull_request_at_ver.target_repo
411 target_repo = pull_request_at_ver.target_repo
412 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
412 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
413
413
414 if compare:
414 if compare:
415 # in compare switch the diff base to latest commit from prev version
415 # in compare switch the diff base to latest commit from prev version
416 target_ref_id = prev_pull_request_display_obj.revisions[0]
416 target_ref_id = prev_pull_request_display_obj.revisions[0]
417
417
418 # despite opening commits for bookmarks/branches/tags, we always
418 # despite opening commits for bookmarks/branches/tags, we always
419 # convert this to rev to prevent changes after bookmark or branch change
419 # convert this to rev to prevent changes after bookmark or branch change
420 c.source_ref_type = 'rev'
420 c.source_ref_type = 'rev'
421 c.source_ref = source_ref_id
421 c.source_ref = source_ref_id
422
422
423 c.target_ref_type = 'rev'
423 c.target_ref_type = 'rev'
424 c.target_ref = target_ref_id
424 c.target_ref = target_ref_id
425
425
426 c.source_repo = source_repo
426 c.source_repo = source_repo
427 c.target_repo = target_repo
427 c.target_repo = target_repo
428
428
429 c.commit_ranges = []
429 c.commit_ranges = []
430 source_commit = EmptyCommit()
430 source_commit = EmptyCommit()
431 target_commit = EmptyCommit()
431 target_commit = EmptyCommit()
432 c.missing_requirements = False
432 c.missing_requirements = False
433
433
434 source_scm = source_repo.scm_instance()
434 source_scm = source_repo.scm_instance()
435 target_scm = target_repo.scm_instance()
435 target_scm = target_repo.scm_instance()
436
436
437 shadow_scm = None
437 shadow_scm = None
438 try:
438 try:
439 shadow_scm = pull_request_latest.get_shadow_repo()
439 shadow_scm = pull_request_latest.get_shadow_repo()
440 except Exception:
440 except Exception:
441 log.debug('Failed to get shadow repo', exc_info=True)
441 log.debug('Failed to get shadow repo', exc_info=True)
442 # try first the existing source_repo, and then shadow
442 # try first the existing source_repo, and then shadow
443 # repo if we can obtain one
443 # repo if we can obtain one
444 commits_source_repo = source_scm or shadow_scm
444 commits_source_repo = source_scm or shadow_scm
445
445
446 c.commits_source_repo = commits_source_repo
446 c.commits_source_repo = commits_source_repo
447 c.ancestor = None # set it to None, to hide it from PR view
447 c.ancestor = None # set it to None, to hide it from PR view
448
448
449 # empty version means latest, so we keep this to prevent
449 # empty version means latest, so we keep this to prevent
450 # double caching
450 # double caching
451 version_normalized = version or 'latest'
451 version_normalized = version or 'latest'
452 from_version_normalized = from_version or 'latest'
452 from_version_normalized = from_version or 'latest'
453
453
454 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
454 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
455 target_repo)
455 target_repo)
456 cache_file_path = diff_cache_exist(
456 cache_file_path = diff_cache_exist(
457 cache_path, 'pull_request', pull_request_id, version_normalized,
457 cache_path, 'pull_request', pull_request_id, version_normalized,
458 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
458 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
459
459
460 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
460 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
461 force_recache = str2bool(self.request.GET.get('force_recache'))
461 force_recache = str2bool(self.request.GET.get('force_recache'))
462
462
463 cached_diff = None
463 cached_diff = None
464 if caching_enabled:
464 if caching_enabled:
465 cached_diff = load_cached_diff(cache_file_path)
465 cached_diff = load_cached_diff(cache_file_path)
466
466
467 has_proper_commit_cache = (
467 has_proper_commit_cache = (
468 cached_diff and cached_diff.get('commits')
468 cached_diff and cached_diff.get('commits')
469 and len(cached_diff.get('commits', [])) == 5
469 and len(cached_diff.get('commits', [])) == 5
470 and cached_diff.get('commits')[0]
470 and cached_diff.get('commits')[0]
471 and cached_diff.get('commits')[3])
471 and cached_diff.get('commits')[3])
472 if not force_recache and has_proper_commit_cache:
472 if not force_recache and has_proper_commit_cache:
473 diff_commit_cache = \
473 diff_commit_cache = \
474 (ancestor_commit, commit_cache, missing_requirements,
474 (ancestor_commit, commit_cache, missing_requirements,
475 source_commit, target_commit) = cached_diff['commits']
475 source_commit, target_commit) = cached_diff['commits']
476 else:
476 else:
477 diff_commit_cache = \
477 diff_commit_cache = \
478 (ancestor_commit, commit_cache, missing_requirements,
478 (ancestor_commit, commit_cache, missing_requirements,
479 source_commit, target_commit) = self.get_commits(
479 source_commit, target_commit) = self.get_commits(
480 commits_source_repo,
480 commits_source_repo,
481 pull_request_at_ver,
481 pull_request_at_ver,
482 source_commit,
482 source_commit,
483 source_ref_id,
483 source_ref_id,
484 source_scm,
484 source_scm,
485 target_commit,
485 target_commit,
486 target_ref_id,
486 target_ref_id,
487 target_scm)
487 target_scm)
488
488
489 # register our commit range
489 # register our commit range
490 for comm in commit_cache.values():
490 for comm in commit_cache.values():
491 c.commit_ranges.append(comm)
491 c.commit_ranges.append(comm)
492
492
493 c.missing_requirements = missing_requirements
493 c.missing_requirements = missing_requirements
494 c.ancestor_commit = ancestor_commit
494 c.ancestor_commit = ancestor_commit
495 c.statuses = source_repo.statuses(
495 c.statuses = source_repo.statuses(
496 [x.raw_id for x in c.commit_ranges])
496 [x.raw_id for x in c.commit_ranges])
497
497
498 # auto collapse if we have more than limit
498 # auto collapse if we have more than limit
499 collapse_limit = diffs.DiffProcessor._collapse_commits_over
499 collapse_limit = diffs.DiffProcessor._collapse_commits_over
500 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
500 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
501 c.compare_mode = compare
501 c.compare_mode = compare
502
502
503 # diff_limit is the old behavior, will cut off the whole diff
503 # diff_limit is the old behavior, will cut off the whole diff
504 # if the limit is applied otherwise will just hide the
504 # if the limit is applied otherwise will just hide the
505 # big files from the front-end
505 # big files from the front-end
506 diff_limit = c.visual.cut_off_limit_diff
506 diff_limit = c.visual.cut_off_limit_diff
507 file_limit = c.visual.cut_off_limit_file
507 file_limit = c.visual.cut_off_limit_file
508
508
509 c.missing_commits = False
509 c.missing_commits = False
510 if (c.missing_requirements
510 if (c.missing_requirements
511 or isinstance(source_commit, EmptyCommit)
511 or isinstance(source_commit, EmptyCommit)
512 or source_commit == target_commit):
512 or source_commit == target_commit):
513
513
514 c.missing_commits = True
514 c.missing_commits = True
515 else:
515 else:
516 c.inline_comments = display_inline_comments
516 c.inline_comments = display_inline_comments
517
517
518 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
518 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
519 if not force_recache and has_proper_diff_cache:
519 if not force_recache and has_proper_diff_cache:
520 c.diffset = cached_diff['diff']
520 c.diffset = cached_diff['diff']
521 (ancestor_commit, commit_cache, missing_requirements,
521 (ancestor_commit, commit_cache, missing_requirements,
522 source_commit, target_commit) = cached_diff['commits']
522 source_commit, target_commit) = cached_diff['commits']
523 else:
523 else:
524 c.diffset = self._get_diffset(
524 c.diffset = self._get_diffset(
525 c.source_repo.repo_name, commits_source_repo,
525 c.source_repo.repo_name, commits_source_repo,
526 source_ref_id, target_ref_id,
526 source_ref_id, target_ref_id,
527 target_commit, source_commit,
527 target_commit, source_commit,
528 diff_limit, file_limit, c.fulldiff)
528 diff_limit, file_limit, c.fulldiff)
529
529
530 # save cached diff
530 # save cached diff
531 if caching_enabled:
531 if caching_enabled:
532 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
532 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
533
533
534 c.limited_diff = c.diffset.limited_diff
534 c.limited_diff = c.diffset.limited_diff
535
535
536 # calculate removed files that are bound to comments
536 # calculate removed files that are bound to comments
537 comment_deleted_files = [
537 comment_deleted_files = [
538 fname for fname in display_inline_comments
538 fname for fname in display_inline_comments
539 if fname not in c.diffset.file_stats]
539 if fname not in c.diffset.file_stats]
540
540
541 c.deleted_files_comments = collections.defaultdict(dict)
541 c.deleted_files_comments = collections.defaultdict(dict)
542 for fname, per_line_comments in display_inline_comments.items():
542 for fname, per_line_comments in display_inline_comments.items():
543 if fname in comment_deleted_files:
543 if fname in comment_deleted_files:
544 c.deleted_files_comments[fname]['stats'] = 0
544 c.deleted_files_comments[fname]['stats'] = 0
545 c.deleted_files_comments[fname]['comments'] = list()
545 c.deleted_files_comments[fname]['comments'] = list()
546 for lno, comments in per_line_comments.items():
546 for lno, comments in per_line_comments.items():
547 c.deleted_files_comments[fname]['comments'].extend(
547 c.deleted_files_comments[fname]['comments'].extend(
548 comments)
548 comments)
549
549
550 # this is a hack to properly display links, when creating PR, the
550 # this is a hack to properly display links, when creating PR, the
551 # compare view and others uses different notation, and
551 # compare view and others uses different notation, and
552 # compare_commits.mako renders links based on the target_repo.
552 # compare_commits.mako renders links based on the target_repo.
553 # We need to swap that here to generate it properly on the html side
553 # We need to swap that here to generate it properly on the html side
554 c.target_repo = c.source_repo
554 c.target_repo = c.source_repo
555
555
556 c.commit_statuses = ChangesetStatus.STATUSES
556 c.commit_statuses = ChangesetStatus.STATUSES
557
557
558 c.show_version_changes = not pr_closed
558 c.show_version_changes = not pr_closed
559 if c.show_version_changes:
559 if c.show_version_changes:
560 cur_obj = pull_request_at_ver
560 cur_obj = pull_request_at_ver
561 prev_obj = prev_pull_request_at_ver
561 prev_obj = prev_pull_request_at_ver
562
562
563 old_commit_ids = prev_obj.revisions
563 old_commit_ids = prev_obj.revisions
564 new_commit_ids = cur_obj.revisions
564 new_commit_ids = cur_obj.revisions
565 commit_changes = PullRequestModel()._calculate_commit_id_changes(
565 commit_changes = PullRequestModel()._calculate_commit_id_changes(
566 old_commit_ids, new_commit_ids)
566 old_commit_ids, new_commit_ids)
567 c.commit_changes_summary = commit_changes
567 c.commit_changes_summary = commit_changes
568
568
569 # calculate the diff for commits between versions
569 # calculate the diff for commits between versions
570 c.commit_changes = []
570 c.commit_changes = []
571 mark = lambda cs, fw: list(
571 mark = lambda cs, fw: list(
572 h.itertools.izip_longest([], cs, fillvalue=fw))
572 h.itertools.izip_longest([], cs, fillvalue=fw))
573 for c_type, raw_id in mark(commit_changes.added, 'a') \
573 for c_type, raw_id in mark(commit_changes.added, 'a') \
574 + mark(commit_changes.removed, 'r') \
574 + mark(commit_changes.removed, 'r') \
575 + mark(commit_changes.common, 'c'):
575 + mark(commit_changes.common, 'c'):
576
576
577 if raw_id in commit_cache:
577 if raw_id in commit_cache:
578 commit = commit_cache[raw_id]
578 commit = commit_cache[raw_id]
579 else:
579 else:
580 try:
580 try:
581 commit = commits_source_repo.get_commit(raw_id)
581 commit = commits_source_repo.get_commit(raw_id)
582 except CommitDoesNotExistError:
582 except CommitDoesNotExistError:
583 # in case we fail extracting still use "dummy" commit
583 # in case we fail extracting still use "dummy" commit
584 # for display in commit diff
584 # for display in commit diff
585 commit = h.AttributeDict(
585 commit = h.AttributeDict(
586 {'raw_id': raw_id,
586 {'raw_id': raw_id,
587 'message': 'EMPTY or MISSING COMMIT'})
587 'message': 'EMPTY or MISSING COMMIT'})
588 c.commit_changes.append([c_type, commit])
588 c.commit_changes.append([c_type, commit])
589
589
590 # current user review statuses for each version
590 # current user review statuses for each version
591 c.review_versions = {}
591 c.review_versions = {}
592 if self._rhodecode_user.user_id in allowed_reviewers:
592 if self._rhodecode_user.user_id in allowed_reviewers:
593 for co in general_comments:
593 for co in general_comments:
594 if co.author.user_id == self._rhodecode_user.user_id:
594 if co.author.user_id == self._rhodecode_user.user_id:
595 status = co.status_change
595 status = co.status_change
596 if status:
596 if status:
597 _ver_pr = status[0].comment.pull_request_version_id
597 _ver_pr = status[0].comment.pull_request_version_id
598 c.review_versions[_ver_pr] = status[0]
598 c.review_versions[_ver_pr] = status[0]
599
599
600 return self._get_template_context(c)
600 return self._get_template_context(c)
601
601
602 def get_commits(
602 def get_commits(
603 self, commits_source_repo, pull_request_at_ver, source_commit,
603 self, commits_source_repo, pull_request_at_ver, source_commit,
604 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
604 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
605 commit_cache = collections.OrderedDict()
605 commit_cache = collections.OrderedDict()
606 missing_requirements = False
606 missing_requirements = False
607 try:
607 try:
608 pre_load = ["author", "branch", "date", "message"]
608 pre_load = ["author", "branch", "date", "message"]
609 show_revs = pull_request_at_ver.revisions
609 show_revs = pull_request_at_ver.revisions
610 for rev in show_revs:
610 for rev in show_revs:
611 comm = commits_source_repo.get_commit(
611 comm = commits_source_repo.get_commit(
612 commit_id=rev, pre_load=pre_load)
612 commit_id=rev, pre_load=pre_load)
613 commit_cache[comm.raw_id] = comm
613 commit_cache[comm.raw_id] = comm
614
614
615 # Order here matters, we first need to get target, and then
615 # Order here matters, we first need to get target, and then
616 # the source
616 # the source
617 target_commit = commits_source_repo.get_commit(
617 target_commit = commits_source_repo.get_commit(
618 commit_id=safe_str(target_ref_id))
618 commit_id=safe_str(target_ref_id))
619
619
620 source_commit = commits_source_repo.get_commit(
620 source_commit = commits_source_repo.get_commit(
621 commit_id=safe_str(source_ref_id))
621 commit_id=safe_str(source_ref_id))
622 except CommitDoesNotExistError:
622 except CommitDoesNotExistError:
623 log.warning(
623 log.warning(
624 'Failed to get commit from `{}` repo'.format(
624 'Failed to get commit from `{}` repo'.format(
625 commits_source_repo), exc_info=True)
625 commits_source_repo), exc_info=True)
626 except RepositoryRequirementError:
626 except RepositoryRequirementError:
627 log.warning(
627 log.warning(
628 'Failed to get all required data from repo', exc_info=True)
628 'Failed to get all required data from repo', exc_info=True)
629 missing_requirements = True
629 missing_requirements = True
630 ancestor_commit = None
630 ancestor_commit = None
631 try:
631 try:
632 ancestor_id = source_scm.get_common_ancestor(
632 ancestor_id = source_scm.get_common_ancestor(
633 source_commit.raw_id, target_commit.raw_id, target_scm)
633 source_commit.raw_id, target_commit.raw_id, target_scm)
634 ancestor_commit = source_scm.get_commit(ancestor_id)
634 ancestor_commit = source_scm.get_commit(ancestor_id)
635 except Exception:
635 except Exception:
636 ancestor_commit = None
636 ancestor_commit = None
637 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
637 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
638
638
639 def assure_not_empty_repo(self):
639 def assure_not_empty_repo(self):
640 _ = self.request.translate
640 _ = self.request.translate
641
641
642 try:
642 try:
643 self.db_repo.scm_instance().get_commit()
643 self.db_repo.scm_instance().get_commit()
644 except EmptyRepositoryError:
644 except EmptyRepositoryError:
645 h.flash(h.literal(_('There are no commits yet')),
645 h.flash(h.literal(_('There are no commits yet')),
646 category='warning')
646 category='warning')
647 raise HTTPFound(
647 raise HTTPFound(
648 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
648 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
649
649
650 @LoginRequired()
650 @LoginRequired()
651 @NotAnonymous()
651 @NotAnonymous()
652 @HasRepoPermissionAnyDecorator(
652 @HasRepoPermissionAnyDecorator(
653 'repository.read', 'repository.write', 'repository.admin')
653 'repository.read', 'repository.write', 'repository.admin')
654 @view_config(
654 @view_config(
655 route_name='pullrequest_new', request_method='GET',
655 route_name='pullrequest_new', request_method='GET',
656 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
656 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
657 def pull_request_new(self):
657 def pull_request_new(self):
658 _ = self.request.translate
658 _ = self.request.translate
659 c = self.load_default_context()
659 c = self.load_default_context()
660
660
661 self.assure_not_empty_repo()
661 self.assure_not_empty_repo()
662 source_repo = self.db_repo
662 source_repo = self.db_repo
663
663
664 commit_id = self.request.GET.get('commit')
664 commit_id = self.request.GET.get('commit')
665 branch_ref = self.request.GET.get('branch')
665 branch_ref = self.request.GET.get('branch')
666 bookmark_ref = self.request.GET.get('bookmark')
666 bookmark_ref = self.request.GET.get('bookmark')
667
667
668 try:
668 try:
669 source_repo_data = PullRequestModel().generate_repo_data(
669 source_repo_data = PullRequestModel().generate_repo_data(
670 source_repo, commit_id=commit_id,
670 source_repo, commit_id=commit_id,
671 branch=branch_ref, bookmark=bookmark_ref,
671 branch=branch_ref, bookmark=bookmark_ref,
672 translator=self.request.translate)
672 translator=self.request.translate)
673 except CommitDoesNotExistError as e:
673 except CommitDoesNotExistError as e:
674 log.exception(e)
674 log.exception(e)
675 h.flash(_('Commit does not exist'), 'error')
675 h.flash(_('Commit does not exist'), 'error')
676 raise HTTPFound(
676 raise HTTPFound(
677 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
677 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
678
678
679 default_target_repo = source_repo
679 default_target_repo = source_repo
680
680
681 if source_repo.parent:
681 if source_repo.parent:
682 parent_vcs_obj = source_repo.parent.scm_instance()
682 parent_vcs_obj = source_repo.parent.scm_instance()
683 if parent_vcs_obj and not parent_vcs_obj.is_empty():
683 if parent_vcs_obj and not parent_vcs_obj.is_empty():
684 # change default if we have a parent repo
684 # change default if we have a parent repo
685 default_target_repo = source_repo.parent
685 default_target_repo = source_repo.parent
686
686
687 target_repo_data = PullRequestModel().generate_repo_data(
687 target_repo_data = PullRequestModel().generate_repo_data(
688 default_target_repo, translator=self.request.translate)
688 default_target_repo, translator=self.request.translate)
689
689
690 selected_source_ref = source_repo_data['refs']['selected_ref']
690 selected_source_ref = source_repo_data['refs']['selected_ref']
691 title_source_ref = ''
691 title_source_ref = ''
692 if selected_source_ref:
692 if selected_source_ref:
693 title_source_ref = selected_source_ref.split(':', 2)[1]
693 title_source_ref = selected_source_ref.split(':', 2)[1]
694 c.default_title = PullRequestModel().generate_pullrequest_title(
694 c.default_title = PullRequestModel().generate_pullrequest_title(
695 source=source_repo.repo_name,
695 source=source_repo.repo_name,
696 source_ref=title_source_ref,
696 source_ref=title_source_ref,
697 target=default_target_repo.repo_name
697 target=default_target_repo.repo_name
698 )
698 )
699
699
700 c.default_repo_data = {
700 c.default_repo_data = {
701 'source_repo_name': source_repo.repo_name,
701 'source_repo_name': source_repo.repo_name,
702 'source_refs_json': json.dumps(source_repo_data),
702 'source_refs_json': json.dumps(source_repo_data),
703 'target_repo_name': default_target_repo.repo_name,
703 'target_repo_name': default_target_repo.repo_name,
704 'target_refs_json': json.dumps(target_repo_data),
704 'target_refs_json': json.dumps(target_repo_data),
705 }
705 }
706 c.default_source_ref = selected_source_ref
706 c.default_source_ref = selected_source_ref
707
707
708 return self._get_template_context(c)
708 return self._get_template_context(c)
709
709
710 @LoginRequired()
710 @LoginRequired()
711 @NotAnonymous()
711 @NotAnonymous()
712 @HasRepoPermissionAnyDecorator(
712 @HasRepoPermissionAnyDecorator(
713 'repository.read', 'repository.write', 'repository.admin')
713 'repository.read', 'repository.write', 'repository.admin')
714 @view_config(
714 @view_config(
715 route_name='pullrequest_repo_refs', request_method='GET',
715 route_name='pullrequest_repo_refs', request_method='GET',
716 renderer='json_ext', xhr=True)
716 renderer='json_ext', xhr=True)
717 def pull_request_repo_refs(self):
717 def pull_request_repo_refs(self):
718 self.load_default_context()
718 self.load_default_context()
719 target_repo_name = self.request.matchdict['target_repo_name']
719 target_repo_name = self.request.matchdict['target_repo_name']
720 repo = Repository.get_by_repo_name(target_repo_name)
720 repo = Repository.get_by_repo_name(target_repo_name)
721 if not repo:
721 if not repo:
722 raise HTTPNotFound()
722 raise HTTPNotFound()
723
723
724 target_perm = HasRepoPermissionAny(
724 target_perm = HasRepoPermissionAny(
725 'repository.read', 'repository.write', 'repository.admin')(
725 'repository.read', 'repository.write', 'repository.admin')(
726 target_repo_name)
726 target_repo_name)
727 if not target_perm:
727 if not target_perm:
728 raise HTTPNotFound()
728 raise HTTPNotFound()
729
729
730 return PullRequestModel().generate_repo_data(
730 return PullRequestModel().generate_repo_data(
731 repo, translator=self.request.translate)
731 repo, translator=self.request.translate)
732
732
733 @LoginRequired()
733 @LoginRequired()
734 @NotAnonymous()
734 @NotAnonymous()
735 @HasRepoPermissionAnyDecorator(
735 @HasRepoPermissionAnyDecorator(
736 'repository.read', 'repository.write', 'repository.admin')
736 'repository.read', 'repository.write', 'repository.admin')
737 @view_config(
737 @view_config(
738 route_name='pullrequest_repo_destinations', request_method='GET',
738 route_name='pullrequest_repo_destinations', request_method='GET',
739 renderer='json_ext', xhr=True)
739 renderer='json_ext', xhr=True)
740 def pull_request_repo_destinations(self):
740 def pull_request_repo_destinations(self):
741 _ = self.request.translate
741 _ = self.request.translate
742 filter_query = self.request.GET.get('query')
742 filter_query = self.request.GET.get('query')
743
743
744 query = Repository.query() \
744 query = Repository.query() \
745 .order_by(func.length(Repository.repo_name)) \
745 .order_by(func.length(Repository.repo_name)) \
746 .filter(
746 .filter(
747 or_(Repository.repo_name == self.db_repo.repo_name,
747 or_(Repository.repo_name == self.db_repo.repo_name,
748 Repository.fork_id == self.db_repo.repo_id))
748 Repository.fork_id == self.db_repo.repo_id))
749
749
750 if filter_query:
750 if filter_query:
751 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
751 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
752 query = query.filter(
752 query = query.filter(
753 Repository.repo_name.ilike(ilike_expression))
753 Repository.repo_name.ilike(ilike_expression))
754
754
755 add_parent = False
755 add_parent = False
756 if self.db_repo.parent:
756 if self.db_repo.parent:
757 if filter_query in self.db_repo.parent.repo_name:
757 if filter_query in self.db_repo.parent.repo_name:
758 parent_vcs_obj = self.db_repo.parent.scm_instance()
758 parent_vcs_obj = self.db_repo.parent.scm_instance()
759 if parent_vcs_obj and not parent_vcs_obj.is_empty():
759 if parent_vcs_obj and not parent_vcs_obj.is_empty():
760 add_parent = True
760 add_parent = True
761
761
762 limit = 20 - 1 if add_parent else 20
762 limit = 20 - 1 if add_parent else 20
763 all_repos = query.limit(limit).all()
763 all_repos = query.limit(limit).all()
764 if add_parent:
764 if add_parent:
765 all_repos += [self.db_repo.parent]
765 all_repos += [self.db_repo.parent]
766
766
767 repos = []
767 repos = []
768 for obj in ScmModel().get_repos(all_repos):
768 for obj in ScmModel().get_repos(all_repos):
769 repos.append({
769 repos.append({
770 'id': obj['name'],
770 'id': obj['name'],
771 'text': obj['name'],
771 'text': obj['name'],
772 'type': 'repo',
772 'type': 'repo',
773 'repo_id': obj['dbrepo']['repo_id'],
773 'repo_id': obj['dbrepo']['repo_id'],
774 'repo_type': obj['dbrepo']['repo_type'],
774 'repo_type': obj['dbrepo']['repo_type'],
775 'private': obj['dbrepo']['private'],
775 'private': obj['dbrepo']['private'],
776
776
777 })
777 })
778
778
779 data = {
779 data = {
780 'more': False,
780 'more': False,
781 'results': [{
781 'results': [{
782 'text': _('Repositories'),
782 'text': _('Repositories'),
783 'children': repos
783 'children': repos
784 }] if repos else []
784 }] if repos else []
785 }
785 }
786 return data
786 return data
787
787
788 @LoginRequired()
788 @LoginRequired()
789 @NotAnonymous()
789 @NotAnonymous()
790 @HasRepoPermissionAnyDecorator(
790 @HasRepoPermissionAnyDecorator(
791 'repository.read', 'repository.write', 'repository.admin')
791 'repository.read', 'repository.write', 'repository.admin')
792 @CSRFRequired()
792 @CSRFRequired()
793 @view_config(
793 @view_config(
794 route_name='pullrequest_create', request_method='POST',
794 route_name='pullrequest_create', request_method='POST',
795 renderer=None)
795 renderer=None)
796 def pull_request_create(self):
796 def pull_request_create(self):
797 _ = self.request.translate
797 _ = self.request.translate
798 self.assure_not_empty_repo()
798 self.assure_not_empty_repo()
799 self.load_default_context()
799 self.load_default_context()
800
800
801 controls = peppercorn.parse(self.request.POST.items())
801 controls = peppercorn.parse(self.request.POST.items())
802
802
803 try:
803 try:
804 form = PullRequestForm(
804 form = PullRequestForm(
805 self.request.translate, self.db_repo.repo_id)()
805 self.request.translate, self.db_repo.repo_id)()
806 _form = form.to_python(controls)
806 _form = form.to_python(controls)
807 except formencode.Invalid as errors:
807 except formencode.Invalid as errors:
808 if errors.error_dict.get('revisions'):
808 if errors.error_dict.get('revisions'):
809 msg = 'Revisions: %s' % errors.error_dict['revisions']
809 msg = 'Revisions: %s' % errors.error_dict['revisions']
810 elif errors.error_dict.get('pullrequest_title'):
810 elif errors.error_dict.get('pullrequest_title'):
811 msg = errors.error_dict.get('pullrequest_title')
811 msg = errors.error_dict.get('pullrequest_title')
812 else:
812 else:
813 msg = _('Error creating pull request: {}').format(errors)
813 msg = _('Error creating pull request: {}').format(errors)
814 log.exception(msg)
814 log.exception(msg)
815 h.flash(msg, 'error')
815 h.flash(msg, 'error')
816
816
817 # would rather just go back to form ...
817 # would rather just go back to form ...
818 raise HTTPFound(
818 raise HTTPFound(
819 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
819 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
820
820
821 source_repo = _form['source_repo']
821 source_repo = _form['source_repo']
822 source_ref = _form['source_ref']
822 source_ref = _form['source_ref']
823 target_repo = _form['target_repo']
823 target_repo = _form['target_repo']
824 target_ref = _form['target_ref']
824 target_ref = _form['target_ref']
825 commit_ids = _form['revisions'][::-1]
825 commit_ids = _form['revisions'][::-1]
826
826
827 # find the ancestor for this pr
827 # find the ancestor for this pr
828 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
828 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
829 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
829 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
830
830
831 # re-check permissions again here
831 # re-check permissions again here
832 # source_repo we must have read permissions
832 # source_repo we must have read permissions
833
833
834 source_perm = HasRepoPermissionAny(
834 source_perm = HasRepoPermissionAny(
835 'repository.read',
835 'repository.read',
836 'repository.write', 'repository.admin')(source_db_repo.repo_name)
836 'repository.write', 'repository.admin')(source_db_repo.repo_name)
837 if not source_perm:
837 if not source_perm:
838 msg = _('Not Enough permissions to source repo `{}`.'.format(
838 msg = _('Not Enough permissions to source repo `{}`.'.format(
839 source_db_repo.repo_name))
839 source_db_repo.repo_name))
840 h.flash(msg, category='error')
840 h.flash(msg, category='error')
841 # copy the args back to redirect
841 # copy the args back to redirect
842 org_query = self.request.GET.mixed()
842 org_query = self.request.GET.mixed()
843 raise HTTPFound(
843 raise HTTPFound(
844 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
844 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
845 _query=org_query))
845 _query=org_query))
846
846
847 # target repo we must have read permissions, and also later on
847 # target repo we must have read permissions, and also later on
848 # we want to check branch permissions here
848 # we want to check branch permissions here
849 target_perm = HasRepoPermissionAny(
849 target_perm = HasRepoPermissionAny(
850 'repository.read',
850 'repository.read',
851 'repository.write', 'repository.admin')(target_db_repo.repo_name)
851 'repository.write', 'repository.admin')(target_db_repo.repo_name)
852 if not target_perm:
852 if not target_perm:
853 msg = _('Not Enough permissions to target repo `{}`.'.format(
853 msg = _('Not Enough permissions to target repo `{}`.'.format(
854 target_db_repo.repo_name))
854 target_db_repo.repo_name))
855 h.flash(msg, category='error')
855 h.flash(msg, category='error')
856 # copy the args back to redirect
856 # copy the args back to redirect
857 org_query = self.request.GET.mixed()
857 org_query = self.request.GET.mixed()
858 raise HTTPFound(
858 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
860 _query=org_query))
860 _query=org_query))
861
861
862 source_scm = source_db_repo.scm_instance()
862 source_scm = source_db_repo.scm_instance()
863 target_scm = target_db_repo.scm_instance()
863 target_scm = target_db_repo.scm_instance()
864
864
865 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
865 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
866 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
866 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
867
867
868 ancestor = source_scm.get_common_ancestor(
868 ancestor = source_scm.get_common_ancestor(
869 source_commit.raw_id, target_commit.raw_id, target_scm)
869 source_commit.raw_id, target_commit.raw_id, target_scm)
870
870
871 # recalculate target ref based on ancestor
871 # recalculate target ref based on ancestor
872 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
872 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
873 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
873 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
874
874
875 get_default_reviewers_data, validate_default_reviewers = \
875 get_default_reviewers_data, validate_default_reviewers = \
876 PullRequestModel().get_reviewer_functions()
876 PullRequestModel().get_reviewer_functions()
877
877
878 # recalculate reviewers logic, to make sure we can validate this
878 # recalculate reviewers logic, to make sure we can validate this
879 reviewer_rules = get_default_reviewers_data(
879 reviewer_rules = get_default_reviewers_data(
880 self._rhodecode_db_user, source_db_repo,
880 self._rhodecode_db_user, source_db_repo,
881 source_commit, target_db_repo, target_commit)
881 source_commit, target_db_repo, target_commit)
882
882
883 given_reviewers = _form['review_members']
883 given_reviewers = _form['review_members']
884 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
884 reviewers = validate_default_reviewers(
885 given_reviewers, reviewer_rules)
885
886
886 pullrequest_title = _form['pullrequest_title']
887 pullrequest_title = _form['pullrequest_title']
887 title_source_ref = source_ref.split(':', 2)[1]
888 title_source_ref = source_ref.split(':', 2)[1]
888 if not pullrequest_title:
889 if not pullrequest_title:
889 pullrequest_title = PullRequestModel().generate_pullrequest_title(
890 pullrequest_title = PullRequestModel().generate_pullrequest_title(
890 source=source_repo,
891 source=source_repo,
891 source_ref=title_source_ref,
892 source_ref=title_source_ref,
892 target=target_repo
893 target=target_repo
893 )
894 )
894
895
895 description = _form['pullrequest_desc']
896 description = _form['pullrequest_desc']
896
897
897 try:
898 try:
898 pull_request = PullRequestModel().create(
899 pull_request = PullRequestModel().create(
899 created_by=self._rhodecode_user.user_id,
900 created_by=self._rhodecode_user.user_id,
900 source_repo=source_repo,
901 source_repo=source_repo,
901 source_ref=source_ref,
902 source_ref=source_ref,
902 target_repo=target_repo,
903 target_repo=target_repo,
903 target_ref=target_ref,
904 target_ref=target_ref,
904 revisions=commit_ids,
905 revisions=commit_ids,
905 reviewers=reviewers,
906 reviewers=reviewers,
906 title=pullrequest_title,
907 title=pullrequest_title,
907 description=description,
908 description=description,
908 reviewer_data=reviewer_rules,
909 reviewer_data=reviewer_rules,
909 auth_user=self._rhodecode_user
910 auth_user=self._rhodecode_user
910 )
911 )
911 Session().commit()
912 Session().commit()
912
913
913 h.flash(_('Successfully opened new pull request'),
914 h.flash(_('Successfully opened new pull request'),
914 category='success')
915 category='success')
915 except Exception:
916 except Exception:
916 msg = _('Error occurred during creation of this pull request.')
917 msg = _('Error occurred during creation of this pull request.')
917 log.exception(msg)
918 log.exception(msg)
918 h.flash(msg, category='error')
919 h.flash(msg, category='error')
919
920
920 # copy the args back to redirect
921 # copy the args back to redirect
921 org_query = self.request.GET.mixed()
922 org_query = self.request.GET.mixed()
922 raise HTTPFound(
923 raise HTTPFound(
923 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
924 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
924 _query=org_query))
925 _query=org_query))
925
926
926 raise HTTPFound(
927 raise HTTPFound(
927 h.route_path('pullrequest_show', repo_name=target_repo,
928 h.route_path('pullrequest_show', repo_name=target_repo,
928 pull_request_id=pull_request.pull_request_id))
929 pull_request_id=pull_request.pull_request_id))
929
930
930 @LoginRequired()
931 @LoginRequired()
931 @NotAnonymous()
932 @NotAnonymous()
932 @HasRepoPermissionAnyDecorator(
933 @HasRepoPermissionAnyDecorator(
933 'repository.read', 'repository.write', 'repository.admin')
934 'repository.read', 'repository.write', 'repository.admin')
934 @CSRFRequired()
935 @CSRFRequired()
935 @view_config(
936 @view_config(
936 route_name='pullrequest_update', request_method='POST',
937 route_name='pullrequest_update', request_method='POST',
937 renderer='json_ext')
938 renderer='json_ext')
938 def pull_request_update(self):
939 def pull_request_update(self):
939 pull_request = PullRequest.get_or_404(
940 pull_request = PullRequest.get_or_404(
940 self.request.matchdict['pull_request_id'])
941 self.request.matchdict['pull_request_id'])
941 _ = self.request.translate
942 _ = self.request.translate
942
943
943 self.load_default_context()
944 self.load_default_context()
944
945
945 if pull_request.is_closed():
946 if pull_request.is_closed():
946 log.debug('update: forbidden because pull request is closed')
947 log.debug('update: forbidden because pull request is closed')
947 msg = _(u'Cannot update closed pull requests.')
948 msg = _(u'Cannot update closed pull requests.')
948 h.flash(msg, category='error')
949 h.flash(msg, category='error')
949 return True
950 return True
950
951
951 # only owner or admin can update it
952 # only owner or admin can update it
952 allowed_to_update = PullRequestModel().check_user_update(
953 allowed_to_update = PullRequestModel().check_user_update(
953 pull_request, self._rhodecode_user)
954 pull_request, self._rhodecode_user)
954 if allowed_to_update:
955 if allowed_to_update:
955 controls = peppercorn.parse(self.request.POST.items())
956 controls = peppercorn.parse(self.request.POST.items())
956
957
957 if 'review_members' in controls:
958 if 'review_members' in controls:
958 self._update_reviewers(
959 self._update_reviewers(
959 pull_request, controls['review_members'],
960 pull_request, controls['review_members'],
960 pull_request.reviewer_data)
961 pull_request.reviewer_data)
961 elif str2bool(self.request.POST.get('update_commits', 'false')):
962 elif str2bool(self.request.POST.get('update_commits', 'false')):
962 self._update_commits(pull_request)
963 self._update_commits(pull_request)
963 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
964 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
964 self._edit_pull_request(pull_request)
965 self._edit_pull_request(pull_request)
965 else:
966 else:
966 raise HTTPBadRequest()
967 raise HTTPBadRequest()
967 return True
968 return True
968 raise HTTPForbidden()
969 raise HTTPForbidden()
969
970
970 def _edit_pull_request(self, pull_request):
971 def _edit_pull_request(self, pull_request):
971 _ = self.request.translate
972 _ = self.request.translate
972 try:
973 try:
973 PullRequestModel().edit(
974 PullRequestModel().edit(
974 pull_request, self.request.POST.get('title'),
975 pull_request, self.request.POST.get('title'),
975 self.request.POST.get('description'), self._rhodecode_user)
976 self.request.POST.get('description'), self._rhodecode_user)
976 except ValueError:
977 except ValueError:
977 msg = _(u'Cannot update closed pull requests.')
978 msg = _(u'Cannot update closed pull requests.')
978 h.flash(msg, category='error')
979 h.flash(msg, category='error')
979 return
980 return
980 else:
981 else:
981 Session().commit()
982 Session().commit()
982
983
983 msg = _(u'Pull request title & description updated.')
984 msg = _(u'Pull request title & description updated.')
984 h.flash(msg, category='success')
985 h.flash(msg, category='success')
985 return
986 return
986
987
987 def _update_commits(self, pull_request):
988 def _update_commits(self, pull_request):
988 _ = self.request.translate
989 _ = self.request.translate
989 resp = PullRequestModel().update_commits(pull_request)
990 resp = PullRequestModel().update_commits(pull_request)
990
991
991 if resp.executed:
992 if resp.executed:
992
993
993 if resp.target_changed and resp.source_changed:
994 if resp.target_changed and resp.source_changed:
994 changed = 'target and source repositories'
995 changed = 'target and source repositories'
995 elif resp.target_changed and not resp.source_changed:
996 elif resp.target_changed and not resp.source_changed:
996 changed = 'target repository'
997 changed = 'target repository'
997 elif not resp.target_changed and resp.source_changed:
998 elif not resp.target_changed and resp.source_changed:
998 changed = 'source repository'
999 changed = 'source repository'
999 else:
1000 else:
1000 changed = 'nothing'
1001 changed = 'nothing'
1001
1002
1002 msg = _(
1003 msg = _(
1003 u'Pull request updated to "{source_commit_id}" with '
1004 u'Pull request updated to "{source_commit_id}" with '
1004 u'{count_added} added, {count_removed} removed commits. '
1005 u'{count_added} added, {count_removed} removed commits. '
1005 u'Source of changes: {change_source}')
1006 u'Source of changes: {change_source}')
1006 msg = msg.format(
1007 msg = msg.format(
1007 source_commit_id=pull_request.source_ref_parts.commit_id,
1008 source_commit_id=pull_request.source_ref_parts.commit_id,
1008 count_added=len(resp.changes.added),
1009 count_added=len(resp.changes.added),
1009 count_removed=len(resp.changes.removed),
1010 count_removed=len(resp.changes.removed),
1010 change_source=changed)
1011 change_source=changed)
1011 h.flash(msg, category='success')
1012 h.flash(msg, category='success')
1012
1013
1013 channel = '/repo${}$/pr/{}'.format(
1014 channel = '/repo${}$/pr/{}'.format(
1014 pull_request.target_repo.repo_name,
1015 pull_request.target_repo.repo_name,
1015 pull_request.pull_request_id)
1016 pull_request.pull_request_id)
1016 message = msg + (
1017 message = msg + (
1017 ' - <a onclick="window.location.reload()">'
1018 ' - <a onclick="window.location.reload()">'
1018 '<strong>{}</strong></a>'.format(_('Reload page')))
1019 '<strong>{}</strong></a>'.format(_('Reload page')))
1019 channelstream.post_message(
1020 channelstream.post_message(
1020 channel, message, self._rhodecode_user.username,
1021 channel, message, self._rhodecode_user.username,
1021 registry=self.request.registry)
1022 registry=self.request.registry)
1022 else:
1023 else:
1023 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1024 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1024 warning_reasons = [
1025 warning_reasons = [
1025 UpdateFailureReason.NO_CHANGE,
1026 UpdateFailureReason.NO_CHANGE,
1026 UpdateFailureReason.WRONG_REF_TYPE,
1027 UpdateFailureReason.WRONG_REF_TYPE,
1027 ]
1028 ]
1028 category = 'warning' if resp.reason in warning_reasons else 'error'
1029 category = 'warning' if resp.reason in warning_reasons else 'error'
1029 h.flash(msg, category=category)
1030 h.flash(msg, category=category)
1030
1031
1031 @LoginRequired()
1032 @LoginRequired()
1032 @NotAnonymous()
1033 @NotAnonymous()
1033 @HasRepoPermissionAnyDecorator(
1034 @HasRepoPermissionAnyDecorator(
1034 'repository.read', 'repository.write', 'repository.admin')
1035 'repository.read', 'repository.write', 'repository.admin')
1035 @CSRFRequired()
1036 @CSRFRequired()
1036 @view_config(
1037 @view_config(
1037 route_name='pullrequest_merge', request_method='POST',
1038 route_name='pullrequest_merge', request_method='POST',
1038 renderer='json_ext')
1039 renderer='json_ext')
1039 def pull_request_merge(self):
1040 def pull_request_merge(self):
1040 """
1041 """
1041 Merge will perform a server-side merge of the specified
1042 Merge will perform a server-side merge of the specified
1042 pull request, if the pull request is approved and mergeable.
1043 pull request, if the pull request is approved and mergeable.
1043 After successful merging, the pull request is automatically
1044 After successful merging, the pull request is automatically
1044 closed, with a relevant comment.
1045 closed, with a relevant comment.
1045 """
1046 """
1046 pull_request = PullRequest.get_or_404(
1047 pull_request = PullRequest.get_or_404(
1047 self.request.matchdict['pull_request_id'])
1048 self.request.matchdict['pull_request_id'])
1048
1049
1049 self.load_default_context()
1050 self.load_default_context()
1050 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1051 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1051 translator=self.request.translate)
1052 translator=self.request.translate)
1052 merge_possible = not check.failed
1053 merge_possible = not check.failed
1053
1054
1054 for err_type, error_msg in check.errors:
1055 for err_type, error_msg in check.errors:
1055 h.flash(error_msg, category=err_type)
1056 h.flash(error_msg, category=err_type)
1056
1057
1057 if merge_possible:
1058 if merge_possible:
1058 log.debug("Pre-conditions checked, trying to merge.")
1059 log.debug("Pre-conditions checked, trying to merge.")
1059 extras = vcs_operation_context(
1060 extras = vcs_operation_context(
1060 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1061 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1061 username=self._rhodecode_db_user.username, action='push',
1062 username=self._rhodecode_db_user.username, action='push',
1062 scm=pull_request.target_repo.repo_type)
1063 scm=pull_request.target_repo.repo_type)
1063 self._merge_pull_request(
1064 self._merge_pull_request(
1064 pull_request, self._rhodecode_db_user, extras)
1065 pull_request, self._rhodecode_db_user, extras)
1065 else:
1066 else:
1066 log.debug("Pre-conditions failed, NOT merging.")
1067 log.debug("Pre-conditions failed, NOT merging.")
1067
1068
1068 raise HTTPFound(
1069 raise HTTPFound(
1069 h.route_path('pullrequest_show',
1070 h.route_path('pullrequest_show',
1070 repo_name=pull_request.target_repo.repo_name,
1071 repo_name=pull_request.target_repo.repo_name,
1071 pull_request_id=pull_request.pull_request_id))
1072 pull_request_id=pull_request.pull_request_id))
1072
1073
1073 def _merge_pull_request(self, pull_request, user, extras):
1074 def _merge_pull_request(self, pull_request, user, extras):
1074 _ = self.request.translate
1075 _ = self.request.translate
1075 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1076 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1076
1077
1077 if merge_resp.executed:
1078 if merge_resp.executed:
1078 log.debug("The merge was successful, closing the pull request.")
1079 log.debug("The merge was successful, closing the pull request.")
1079 PullRequestModel().close_pull_request(
1080 PullRequestModel().close_pull_request(
1080 pull_request.pull_request_id, user)
1081 pull_request.pull_request_id, user)
1081 Session().commit()
1082 Session().commit()
1082 msg = _('Pull request was successfully merged and closed.')
1083 msg = _('Pull request was successfully merged and closed.')
1083 h.flash(msg, category='success')
1084 h.flash(msg, category='success')
1084 else:
1085 else:
1085 log.debug(
1086 log.debug(
1086 "The merge was not successful. Merge response: %s",
1087 "The merge was not successful. Merge response: %s",
1087 merge_resp)
1088 merge_resp)
1088 msg = PullRequestModel().merge_status_message(
1089 msg = PullRequestModel().merge_status_message(
1089 merge_resp.failure_reason)
1090 merge_resp.failure_reason)
1090 h.flash(msg, category='error')
1091 h.flash(msg, category='error')
1091
1092
1092 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1093 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1093 _ = self.request.translate
1094 _ = self.request.translate
1094 get_default_reviewers_data, validate_default_reviewers = \
1095 get_default_reviewers_data, validate_default_reviewers = \
1095 PullRequestModel().get_reviewer_functions()
1096 PullRequestModel().get_reviewer_functions()
1096
1097
1097 try:
1098 try:
1098 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1099 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1099 except ValueError as e:
1100 except ValueError as e:
1100 log.error('Reviewers Validation: {}'.format(e))
1101 log.error('Reviewers Validation: {}'.format(e))
1101 h.flash(e, category='error')
1102 h.flash(e, category='error')
1102 return
1103 return
1103
1104
1104 PullRequestModel().update_reviewers(
1105 PullRequestModel().update_reviewers(
1105 pull_request, reviewers, self._rhodecode_user)
1106 pull_request, reviewers, self._rhodecode_user)
1106 h.flash(_('Pull request reviewers updated.'), category='success')
1107 h.flash(_('Pull request reviewers updated.'), category='success')
1107 Session().commit()
1108 Session().commit()
1108
1109
1109 @LoginRequired()
1110 @LoginRequired()
1110 @NotAnonymous()
1111 @NotAnonymous()
1111 @HasRepoPermissionAnyDecorator(
1112 @HasRepoPermissionAnyDecorator(
1112 'repository.read', 'repository.write', 'repository.admin')
1113 'repository.read', 'repository.write', 'repository.admin')
1113 @CSRFRequired()
1114 @CSRFRequired()
1114 @view_config(
1115 @view_config(
1115 route_name='pullrequest_delete', request_method='POST',
1116 route_name='pullrequest_delete', request_method='POST',
1116 renderer='json_ext')
1117 renderer='json_ext')
1117 def pull_request_delete(self):
1118 def pull_request_delete(self):
1118 _ = self.request.translate
1119 _ = self.request.translate
1119
1120
1120 pull_request = PullRequest.get_or_404(
1121 pull_request = PullRequest.get_or_404(
1121 self.request.matchdict['pull_request_id'])
1122 self.request.matchdict['pull_request_id'])
1122 self.load_default_context()
1123 self.load_default_context()
1123
1124
1124 pr_closed = pull_request.is_closed()
1125 pr_closed = pull_request.is_closed()
1125 allowed_to_delete = PullRequestModel().check_user_delete(
1126 allowed_to_delete = PullRequestModel().check_user_delete(
1126 pull_request, self._rhodecode_user) and not pr_closed
1127 pull_request, self._rhodecode_user) and not pr_closed
1127
1128
1128 # only owner can delete it !
1129 # only owner can delete it !
1129 if allowed_to_delete:
1130 if allowed_to_delete:
1130 PullRequestModel().delete(pull_request, self._rhodecode_user)
1131 PullRequestModel().delete(pull_request, self._rhodecode_user)
1131 Session().commit()
1132 Session().commit()
1132 h.flash(_('Successfully deleted pull request'),
1133 h.flash(_('Successfully deleted pull request'),
1133 category='success')
1134 category='success')
1134 raise HTTPFound(h.route_path('pullrequest_show_all',
1135 raise HTTPFound(h.route_path('pullrequest_show_all',
1135 repo_name=self.db_repo_name))
1136 repo_name=self.db_repo_name))
1136
1137
1137 log.warning('user %s tried to delete pull request without access',
1138 log.warning('user %s tried to delete pull request without access',
1138 self._rhodecode_user)
1139 self._rhodecode_user)
1139 raise HTTPNotFound()
1140 raise HTTPNotFound()
1140
1141
1141 @LoginRequired()
1142 @LoginRequired()
1142 @NotAnonymous()
1143 @NotAnonymous()
1143 @HasRepoPermissionAnyDecorator(
1144 @HasRepoPermissionAnyDecorator(
1144 'repository.read', 'repository.write', 'repository.admin')
1145 'repository.read', 'repository.write', 'repository.admin')
1145 @CSRFRequired()
1146 @CSRFRequired()
1146 @view_config(
1147 @view_config(
1147 route_name='pullrequest_comment_create', request_method='POST',
1148 route_name='pullrequest_comment_create', request_method='POST',
1148 renderer='json_ext')
1149 renderer='json_ext')
1149 def pull_request_comment_create(self):
1150 def pull_request_comment_create(self):
1150 _ = self.request.translate
1151 _ = self.request.translate
1151
1152
1152 pull_request = PullRequest.get_or_404(
1153 pull_request = PullRequest.get_or_404(
1153 self.request.matchdict['pull_request_id'])
1154 self.request.matchdict['pull_request_id'])
1154 pull_request_id = pull_request.pull_request_id
1155 pull_request_id = pull_request.pull_request_id
1155
1156
1156 if pull_request.is_closed():
1157 if pull_request.is_closed():
1157 log.debug('comment: forbidden because pull request is closed')
1158 log.debug('comment: forbidden because pull request is closed')
1158 raise HTTPForbidden()
1159 raise HTTPForbidden()
1159
1160
1160 allowed_to_comment = PullRequestModel().check_user_comment(
1161 allowed_to_comment = PullRequestModel().check_user_comment(
1161 pull_request, self._rhodecode_user)
1162 pull_request, self._rhodecode_user)
1162 if not allowed_to_comment:
1163 if not allowed_to_comment:
1163 log.debug(
1164 log.debug(
1164 'comment: forbidden because pull request is from forbidden repo')
1165 'comment: forbidden because pull request is from forbidden repo')
1165 raise HTTPForbidden()
1166 raise HTTPForbidden()
1166
1167
1167 c = self.load_default_context()
1168 c = self.load_default_context()
1168
1169
1169 status = self.request.POST.get('changeset_status', None)
1170 status = self.request.POST.get('changeset_status', None)
1170 text = self.request.POST.get('text')
1171 text = self.request.POST.get('text')
1171 comment_type = self.request.POST.get('comment_type')
1172 comment_type = self.request.POST.get('comment_type')
1172 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1173 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1173 close_pull_request = self.request.POST.get('close_pull_request')
1174 close_pull_request = self.request.POST.get('close_pull_request')
1174
1175
1175 # the logic here should work like following, if we submit close
1176 # the logic here should work like following, if we submit close
1176 # pr comment, use `close_pull_request_with_comment` function
1177 # pr comment, use `close_pull_request_with_comment` function
1177 # else handle regular comment logic
1178 # else handle regular comment logic
1178
1179
1179 if close_pull_request:
1180 if close_pull_request:
1180 # only owner or admin or person with write permissions
1181 # only owner or admin or person with write permissions
1181 allowed_to_close = PullRequestModel().check_user_update(
1182 allowed_to_close = PullRequestModel().check_user_update(
1182 pull_request, self._rhodecode_user)
1183 pull_request, self._rhodecode_user)
1183 if not allowed_to_close:
1184 if not allowed_to_close:
1184 log.debug('comment: forbidden because not allowed to close '
1185 log.debug('comment: forbidden because not allowed to close '
1185 'pull request %s', pull_request_id)
1186 'pull request %s', pull_request_id)
1186 raise HTTPForbidden()
1187 raise HTTPForbidden()
1187 comment, status = PullRequestModel().close_pull_request_with_comment(
1188 comment, status = PullRequestModel().close_pull_request_with_comment(
1188 pull_request, self._rhodecode_user, self.db_repo, message=text)
1189 pull_request, self._rhodecode_user, self.db_repo, message=text)
1189 Session().flush()
1190 Session().flush()
1190 events.trigger(
1191 events.trigger(
1191 events.PullRequestCommentEvent(pull_request, comment))
1192 events.PullRequestCommentEvent(pull_request, comment))
1192
1193
1193 else:
1194 else:
1194 # regular comment case, could be inline, or one with status.
1195 # regular comment case, could be inline, or one with status.
1195 # for that one we check also permissions
1196 # for that one we check also permissions
1196
1197
1197 allowed_to_change_status = PullRequestModel().check_user_change_status(
1198 allowed_to_change_status = PullRequestModel().check_user_change_status(
1198 pull_request, self._rhodecode_user)
1199 pull_request, self._rhodecode_user)
1199
1200
1200 if status and allowed_to_change_status:
1201 if status and allowed_to_change_status:
1201 message = (_('Status change %(transition_icon)s %(status)s')
1202 message = (_('Status change %(transition_icon)s %(status)s')
1202 % {'transition_icon': '>',
1203 % {'transition_icon': '>',
1203 'status': ChangesetStatus.get_status_lbl(status)})
1204 'status': ChangesetStatus.get_status_lbl(status)})
1204 text = text or message
1205 text = text or message
1205
1206
1206 comment = CommentsModel().create(
1207 comment = CommentsModel().create(
1207 text=text,
1208 text=text,
1208 repo=self.db_repo.repo_id,
1209 repo=self.db_repo.repo_id,
1209 user=self._rhodecode_user.user_id,
1210 user=self._rhodecode_user.user_id,
1210 pull_request=pull_request,
1211 pull_request=pull_request,
1211 f_path=self.request.POST.get('f_path'),
1212 f_path=self.request.POST.get('f_path'),
1212 line_no=self.request.POST.get('line'),
1213 line_no=self.request.POST.get('line'),
1213 status_change=(ChangesetStatus.get_status_lbl(status)
1214 status_change=(ChangesetStatus.get_status_lbl(status)
1214 if status and allowed_to_change_status else None),
1215 if status and allowed_to_change_status else None),
1215 status_change_type=(status
1216 status_change_type=(status
1216 if status and allowed_to_change_status else None),
1217 if status and allowed_to_change_status else None),
1217 comment_type=comment_type,
1218 comment_type=comment_type,
1218 resolves_comment_id=resolves_comment_id,
1219 resolves_comment_id=resolves_comment_id,
1219 auth_user=self._rhodecode_user
1220 auth_user=self._rhodecode_user
1220 )
1221 )
1221
1222
1222 if allowed_to_change_status:
1223 if allowed_to_change_status:
1223 # calculate old status before we change it
1224 # calculate old status before we change it
1224 old_calculated_status = pull_request.calculated_review_status()
1225 old_calculated_status = pull_request.calculated_review_status()
1225
1226
1226 # get status if set !
1227 # get status if set !
1227 if status:
1228 if status:
1228 ChangesetStatusModel().set_status(
1229 ChangesetStatusModel().set_status(
1229 self.db_repo.repo_id,
1230 self.db_repo.repo_id,
1230 status,
1231 status,
1231 self._rhodecode_user.user_id,
1232 self._rhodecode_user.user_id,
1232 comment,
1233 comment,
1233 pull_request=pull_request
1234 pull_request=pull_request
1234 )
1235 )
1235
1236
1236 Session().flush()
1237 Session().flush()
1237 # this is somehow required to get access to some relationship
1238 # this is somehow required to get access to some relationship
1238 # loaded on comment
1239 # loaded on comment
1239 Session().refresh(comment)
1240 Session().refresh(comment)
1240
1241
1241 events.trigger(
1242 events.trigger(
1242 events.PullRequestCommentEvent(pull_request, comment))
1243 events.PullRequestCommentEvent(pull_request, comment))
1243
1244
1244 # we now calculate the status of pull request, and based on that
1245 # we now calculate the status of pull request, and based on that
1245 # calculation we set the commits status
1246 # calculation we set the commits status
1246 calculated_status = pull_request.calculated_review_status()
1247 calculated_status = pull_request.calculated_review_status()
1247 if old_calculated_status != calculated_status:
1248 if old_calculated_status != calculated_status:
1248 PullRequestModel()._trigger_pull_request_hook(
1249 PullRequestModel()._trigger_pull_request_hook(
1249 pull_request, self._rhodecode_user, 'review_status_change')
1250 pull_request, self._rhodecode_user, 'review_status_change')
1250
1251
1251 Session().commit()
1252 Session().commit()
1252
1253
1253 data = {
1254 data = {
1254 'target_id': h.safeid(h.safe_unicode(
1255 'target_id': h.safeid(h.safe_unicode(
1255 self.request.POST.get('f_path'))),
1256 self.request.POST.get('f_path'))),
1256 }
1257 }
1257 if comment:
1258 if comment:
1258 c.co = comment
1259 c.co = comment
1259 rendered_comment = render(
1260 rendered_comment = render(
1260 'rhodecode:templates/changeset/changeset_comment_block.mako',
1261 'rhodecode:templates/changeset/changeset_comment_block.mako',
1261 self._get_template_context(c), self.request)
1262 self._get_template_context(c), self.request)
1262
1263
1263 data.update(comment.get_dict())
1264 data.update(comment.get_dict())
1264 data.update({'rendered_text': rendered_comment})
1265 data.update({'rendered_text': rendered_comment})
1265
1266
1266 return data
1267 return data
1267
1268
1268 @LoginRequired()
1269 @LoginRequired()
1269 @NotAnonymous()
1270 @NotAnonymous()
1270 @HasRepoPermissionAnyDecorator(
1271 @HasRepoPermissionAnyDecorator(
1271 'repository.read', 'repository.write', 'repository.admin')
1272 'repository.read', 'repository.write', 'repository.admin')
1272 @CSRFRequired()
1273 @CSRFRequired()
1273 @view_config(
1274 @view_config(
1274 route_name='pullrequest_comment_delete', request_method='POST',
1275 route_name='pullrequest_comment_delete', request_method='POST',
1275 renderer='json_ext')
1276 renderer='json_ext')
1276 def pull_request_comment_delete(self):
1277 def pull_request_comment_delete(self):
1277 pull_request = PullRequest.get_or_404(
1278 pull_request = PullRequest.get_or_404(
1278 self.request.matchdict['pull_request_id'])
1279 self.request.matchdict['pull_request_id'])
1279
1280
1280 comment = ChangesetComment.get_or_404(
1281 comment = ChangesetComment.get_or_404(
1281 self.request.matchdict['comment_id'])
1282 self.request.matchdict['comment_id'])
1282 comment_id = comment.comment_id
1283 comment_id = comment.comment_id
1283
1284
1284 if pull_request.is_closed():
1285 if pull_request.is_closed():
1285 log.debug('comment: forbidden because pull request is closed')
1286 log.debug('comment: forbidden because pull request is closed')
1286 raise HTTPForbidden()
1287 raise HTTPForbidden()
1287
1288
1288 if not comment:
1289 if not comment:
1289 log.debug('Comment with id:%s not found, skipping', comment_id)
1290 log.debug('Comment with id:%s not found, skipping', comment_id)
1290 # comment already deleted in another call probably
1291 # comment already deleted in another call probably
1291 return True
1292 return True
1292
1293
1293 if comment.pull_request.is_closed():
1294 if comment.pull_request.is_closed():
1294 # don't allow deleting comments on closed pull request
1295 # don't allow deleting comments on closed pull request
1295 raise HTTPForbidden()
1296 raise HTTPForbidden()
1296
1297
1297 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1298 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1298 super_admin = h.HasPermissionAny('hg.admin')()
1299 super_admin = h.HasPermissionAny('hg.admin')()
1299 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1300 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1300 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1301 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1301 comment_repo_admin = is_repo_admin and is_repo_comment
1302 comment_repo_admin = is_repo_admin and is_repo_comment
1302
1303
1303 if super_admin or comment_owner or comment_repo_admin:
1304 if super_admin or comment_owner or comment_repo_admin:
1304 old_calculated_status = comment.pull_request.calculated_review_status()
1305 old_calculated_status = comment.pull_request.calculated_review_status()
1305 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1306 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1306 Session().commit()
1307 Session().commit()
1307 calculated_status = comment.pull_request.calculated_review_status()
1308 calculated_status = comment.pull_request.calculated_review_status()
1308 if old_calculated_status != calculated_status:
1309 if old_calculated_status != calculated_status:
1309 PullRequestModel()._trigger_pull_request_hook(
1310 PullRequestModel()._trigger_pull_request_hook(
1310 comment.pull_request, self._rhodecode_user, 'review_status_change')
1311 comment.pull_request, self._rhodecode_user, 'review_status_change')
1311 return True
1312 return True
1312 else:
1313 else:
1313 log.warning('No permissions for user %s to delete comment_id: %s',
1314 log.warning('No permissions for user %s to delete comment_id: %s',
1314 self._rhodecode_db_user, comment_id)
1315 self._rhodecode_db_user, comment_id)
1315 raise HTTPNotFound()
1316 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now