##// END OF EJS Templates
api: pull-requests fixed logic of ancestor calculation and target ref calculation based on the web view.
marcink -
r2873:d22eab3d default
parent child Browse files
Show More
@@ -1,296 +1,311 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):
98 data = self._prepare_data(backend)
99 data.pop('title')
100 id_, params = build_data(
101 self.apikey_regular, 'create_pull_request', **data)
102 response = api_call(self.app, params)
103 result = response.json
104 pull_request_id = result['result']['pull_request_id']
105 pull_request = PullRequestModel().get(pull_request_id)
106 data['ref'] = backend.default_branch_name
107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
108 assert pull_request.title == title
109
110 @pytest.mark.backends("git", "hg")
97 def test_create_with_reviewers_specified_by_names(
111 def test_create_with_reviewers_specified_by_names(
98 self, backend, no_notifications):
112 self, backend, no_notifications):
99 data = self._prepare_data(backend)
113 data = self._prepare_data(backend)
100 reviewers = [
114 reviewers = [
101 {'username': TEST_USER_REGULAR_LOGIN,
115 {'username': TEST_USER_REGULAR_LOGIN,
102 'reasons': ['added manually']},
116 'reasons': ['added manually']},
103 {'username': TEST_USER_ADMIN_LOGIN,
117 {'username': TEST_USER_ADMIN_LOGIN,
104 'reasons': ['added manually']},
118 'reasons': ['added manually']},
105 ]
119 ]
106 data['reviewers'] = reviewers
120 data['reviewers'] = reviewers
107 id_, params = build_data(
121 id_, params = build_data(
108 self.apikey_regular, 'create_pull_request', **data)
122 self.apikey_regular, 'create_pull_request', **data)
109 response = api_call(self.app, params)
123 response = api_call(self.app, params)
110
124
111 expected_message = "Created new pull request `{title}`".format(
125 expected_message = "Created new pull request `{title}`".format(
112 title=data['title'])
126 title=data['title'])
113 result = response.json
127 result = response.json
114 assert result['result']['msg'] == expected_message
128 assert result['result']['msg'] == expected_message
115 pull_request_id = result['result']['pull_request_id']
129 pull_request_id = result['result']['pull_request_id']
116 pull_request = PullRequestModel().get(pull_request_id)
130 pull_request = PullRequestModel().get(pull_request_id)
117 actual_reviewers = [
131 actual_reviewers = [
118 {'username': r.user.username,
132 {'username': r.user.username,
119 'reasons': ['added manually'],
133 'reasons': ['added manually'],
120 } for r in pull_request.reviewers
134 } for r in pull_request.reviewers
121 ]
135 ]
122 assert sorted(actual_reviewers) == sorted(reviewers)
136 assert sorted(actual_reviewers) == sorted(reviewers)
123
137
124 @pytest.mark.backends("git", "hg")
138 @pytest.mark.backends("git", "hg")
125 def test_create_with_reviewers_specified_by_ids(
139 def test_create_with_reviewers_specified_by_ids(
126 self, backend, no_notifications):
140 self, backend, no_notifications):
127 data = self._prepare_data(backend)
141 data = self._prepare_data(backend)
128 reviewers = [
142 reviewers = [
129 {'username': UserModel().get_by_username(
143 {'username': UserModel().get_by_username(
130 TEST_USER_REGULAR_LOGIN).user_id,
144 TEST_USER_REGULAR_LOGIN).user_id,
131 'reasons': ['added manually']},
145 'reasons': ['added manually']},
132 {'username': UserModel().get_by_username(
146 {'username': UserModel().get_by_username(
133 TEST_USER_ADMIN_LOGIN).user_id,
147 TEST_USER_ADMIN_LOGIN).user_id,
134 'reasons': ['added manually']},
148 'reasons': ['added manually']},
135 ]
149 ]
136
150
137 data['reviewers'] = reviewers
151 data['reviewers'] = reviewers
138 id_, params = build_data(
152 id_, params = build_data(
139 self.apikey_regular, 'create_pull_request', **data)
153 self.apikey_regular, 'create_pull_request', **data)
140 response = api_call(self.app, params)
154 response = api_call(self.app, params)
141
155
142 expected_message = "Created new pull request `{title}`".format(
156 expected_message = "Created new pull request `{title}`".format(
143 title=data['title'])
157 title=data['title'])
144 result = response.json
158 result = response.json
145 assert result['result']['msg'] == expected_message
159 assert result['result']['msg'] == expected_message
146 pull_request_id = result['result']['pull_request_id']
160 pull_request_id = result['result']['pull_request_id']
147 pull_request = PullRequestModel().get(pull_request_id)
161 pull_request = PullRequestModel().get(pull_request_id)
148 actual_reviewers = [
162 actual_reviewers = [
149 {'username': r.user.user_id,
163 {'username': r.user.user_id,
150 'reasons': ['added manually'],
164 'reasons': ['added manually'],
151 } for r in pull_request.reviewers
165 } for r in pull_request.reviewers
152 ]
166 ]
153 assert sorted(actual_reviewers) == sorted(reviewers)
167 assert sorted(actual_reviewers) == sorted(reviewers)
154
168
155 @pytest.mark.backends("git", "hg")
169 @pytest.mark.backends("git", "hg")
156 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
170 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
157 data = self._prepare_data(backend)
171 data = self._prepare_data(backend)
158 data['reviewers'] = [{'username': 'somebody'}]
172 data['reviewers'] = [{'username': 'somebody'}]
159 id_, params = build_data(
173 id_, params = build_data(
160 self.apikey_regular, 'create_pull_request', **data)
174 self.apikey_regular, 'create_pull_request', **data)
161 response = api_call(self.app, params)
175 response = api_call(self.app, params)
162 expected_message = 'user `somebody` does not exist'
176 expected_message = 'user `somebody` does not exist'
163 assert_error(id_, expected_message, given=response.body)
177 assert_error(id_, expected_message, given=response.body)
164
178
165 @pytest.mark.backends("git", "hg")
179 @pytest.mark.backends("git", "hg")
166 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
180 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
167 data = self._prepare_data(backend)
181 data = self._prepare_data(backend)
168 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
182 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
169 data['reviewers'] = reviewers
183 data['reviewers'] = reviewers
170 id_, params = build_data(
184 id_, params = build_data(
171 self.apikey_regular, 'create_pull_request', **data)
185 self.apikey_regular, 'create_pull_request', **data)
172 response = api_call(self.app, params)
186 response = api_call(self.app, params)
173 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
187 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
174 assert_error(id_, expected_message, given=response.body)
188 assert_error(id_, expected_message, given=response.body)
175
189
176 @pytest.mark.backends("git", "hg")
190 @pytest.mark.backends("git", "hg")
177 def test_create_with_no_commit_hashes(self, backend):
191 def test_create_with_no_commit_hashes(self, backend):
178 data = self._prepare_data(backend)
192 data = self._prepare_data(backend)
179 expected_source_ref = data['source_ref']
193 expected_source_ref = data['source_ref']
180 expected_target_ref = data['target_ref']
194 expected_target_ref = data['target_ref']
181 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
195 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
182 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
196 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
183 id_, params = build_data(
197 id_, params = build_data(
184 self.apikey_regular, 'create_pull_request', **data)
198 self.apikey_regular, 'create_pull_request', **data)
185 response = api_call(self.app, params)
199 response = api_call(self.app, params)
186 expected_message = "Created new pull request `{title}`".format(
200 expected_message = "Created new pull request `{title}`".format(
187 title=data['title'])
201 title=data['title'])
188 result = response.json
202 result = response.json
189 assert result['result']['msg'] == expected_message
203 assert result['result']['msg'] == expected_message
190 pull_request_id = result['result']['pull_request_id']
204 pull_request_id = result['result']['pull_request_id']
191 pull_request = PullRequestModel().get(pull_request_id)
205 pull_request = PullRequestModel().get(pull_request_id)
192 assert pull_request.source_ref == expected_source_ref
206 assert pull_request.source_ref == expected_source_ref
193 assert pull_request.target_ref == expected_target_ref
207 assert pull_request.target_ref == expected_target_ref
194
208
195 @pytest.mark.backends("git", "hg")
209 @pytest.mark.backends("git", "hg")
196 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
210 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
197 def test_create_fails_with_wrong_repo(self, backend, data_key):
211 def test_create_fails_with_wrong_repo(self, backend, data_key):
198 repo_name = 'fake-repo'
212 repo_name = 'fake-repo'
199 data = self._prepare_data(backend)
213 data = self._prepare_data(backend)
200 data[data_key] = repo_name
214 data[data_key] = repo_name
201 id_, params = build_data(
215 id_, params = build_data(
202 self.apikey_regular, 'create_pull_request', **data)
216 self.apikey_regular, 'create_pull_request', **data)
203 response = api_call(self.app, params)
217 response = api_call(self.app, params)
204 expected_message = 'repository `{}` does not exist'.format(repo_name)
218 expected_message = 'repository `{}` does not exist'.format(repo_name)
205 assert_error(id_, expected_message, given=response.body)
219 assert_error(id_, expected_message, given=response.body)
206
220
207 @pytest.mark.backends("git", "hg")
221 @pytest.mark.backends("git", "hg")
208 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
222 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
209 def test_create_fails_with_non_existing_branch(self, backend, data_key):
223 def test_create_fails_with_non_existing_branch(self, backend, data_key):
210 branch_name = 'test-branch'
224 branch_name = 'test-branch'
211 data = self._prepare_data(backend)
225 data = self._prepare_data(backend)
212 data[data_key] = "branch:{}".format(branch_name)
226 data[data_key] = "branch:{}".format(branch_name)
213 id_, params = build_data(
227 id_, params = build_data(
214 self.apikey_regular, 'create_pull_request', **data)
228 self.apikey_regular, 'create_pull_request', **data)
215 response = api_call(self.app, params)
229 response = api_call(self.app, params)
216 expected_message = 'The specified branch `{}` does not exist'.format(
230 expected_message = 'The specified branch `{}` does not exist'.format(
217 branch_name)
231 branch_name)
218 assert_error(id_, expected_message, given=response.body)
232 assert_error(id_, expected_message, given=response.body)
219
233
220 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
221 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
235 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
222 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
236 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
223 data = self._prepare_data(backend)
237 data = self._prepare_data(backend)
224 ref = 'stange-ref'
238 ref = 'stange-ref'
225 data[data_key] = ref
239 data[data_key] = ref
226 id_, params = build_data(
240 id_, params = build_data(
227 self.apikey_regular, 'create_pull_request', **data)
241 self.apikey_regular, 'create_pull_request', **data)
228 response = api_call(self.app, params)
242 response = api_call(self.app, params)
229 expected_message = (
243 expected_message = (
230 'Ref `{ref}` given in a wrong format. Please check the API'
244 'Ref `{ref}` given in a wrong format. Please check the API'
231 ' documentation for more details'.format(ref=ref))
245 ' documentation for more details'.format(ref=ref))
232 assert_error(id_, expected_message, given=response.body)
246 assert_error(id_, expected_message, given=response.body)
233
247
234 @pytest.mark.backends("git", "hg")
248 @pytest.mark.backends("git", "hg")
235 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
249 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
236 def test_create_fails_with_non_existing_ref(self, backend, data_key):
250 def test_create_fails_with_non_existing_ref(self, backend, data_key):
237 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
251 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
238 ref = self._get_full_ref(backend, commit_id)
252 ref = self._get_full_ref(backend, commit_id)
239 data = self._prepare_data(backend)
253 data = self._prepare_data(backend)
240 data[data_key] = ref
254 data[data_key] = ref
241 id_, params = build_data(
255 id_, params = build_data(
242 self.apikey_regular, 'create_pull_request', **data)
256 self.apikey_regular, 'create_pull_request', **data)
243 response = api_call(self.app, params)
257 response = api_call(self.app, params)
244 expected_message = 'Ref `{}` does not exist'.format(ref)
258 expected_message = 'Ref `{}` does not exist'.format(ref)
245 assert_error(id_, expected_message, given=response.body)
259 assert_error(id_, expected_message, given=response.body)
246
260
247 @pytest.mark.backends("git", "hg")
261 @pytest.mark.backends("git", "hg")
248 def test_create_fails_when_no_revisions(self, backend):
262 def test_create_fails_when_no_revisions(self, backend):
249 data = self._prepare_data(backend, source_head='initial')
263 data = self._prepare_data(backend, source_head='initial')
250 id_, params = build_data(
264 id_, params = build_data(
251 self.apikey_regular, 'create_pull_request', **data)
265 self.apikey_regular, 'create_pull_request', **data)
252 response = api_call(self.app, params)
266 response = api_call(self.app, params)
253 expected_message = 'no commits found'
267 expected_message = 'no commits found'
254 assert_error(id_, expected_message, given=response.body)
268 assert_error(id_, expected_message, given=response.body)
255
269
256 @pytest.mark.backends("git", "hg")
270 @pytest.mark.backends("git", "hg")
257 def test_create_fails_when_no_permissions(self, backend):
271 def test_create_fails_when_no_permissions(self, backend):
258 data = self._prepare_data(backend)
272 data = self._prepare_data(backend)
259 RepoModel().revoke_user_permission(
273 RepoModel().revoke_user_permission(
260 self.source.repo_name, User.DEFAULT_USER)
274 self.source.repo_name, User.DEFAULT_USER)
261 RepoModel().revoke_user_permission(
275 RepoModel().revoke_user_permission(
262 self.source.repo_name, self.test_user)
276 self.source.repo_name, self.test_user)
263 id_, params = build_data(
277 id_, params = build_data(
264 self.apikey_regular, 'create_pull_request', **data)
278 self.apikey_regular, 'create_pull_request', **data)
265 response = api_call(self.app, params)
279 response = api_call(self.app, params)
266 expected_message = 'repository `{}` does not exist'.format(
280 expected_message = 'repository `{}` does not exist'.format(
267 self.source.repo_name)
281 self.source.repo_name)
268 assert_error(id_, expected_message, given=response.body)
282 assert_error(id_, expected_message, given=response.body)
269
283
270 def _prepare_data(
284 def _prepare_data(
271 self, backend, source_head='change', target_head='initial'):
285 self, backend, source_head='change', target_head='initial'):
272 commits = [
286 commits = [
273 {'message': 'initial'},
287 {'message': 'initial'},
274 {'message': 'change'},
288 {'message': 'change'},
275 {'message': 'new-feature', 'parents': ['initial']},
289 {'message': 'new-feature', 'parents': ['initial']},
276 ]
290 ]
277 self.commit_ids = backend.create_master_repo(commits)
291 self.commit_ids = backend.create_master_repo(commits)
278 self.source = backend.create_repo(heads=[source_head])
292 self.source = backend.create_repo(heads=[source_head])
279 self.target = backend.create_repo(heads=[target_head])
293 self.target = backend.create_repo(heads=[target_head])
294
280 data = {
295 data = {
281 'source_repo': self.source.repo_name,
296 'source_repo': self.source.repo_name,
282 'target_repo': self.target.repo_name,
297 'target_repo': self.target.repo_name,
283 'source_ref': self._get_full_ref(
298 'source_ref': self._get_full_ref(
284 backend, self.commit_ids[source_head]),
299 backend, self.commit_ids[source_head]),
285 'target_ref': self._get_full_ref(
300 'target_ref': self._get_full_ref(
286 backend, self.commit_ids[target_head]),
301 backend, self.commit_ids[target_head]),
287 'title': 'Test PR 1',
302 'title': 'Test PR 1',
288 'description': 'Test'
303 'description': 'Test'
289 }
304 }
290 RepoModel().grant_user_permission(
305 RepoModel().grant_user_permission(
291 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
306 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
292 return data
307 return data
293
308
294 def _get_full_ref(self, backend, commit_id):
309 def _get_full_ref(self, backend, commit_id):
295 return 'branch:{branch}:{commit_id}'.format(
310 return 'branch:{branch}:{commit_id}'.format(
296 branch=backend.default_branch_name, commit_id=commit_id)
311 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,913 +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
620 source_scm = source_db_repo.scm_instance()
621 target_scm = target_db_repo.scm_instance()
622
619 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)
620 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)
621 source_scm = source_db_repo.scm_instance()
625
622 target_scm = target_db_repo.scm_instance()
626 ancestor = source_scm.get_common_ancestor(
627 source_commit.raw_id, target_commit.raw_id, target_scm)
628 if not ancestor:
629 raise JSONRPCError('no common ancestor found')
630
631 # recalculate target ref based on ancestor
632 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
633 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
623
634
624 commit_ranges = target_scm.compare(
635 commit_ranges = target_scm.compare(
625 target_commit.raw_id, source_commit.raw_id, source_scm,
636 target_commit.raw_id, source_commit.raw_id, source_scm,
626 merge=True, pre_load=[])
637 merge=True, pre_load=[])
627
638
628 ancestor = target_scm.get_common_ancestor(
629 target_commit.raw_id, source_commit.raw_id, source_scm)
630
631 if not commit_ranges:
639 if not commit_ranges:
632 raise JSONRPCError('no commits found')
640 raise JSONRPCError('no commits found')
633
641
634 if not ancestor:
635 raise JSONRPCError('no common ancestor found')
636
637 reviewer_objects = Optional.extract(reviewers) or []
642 reviewer_objects = Optional.extract(reviewers) or []
638
643
639 if reviewer_objects:
644 if reviewer_objects:
640 schema = ReviewerListSchema()
645 schema = ReviewerListSchema()
641 try:
646 try:
642 reviewer_objects = schema.deserialize(reviewer_objects)
647 reviewer_objects = schema.deserialize(reviewer_objects)
643 except Invalid as err:
648 except Invalid as err:
644 raise JSONRPCValidationError(colander_exc=err)
649 raise JSONRPCValidationError(colander_exc=err)
645
650
646 # validate users
651 # validate users
647 for reviewer_object in reviewer_objects:
652 for reviewer_object in reviewer_objects:
648 user = get_user_or_error(reviewer_object['username'])
653 user = get_user_or_error(reviewer_object['username'])
649 reviewer_object['user_id'] = user.user_id
654 reviewer_object['user_id'] = user.user_id
650
655
651 get_default_reviewers_data, get_validated_reviewers = \
656 get_default_reviewers_data, get_validated_reviewers = \
652 PullRequestModel().get_reviewer_functions()
657 PullRequestModel().get_reviewer_functions()
653
658
654 reviewer_rules = get_default_reviewers_data(
659 reviewer_rules = get_default_reviewers_data(
655 apiuser.get_instance(), source_db_repo,
660 apiuser.get_instance(), source_db_repo,
656 source_commit, target_db_repo, target_commit)
661 source_commit, target_db_repo, target_commit)
657
662
658 # specified rules are later re-validated, thus we can assume users will
663 # specified rules are later re-validated, thus we can assume users will
659 # eventually provide those that meet the reviewer criteria.
664 # eventually provide those that meet the reviewer criteria.
660 if not reviewer_objects:
665 if not reviewer_objects:
661 reviewer_objects = reviewer_rules['reviewers']
666 reviewer_objects = reviewer_rules['reviewers']
662
667
663 try:
668 try:
664 reviewers = get_validated_reviewers(
669 reviewers = get_validated_reviewers(
665 reviewer_objects, reviewer_rules)
670 reviewer_objects, reviewer_rules)
666 except ValueError as e:
671 except ValueError as e:
667 raise JSONRPCError('Reviewers Validation: {}'.format(e))
672 raise JSONRPCError('Reviewers Validation: {}'.format(e))
668
673
669 title = Optional.extract(title)
674 title = Optional.extract(title)
670 if not title:
675 if not title:
671 title_source_ref = source_ref.split(':', 2)[1]
676 title_source_ref = source_ref.split(':', 2)[1]
672 title = PullRequestModel().generate_pullrequest_title(
677 title = PullRequestModel().generate_pullrequest_title(
673 source=source_repo,
678 source=source_repo,
674 source_ref=title_source_ref,
679 source_ref=title_source_ref,
675 target=target_repo
680 target=target_repo
676 )
681 )
682 description = Optional.extract(description)
677
683
678 pull_request = PullRequestModel().create(
684 pull_request = PullRequestModel().create(
679 created_by=apiuser.user_id,
685 created_by=apiuser.user_id,
680 source_repo=source_repo,
686 source_repo=source_repo,
681 source_ref=full_source_ref,
687 source_ref=full_source_ref,
682 target_repo=target_repo,
688 target_repo=target_repo,
683 target_ref=full_target_ref,
689 target_ref=full_target_ref,
684 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
690 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
685 reviewers=reviewers,
691 reviewers=reviewers,
686 title=title,
692 title=title,
687 description=Optional.extract(description),
693 description=description,
688 reviewer_data=reviewer_rules,
694 reviewer_data=reviewer_rules,
689 auth_user=apiuser
695 auth_user=apiuser
690 )
696 )
691
697
692 Session().commit()
698 Session().commit()
693 data = {
699 data = {
694 'msg': 'Created new pull request `{}`'.format(title),
700 'msg': 'Created new pull request `{}`'.format(title),
695 'pull_request_id': pull_request.pull_request_id,
701 'pull_request_id': pull_request.pull_request_id,
696 }
702 }
697 return data
703 return data
698
704
699
705
700 @jsonrpc_method()
706 @jsonrpc_method()
701 def update_pull_request(
707 def update_pull_request(
702 request, apiuser, pullrequestid, repoid=Optional(None),
708 request, apiuser, pullrequestid, repoid=Optional(None),
703 title=Optional(''), description=Optional(''), reviewers=Optional(None),
709 title=Optional(''), description=Optional(''), reviewers=Optional(None),
704 update_commits=Optional(None)):
710 update_commits=Optional(None)):
705 """
711 """
706 Updates a pull request.
712 Updates a pull request.
707
713
708 :param apiuser: This is filled automatically from the |authtoken|.
714 :param apiuser: This is filled automatically from the |authtoken|.
709 :type apiuser: AuthUser
715 :type apiuser: AuthUser
710 :param repoid: Optional repository name or repository ID.
716 :param repoid: Optional repository name or repository ID.
711 :type repoid: str or int
717 :type repoid: str or int
712 :param pullrequestid: The pull request ID.
718 :param pullrequestid: The pull request ID.
713 :type pullrequestid: int
719 :type pullrequestid: int
714 :param title: Set the pull request title.
720 :param title: Set the pull request title.
715 :type title: str
721 :type title: str
716 :param description: Update pull request description.
722 :param description: Update pull request description.
717 :type description: Optional(str)
723 :type description: Optional(str)
718 :param reviewers: Update pull request reviewers list with new value.
724 :param reviewers: Update pull request reviewers list with new value.
719 :type reviewers: Optional(list)
725 :type reviewers: Optional(list)
720 Accepts username strings or objects of the format:
726 Accepts username strings or objects of the format:
721
727
722 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
728 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
723
729
724 :param update_commits: Trigger update of commits for this pull request
730 :param update_commits: Trigger update of commits for this pull request
725 :type: update_commits: Optional(bool)
731 :type: update_commits: Optional(bool)
726
732
727 Example output:
733 Example output:
728
734
729 .. code-block:: bash
735 .. code-block:: bash
730
736
731 id : <id_given_in_input>
737 id : <id_given_in_input>
732 result : {
738 result : {
733 "msg": "Updated pull request `63`",
739 "msg": "Updated pull request `63`",
734 "pull_request": <pull_request_object>,
740 "pull_request": <pull_request_object>,
735 "updated_reviewers": {
741 "updated_reviewers": {
736 "added": [
742 "added": [
737 "username"
743 "username"
738 ],
744 ],
739 "removed": []
745 "removed": []
740 },
746 },
741 "updated_commits": {
747 "updated_commits": {
742 "added": [
748 "added": [
743 "<sha1_hash>"
749 "<sha1_hash>"
744 ],
750 ],
745 "common": [
751 "common": [
746 "<sha1_hash>",
752 "<sha1_hash>",
747 "<sha1_hash>",
753 "<sha1_hash>",
748 ],
754 ],
749 "removed": []
755 "removed": []
750 }
756 }
751 }
757 }
752 error : null
758 error : null
753 """
759 """
754
760
755 pull_request = get_pull_request_or_error(pullrequestid)
761 pull_request = get_pull_request_or_error(pullrequestid)
756 if Optional.extract(repoid):
762 if Optional.extract(repoid):
757 repo = get_repo_or_error(repoid)
763 repo = get_repo_or_error(repoid)
758 else:
764 else:
759 repo = pull_request.target_repo
765 repo = pull_request.target_repo
760
766
761 if not PullRequestModel().check_user_update(
767 if not PullRequestModel().check_user_update(
762 pull_request, apiuser, api=True):
768 pull_request, apiuser, api=True):
763 raise JSONRPCError(
769 raise JSONRPCError(
764 'pull request `%s` update failed, no permission to update.' % (
770 'pull request `%s` update failed, no permission to update.' % (
765 pullrequestid,))
771 pullrequestid,))
766 if pull_request.is_closed():
772 if pull_request.is_closed():
767 raise JSONRPCError(
773 raise JSONRPCError(
768 'pull request `%s` update failed, pull request is closed' % (
774 'pull request `%s` update failed, pull request is closed' % (
769 pullrequestid,))
775 pullrequestid,))
770
776
771 reviewer_objects = Optional.extract(reviewers) or []
777 reviewer_objects = Optional.extract(reviewers) or []
772
778
773 if reviewer_objects:
779 if reviewer_objects:
774 schema = ReviewerListSchema()
780 schema = ReviewerListSchema()
775 try:
781 try:
776 reviewer_objects = schema.deserialize(reviewer_objects)
782 reviewer_objects = schema.deserialize(reviewer_objects)
777 except Invalid as err:
783 except Invalid as err:
778 raise JSONRPCValidationError(colander_exc=err)
784 raise JSONRPCValidationError(colander_exc=err)
779
785
780 # validate users
786 # validate users
781 for reviewer_object in reviewer_objects:
787 for reviewer_object in reviewer_objects:
782 user = get_user_or_error(reviewer_object['username'])
788 user = get_user_or_error(reviewer_object['username'])
783 reviewer_object['user_id'] = user.user_id
789 reviewer_object['user_id'] = user.user_id
784
790
785 get_default_reviewers_data, get_validated_reviewers = \
791 get_default_reviewers_data, get_validated_reviewers = \
786 PullRequestModel().get_reviewer_functions()
792 PullRequestModel().get_reviewer_functions()
787
793
788 # re-use stored rules
794 # re-use stored rules
789 reviewer_rules = pull_request.reviewer_data
795 reviewer_rules = pull_request.reviewer_data
790 try:
796 try:
791 reviewers = get_validated_reviewers(
797 reviewers = get_validated_reviewers(
792 reviewer_objects, reviewer_rules)
798 reviewer_objects, reviewer_rules)
793 except ValueError as e:
799 except ValueError as e:
794 raise JSONRPCError('Reviewers Validation: {}'.format(e))
800 raise JSONRPCError('Reviewers Validation: {}'.format(e))
795 else:
801 else:
796 reviewers = []
802 reviewers = []
797
803
798 title = Optional.extract(title)
804 title = Optional.extract(title)
799 description = Optional.extract(description)
805 description = Optional.extract(description)
800 if title or description:
806 if title or description:
801 PullRequestModel().edit(
807 PullRequestModel().edit(
802 pull_request, title or pull_request.title,
808 pull_request, title or pull_request.title,
803 description or pull_request.description, apiuser)
809 description or pull_request.description, apiuser)
804 Session().commit()
810 Session().commit()
805
811
806 commit_changes = {"added": [], "common": [], "removed": []}
812 commit_changes = {"added": [], "common": [], "removed": []}
807 if str2bool(Optional.extract(update_commits)):
813 if str2bool(Optional.extract(update_commits)):
808 if PullRequestModel().has_valid_update_type(pull_request):
814 if PullRequestModel().has_valid_update_type(pull_request):
809 update_response = PullRequestModel().update_commits(
815 update_response = PullRequestModel().update_commits(
810 pull_request)
816 pull_request)
811 commit_changes = update_response.changes or commit_changes
817 commit_changes = update_response.changes or commit_changes
812 Session().commit()
818 Session().commit()
813
819
814 reviewers_changes = {"added": [], "removed": []}
820 reviewers_changes = {"added": [], "removed": []}
815 if reviewers:
821 if reviewers:
816 added_reviewers, removed_reviewers = \
822 added_reviewers, removed_reviewers = \
817 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
823 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
818
824
819 reviewers_changes['added'] = sorted(
825 reviewers_changes['added'] = sorted(
820 [get_user_or_error(n).username for n in added_reviewers])
826 [get_user_or_error(n).username for n in added_reviewers])
821 reviewers_changes['removed'] = sorted(
827 reviewers_changes['removed'] = sorted(
822 [get_user_or_error(n).username for n in removed_reviewers])
828 [get_user_or_error(n).username for n in removed_reviewers])
823 Session().commit()
829 Session().commit()
824
830
825 data = {
831 data = {
826 'msg': 'Updated pull request `{}`'.format(
832 'msg': 'Updated pull request `{}`'.format(
827 pull_request.pull_request_id),
833 pull_request.pull_request_id),
828 'pull_request': pull_request.get_api_data(),
834 'pull_request': pull_request.get_api_data(),
829 'updated_commits': commit_changes,
835 'updated_commits': commit_changes,
830 'updated_reviewers': reviewers_changes
836 'updated_reviewers': reviewers_changes
831 }
837 }
832
838
833 return data
839 return data
834
840
835
841
836 @jsonrpc_method()
842 @jsonrpc_method()
837 def close_pull_request(
843 def close_pull_request(
838 request, apiuser, pullrequestid, repoid=Optional(None),
844 request, apiuser, pullrequestid, repoid=Optional(None),
839 userid=Optional(OAttr('apiuser')), message=Optional('')):
845 userid=Optional(OAttr('apiuser')), message=Optional('')):
840 """
846 """
841 Close the pull request specified by `pullrequestid`.
847 Close the pull request specified by `pullrequestid`.
842
848
843 :param apiuser: This is filled automatically from the |authtoken|.
849 :param apiuser: This is filled automatically from the |authtoken|.
844 :type apiuser: AuthUser
850 :type apiuser: AuthUser
845 :param repoid: Repository name or repository ID to which the pull
851 :param repoid: Repository name or repository ID to which the pull
846 request belongs.
852 request belongs.
847 :type repoid: str or int
853 :type repoid: str or int
848 :param pullrequestid: ID of the pull request to be closed.
854 :param pullrequestid: ID of the pull request to be closed.
849 :type pullrequestid: int
855 :type pullrequestid: int
850 :param userid: Close the pull request as this user.
856 :param userid: Close the pull request as this user.
851 :type userid: Optional(str or int)
857 :type userid: Optional(str or int)
852 :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
853 specified it will be generated automatically.
859 specified it will be generated automatically.
854 :type message: Optional(str)
860 :type message: Optional(str)
855
861
856 Example output:
862 Example output:
857
863
858 .. code-block:: bash
864 .. code-block:: bash
859
865
860 "id": <id_given_in_input>,
866 "id": <id_given_in_input>,
861 "result": {
867 "result": {
862 "pull_request_id": "<int>",
868 "pull_request_id": "<int>",
863 "close_status": "<str:status_lbl>,
869 "close_status": "<str:status_lbl>,
864 "closed": "<bool>"
870 "closed": "<bool>"
865 },
871 },
866 "error": null
872 "error": null
867
873
868 """
874 """
869 _ = request.translate
875 _ = request.translate
870
876
871 pull_request = get_pull_request_or_error(pullrequestid)
877 pull_request = get_pull_request_or_error(pullrequestid)
872 if Optional.extract(repoid):
878 if Optional.extract(repoid):
873 repo = get_repo_or_error(repoid)
879 repo = get_repo_or_error(repoid)
874 else:
880 else:
875 repo = pull_request.target_repo
881 repo = pull_request.target_repo
876
882
877 if not isinstance(userid, Optional):
883 if not isinstance(userid, Optional):
878 if (has_superadmin_permission(apiuser) or
884 if (has_superadmin_permission(apiuser) or
879 HasRepoPermissionAnyApi('repository.admin')(
885 HasRepoPermissionAnyApi('repository.admin')(
880 user=apiuser, repo_name=repo.repo_name)):
886 user=apiuser, repo_name=repo.repo_name)):
881 apiuser = get_user_or_error(userid)
887 apiuser = get_user_or_error(userid)
882 else:
888 else:
883 raise JSONRPCError('userid is not the same as your user')
889 raise JSONRPCError('userid is not the same as your user')
884
890
885 if pull_request.is_closed():
891 if pull_request.is_closed():
886 raise JSONRPCError(
892 raise JSONRPCError(
887 'pull request `%s` is already closed' % (pullrequestid,))
893 'pull request `%s` is already closed' % (pullrequestid,))
888
894
889 # only owner or admin or person with write permissions
895 # only owner or admin or person with write permissions
890 allowed_to_close = PullRequestModel().check_user_update(
896 allowed_to_close = PullRequestModel().check_user_update(
891 pull_request, apiuser, api=True)
897 pull_request, apiuser, api=True)
892
898
893 if not allowed_to_close:
899 if not allowed_to_close:
894 raise JSONRPCError(
900 raise JSONRPCError(
895 'pull request `%s` close failed, no permission to close.' % (
901 'pull request `%s` close failed, no permission to close.' % (
896 pullrequestid,))
902 pullrequestid,))
897
903
898 # 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
899 message = Optional.extract(message)
905 message = Optional.extract(message)
900
906
901 # finally close the PR, with proper message comment
907 # finally close the PR, with proper message comment
902 comment, status = PullRequestModel().close_pull_request_with_comment(
908 comment, status = PullRequestModel().close_pull_request_with_comment(
903 pull_request, apiuser, repo, message=message)
909 pull_request, apiuser, repo, message=message)
904 status_lbl = ChangesetStatus.get_status_lbl(status)
910 status_lbl = ChangesetStatus.get_status_lbl(status)
905
911
906 Session().commit()
912 Session().commit()
907
913
908 data = {
914 data = {
909 'pull_request_id': pull_request.pull_request_id,
915 'pull_request_id': pull_request.pull_request_id,
910 'close_status': status_lbl,
916 'close_status': status_lbl,
911 'closed': True,
917 'closed': True,
912 }
918 }
913 return data
919 return data
General Comments 0
You need to be logged in to leave comments. Login now