##// END OF EJS Templates
api: pull-requests, fixed invocation of merge as another user.
ergo -
r3481:b5202911 default
parent child
Show More
@@ -1,158 +1,259
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import UserLog, PullRequest
24 24 from rhodecode.model.meta import Session
25 25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok)
28 28
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestMergePullRequest(object):
32 32
33 33 @pytest.mark.backends("git", "hg")
34 34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
35 35 pull_request = pr_util.create_pull_request(mergeable=True)
36 36 pull_request_id = pull_request.pull_request_id
37 37 pull_request_repo = pull_request.target_repo.repo_name
38 38
39 39 id_, params = build_data(
40 40 self.apikey, 'merge_pull_request',
41 41 repoid=pull_request_repo,
42 42 pullrequestid=pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 # The above api call detaches the pull request DB object from the
47 47 # session because of an unconditional transaction rollback in our
48 48 # middleware. Therefore we need to add it back here if we want to use it.
49 49 Session().add(pull_request)
50 50
51 51 expected = 'merge not possible for following reasons: ' \
52 52 'Pull request reviewer approval is pending.'
53 53 assert_error(id_, expected, given=response.body)
54 54
55 55 @pytest.mark.backends("git", "hg")
56 56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 57 self, pr_util, no_notifications):
58 58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 59 pull_request_id = pull_request.pull_request_id
60 60 pull_request_repo = pull_request.target_repo.repo_name
61 61
62 62 pr = PullRequest.get(pull_request_id)
63 63 pr.pull_request_state = pull_request.STATE_UPDATING
64 64 Session().add(pr)
65 65 Session().commit()
66 66
67 67 id_, params = build_data(
68 68 self.apikey, 'merge_pull_request',
69 69 repoid=pull_request_repo,
70 70 pullrequestid=pull_request_id)
71 71
72 72 response = api_call(self.app, params)
73 73 expected = 'Operation forbidden because pull request is in state {}, '\
74 74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 75 PullRequest.STATE_CREATED)
76 76 assert_error(id_, expected, given=response.body)
77 77
78 78 @pytest.mark.backends("git", "hg")
79 79 def test_api_merge_pull_request(self, pr_util, no_notifications):
80 80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
81 81 author = pull_request.user_id
82 82 repo = pull_request.target_repo.repo_id
83 83 pull_request_id = pull_request.pull_request_id
84 84 pull_request_repo = pull_request.target_repo.repo_name
85 85
86 86 id_, params = build_data(
87 87 self.apikey, 'comment_pull_request',
88 88 repoid=pull_request_repo,
89 89 pullrequestid=pull_request_id,
90 90 status='approved')
91 91
92 92 response = api_call(self.app, params)
93 93 expected = {
94 94 'comment_id': response.json.get('result', {}).get('comment_id'),
95 95 'pull_request_id': pull_request_id,
96 96 'status': {'given': 'approved', 'was_changed': True}
97 97 }
98 98 assert_ok(id_, expected, given=response.body)
99 99
100 100 id_, params = build_data(
101 101 self.apikey, 'merge_pull_request',
102 102 repoid=pull_request_repo,
103 103 pullrequestid=pull_request_id)
104 104
105 105 response = api_call(self.app, params)
106 106
107 107 pull_request = PullRequest.get(pull_request_id)
108 108
109 109 expected = {
110 110 'executed': True,
111 111 'failure_reason': 0,
112 112 'merge_status_message': 'This pull request can be automatically merged.',
113 113 'possible': True,
114 114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
115 115 'merge_ref': pull_request.shadow_merge_ref._asdict()
116 116 }
117 117
118 118 assert_ok(id_, expected, response.body)
119 119
120 120 journal = UserLog.query()\
121 121 .filter(UserLog.user_id == author)\
122 122 .filter(UserLog.repository_id == repo) \
123 123 .order_by('user_log_id') \
124 124 .all()
125 125 assert journal[-2].action == 'repo.pull_request.merge'
126 126 assert journal[-1].action == 'repo.pull_request.close'
127 127
128 128 id_, params = build_data(
129 129 self.apikey, 'merge_pull_request',
130 130 repoid=pull_request_repo, pullrequestid=pull_request_id)
131 131 response = api_call(self.app, params)
132 132
133 133 expected = 'merge not possible for following reasons: This pull request is closed.'
134 134 assert_error(id_, expected, given=response.body)
135 135
136 136 @pytest.mark.backends("git", "hg")
137 def test_api_merge_pull_request_as_another_user_no_perms_to_merge(
138 self, pr_util, no_notifications, user_util):
139 merge_user = user_util.create_user()
140 merge_user_id = merge_user.user_id
141 merge_user_username = merge_user.username
142
143 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
144
145 pull_request_id = pull_request.pull_request_id
146 pull_request_repo = pull_request.target_repo.repo_name
147
148 id_, params = build_data(
149 self.apikey, 'comment_pull_request',
150 repoid=pull_request_repo,
151 pullrequestid=pull_request_id,
152 status='approved')
153
154 response = api_call(self.app, params)
155 expected = {
156 'comment_id': response.json.get('result', {}).get('comment_id'),
157 'pull_request_id': pull_request_id,
158 'status': {'given': 'approved', 'was_changed': True}
159 }
160 assert_ok(id_, expected, given=response.body)
161 id_, params = build_data(
162 self.apikey, 'merge_pull_request',
163 repoid=pull_request_repo,
164 pullrequestid=pull_request_id,
165 userid=merge_user_id
166 )
167
168 response = api_call(self.app, params)
169 expected = 'merge not possible for following reasons: User `{}` ' \
170 'not allowed to perform merge.'.format(merge_user_username)
171 assert_error(id_, expected, response.body)
172
173 @pytest.mark.backends("git", "hg")
174 def test_api_merge_pull_request_as_another_user(self, pr_util, no_notifications, user_util):
175 merge_user = user_util.create_user()
176 merge_user_id = merge_user.user_id
177 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
178 user_util.grant_user_permission_to_repo(
179 pull_request.target_repo, merge_user, 'repository.write')
180 author = pull_request.user_id
181 repo = pull_request.target_repo.repo_id
182 pull_request_id = pull_request.pull_request_id
183 pull_request_repo = pull_request.target_repo.repo_name
184
185 id_, params = build_data(
186 self.apikey, 'comment_pull_request',
187 repoid=pull_request_repo,
188 pullrequestid=pull_request_id,
189 status='approved')
190
191 response = api_call(self.app, params)
192 expected = {
193 'comment_id': response.json.get('result', {}).get('comment_id'),
194 'pull_request_id': pull_request_id,
195 'status': {'given': 'approved', 'was_changed': True}
196 }
197 assert_ok(id_, expected, given=response.body)
198
199 id_, params = build_data(
200 self.apikey, 'merge_pull_request',
201 repoid=pull_request_repo,
202 pullrequestid=pull_request_id,
203 userid=merge_user_id
204 )
205
206 response = api_call(self.app, params)
207
208 pull_request = PullRequest.get(pull_request_id)
209
210 expected = {
211 'executed': True,
212 'failure_reason': 0,
213 'merge_status_message': 'This pull request can be automatically merged.',
214 'possible': True,
215 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
216 'merge_ref': pull_request.shadow_merge_ref._asdict()
217 }
218
219 assert_ok(id_, expected, response.body)
220
221 journal = UserLog.query() \
222 .filter(UserLog.user_id == merge_user_id) \
223 .filter(UserLog.repository_id == repo) \
224 .order_by('user_log_id') \
225 .all()
226 assert journal[-2].action == 'repo.pull_request.merge'
227 assert journal[-1].action == 'repo.pull_request.close'
228
229 id_, params = build_data(
230 self.apikey, 'merge_pull_request',
231 repoid=pull_request_repo, pullrequestid=pull_request_id, userid=merge_user_id)
232 response = api_call(self.app, params)
233
234 expected = 'merge not possible for following reasons: This pull request is closed.'
235 assert_error(id_, expected, given=response.body)
236
237 @pytest.mark.backends("git", "hg")
137 238 def test_api_merge_pull_request_repo_error(self, pr_util):
138 239 pull_request = pr_util.create_pull_request()
139 240 id_, params = build_data(
140 241 self.apikey, 'merge_pull_request',
141 242 repoid=666, pullrequestid=pull_request.pull_request_id)
142 243 response = api_call(self.app, params)
143 244
144 245 expected = 'repository `666` does not exist'
145 246 assert_error(id_, expected, given=response.body)
146 247
147 248 @pytest.mark.backends("git", "hg")
148 249 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
149 250 pull_request = pr_util.create_pull_request(mergeable=True)
150 251 id_, params = build_data(
151 252 self.apikey_regular, 'merge_pull_request',
152 253 repoid=pull_request.target_repo.repo_name,
153 254 pullrequestid=pull_request.pull_request_id,
154 255 userid=TEST_USER_ADMIN_LOGIN)
155 256 response = api_call(self.app, params)
156 257
157 258 expected = 'userid is not the same as your user'
158 259 assert_error(id_, expected, given=response.body)
@@ -1,996 +1,997
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from rhodecode import events
25 25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 26 from rhodecode.api.utils import (
27 27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 31 from rhodecode.lib.base import vcs_operation_context
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 34 from rhodecode.model.comment import CommentsModel
35 35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 37 from rhodecode.model.settings import SettingsModel
38 38 from rhodecode.model.validation_schema import Invalid
39 39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 40 ReviewerListSchema)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 @jsonrpc_method()
46 46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 47 """
48 48 Get a pull request based on the given ID.
49 49
50 50 :param apiuser: This is filled automatically from the |authtoken|.
51 51 :type apiuser: AuthUser
52 52 :param repoid: Optional, repository name or repository ID from where
53 53 the pull request was opened.
54 54 :type repoid: str or int
55 55 :param pullrequestid: ID of the requested pull request.
56 56 :type pullrequestid: int
57 57
58 58 Example output:
59 59
60 60 .. code-block:: bash
61 61
62 62 "id": <id_given_in_input>,
63 63 "result":
64 64 {
65 65 "pull_request_id": "<pull_request_id>",
66 66 "url": "<url>",
67 67 "title": "<title>",
68 68 "description": "<description>",
69 69 "status" : "<status>",
70 70 "created_on": "<date_time_created>",
71 71 "updated_on": "<date_time_updated>",
72 72 "commit_ids": [
73 73 ...
74 74 "<commit_id>",
75 75 "<commit_id>",
76 76 ...
77 77 ],
78 78 "review_status": "<review_status>",
79 79 "mergeable": {
80 80 "status": "<bool>",
81 81 "message": "<message>",
82 82 },
83 83 "source": {
84 84 "clone_url": "<clone_url>",
85 85 "repository": "<repository_name>",
86 86 "reference":
87 87 {
88 88 "name": "<name>",
89 89 "type": "<type>",
90 90 "commit_id": "<commit_id>",
91 91 }
92 92 },
93 93 "target": {
94 94 "clone_url": "<clone_url>",
95 95 "repository": "<repository_name>",
96 96 "reference":
97 97 {
98 98 "name": "<name>",
99 99 "type": "<type>",
100 100 "commit_id": "<commit_id>",
101 101 }
102 102 },
103 103 "merge": {
104 104 "clone_url": "<clone_url>",
105 105 "reference":
106 106 {
107 107 "name": "<name>",
108 108 "type": "<type>",
109 109 "commit_id": "<commit_id>",
110 110 }
111 111 },
112 112 "author": <user_obj>,
113 113 "reviewers": [
114 114 ...
115 115 {
116 116 "user": "<user_obj>",
117 117 "review_status": "<review_status>",
118 118 }
119 119 ...
120 120 ]
121 121 },
122 122 "error": null
123 123 """
124 124
125 125 pull_request = get_pull_request_or_error(pullrequestid)
126 126 if Optional.extract(repoid):
127 127 repo = get_repo_or_error(repoid)
128 128 else:
129 129 repo = pull_request.target_repo
130 130
131 131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
132 132 raise JSONRPCError('repository `%s` or pull request `%s` '
133 133 'does not exist' % (repoid, pullrequestid))
134 134
135 135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 137 # is happening.
138 138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 139 data = pull_request.get_api_data(with_merge_state=merge_state)
140 140 return data
141 141
142 142
143 143 @jsonrpc_method()
144 144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 145 merge_state=Optional(True)):
146 146 """
147 147 Get all pull requests from the repository specified in `repoid`.
148 148
149 149 :param apiuser: This is filled automatically from the |authtoken|.
150 150 :type apiuser: AuthUser
151 151 :param repoid: Optional repository name or repository ID.
152 152 :type repoid: str or int
153 153 :param status: Only return pull requests with the specified status.
154 154 Valid options are.
155 155 * ``new`` (default)
156 156 * ``open``
157 157 * ``closed``
158 158 :type status: str
159 159 :param merge_state: Optional calculate merge state for each repository.
160 160 This could result in longer time to fetch the data
161 161 :type merge_state: bool
162 162
163 163 Example output:
164 164
165 165 .. code-block:: bash
166 166
167 167 "id": <id_given_in_input>,
168 168 "result":
169 169 [
170 170 ...
171 171 {
172 172 "pull_request_id": "<pull_request_id>",
173 173 "url": "<url>",
174 174 "title" : "<title>",
175 175 "description": "<description>",
176 176 "status": "<status>",
177 177 "created_on": "<date_time_created>",
178 178 "updated_on": "<date_time_updated>",
179 179 "commit_ids": [
180 180 ...
181 181 "<commit_id>",
182 182 "<commit_id>",
183 183 ...
184 184 ],
185 185 "review_status": "<review_status>",
186 186 "mergeable": {
187 187 "status": "<bool>",
188 188 "message: "<message>",
189 189 },
190 190 "source": {
191 191 "clone_url": "<clone_url>",
192 192 "reference":
193 193 {
194 194 "name": "<name>",
195 195 "type": "<type>",
196 196 "commit_id": "<commit_id>",
197 197 }
198 198 },
199 199 "target": {
200 200 "clone_url": "<clone_url>",
201 201 "reference":
202 202 {
203 203 "name": "<name>",
204 204 "type": "<type>",
205 205 "commit_id": "<commit_id>",
206 206 }
207 207 },
208 208 "merge": {
209 209 "clone_url": "<clone_url>",
210 210 "reference":
211 211 {
212 212 "name": "<name>",
213 213 "type": "<type>",
214 214 "commit_id": "<commit_id>",
215 215 }
216 216 },
217 217 "author": <user_obj>,
218 218 "reviewers": [
219 219 ...
220 220 {
221 221 "user": "<user_obj>",
222 222 "review_status": "<review_status>",
223 223 }
224 224 ...
225 225 ]
226 226 }
227 227 ...
228 228 ],
229 229 "error": null
230 230
231 231 """
232 232 repo = get_repo_or_error(repoid)
233 233 if not has_superadmin_permission(apiuser):
234 234 _perms = (
235 235 'repository.admin', 'repository.write', 'repository.read',)
236 236 validate_repo_permissions(apiuser, repoid, repo, _perms)
237 237
238 238 status = Optional.extract(status)
239 239 merge_state = Optional.extract(merge_state, binary=True)
240 240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 241 order_by='id', order_dir='desc')
242 242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
243 243 return data
244 244
245 245
246 246 @jsonrpc_method()
247 247 def merge_pull_request(
248 248 request, apiuser, pullrequestid, repoid=Optional(None),
249 249 userid=Optional(OAttr('apiuser'))):
250 250 """
251 251 Merge the pull request specified by `pullrequestid` into its target
252 252 repository.
253 253
254 254 :param apiuser: This is filled automatically from the |authtoken|.
255 255 :type apiuser: AuthUser
256 256 :param repoid: Optional, repository name or repository ID of the
257 257 target repository to which the |pr| is to be merged.
258 258 :type repoid: str or int
259 259 :param pullrequestid: ID of the pull request which shall be merged.
260 260 :type pullrequestid: int
261 261 :param userid: Merge the pull request as this user.
262 262 :type userid: Optional(str or int)
263 263
264 264 Example output:
265 265
266 266 .. code-block:: bash
267 267
268 268 "id": <id_given_in_input>,
269 269 "result": {
270 270 "executed": "<bool>",
271 271 "failure_reason": "<int>",
272 272 "merge_status_message": "<str>",
273 273 "merge_commit_id": "<merge_commit_id>",
274 274 "possible": "<bool>",
275 275 "merge_ref": {
276 276 "commit_id": "<commit_id>",
277 277 "type": "<type>",
278 278 "name": "<name>"
279 279 }
280 280 },
281 281 "error": null
282 282 """
283 283 pull_request = get_pull_request_or_error(pullrequestid)
284 284 if Optional.extract(repoid):
285 285 repo = get_repo_or_error(repoid)
286 286 else:
287 287 repo = pull_request.target_repo
288
288 auth_user = apiuser
289 289 if not isinstance(userid, Optional):
290 290 if (has_superadmin_permission(apiuser) or
291 291 HasRepoPermissionAnyApi('repository.admin')(
292 292 user=apiuser, repo_name=repo.repo_name)):
293 293 apiuser = get_user_or_error(userid)
294 auth_user = apiuser.AuthUser()
294 295 else:
295 296 raise JSONRPCError('userid is not the same as your user')
296 297
297 298 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
298 299 raise JSONRPCError(
299 300 'Operation forbidden because pull request is in state {}, '
300 301 'only state {} is allowed.'.format(
301 302 pull_request.pull_request_state, PullRequest.STATE_CREATED))
302 303
303 304 with pull_request.set_state(PullRequest.STATE_UPDATING):
304 check = MergeCheck.validate(
305 pull_request, auth_user=apiuser,
306 translator=request.translate)
305 check = MergeCheck.validate(pull_request, auth_user=auth_user,
306 translator=request.translate)
307 307 merge_possible = not check.failed
308 308
309 309 if not merge_possible:
310 310 error_messages = []
311 311 for err_type, error_msg in check.errors:
312 312 error_msg = request.translate(error_msg)
313 313 error_messages.append(error_msg)
314 314
315 315 reasons = ','.join(error_messages)
316 316 raise JSONRPCError(
317 317 'merge not possible for following reasons: {}'.format(reasons))
318 318
319 319 target_repo = pull_request.target_repo
320 320 extras = vcs_operation_context(
321 321 request.environ, repo_name=target_repo.repo_name,
322 username=apiuser.username, action='push',
322 username=auth_user.username, action='push',
323 323 scm=target_repo.repo_type)
324 324 with pull_request.set_state(PullRequest.STATE_UPDATING):
325 325 merge_response = PullRequestModel().merge_repo(
326 326 pull_request, apiuser, extras=extras)
327 327 if merge_response.executed:
328 PullRequestModel().close_pull_request(
329 pull_request.pull_request_id, apiuser)
328 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
330 329
331 330 Session().commit()
332 331
333 332 # In previous versions the merge response directly contained the merge
334 333 # commit id. It is now contained in the merge reference object. To be
335 334 # backwards compatible we have to extract it again.
336 335 merge_response = merge_response.asdict()
337 336 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
338 337
339 338 return merge_response
340 339
341 340
342 341 @jsonrpc_method()
343 342 def get_pull_request_comments(
344 343 request, apiuser, pullrequestid, repoid=Optional(None)):
345 344 """
346 345 Get all comments of pull request specified with the `pullrequestid`
347 346
348 347 :param apiuser: This is filled automatically from the |authtoken|.
349 348 :type apiuser: AuthUser
350 349 :param repoid: Optional repository name or repository ID.
351 350 :type repoid: str or int
352 351 :param pullrequestid: The pull request ID.
353 352 :type pullrequestid: int
354 353
355 354 Example output:
356 355
357 356 .. code-block:: bash
358 357
359 358 id : <id_given_in_input>
360 359 result : [
361 360 {
362 361 "comment_author": {
363 362 "active": true,
364 363 "full_name_or_username": "Tom Gore",
365 364 "username": "admin"
366 365 },
367 366 "comment_created_on": "2017-01-02T18:43:45.533",
368 367 "comment_f_path": null,
369 368 "comment_id": 25,
370 369 "comment_lineno": null,
371 370 "comment_status": {
372 371 "status": "under_review",
373 372 "status_lbl": "Under Review"
374 373 },
375 374 "comment_text": "Example text",
376 375 "comment_type": null,
377 376 "pull_request_version": null
378 377 }
379 378 ],
380 379 error : null
381 380 """
382 381
383 382 pull_request = get_pull_request_or_error(pullrequestid)
384 383 if Optional.extract(repoid):
385 384 repo = get_repo_or_error(repoid)
386 385 else:
387 386 repo = pull_request.target_repo
388 387
389 388 if not PullRequestModel().check_user_read(
390 389 pull_request, apiuser, api=True):
391 390 raise JSONRPCError('repository `%s` or pull request `%s` '
392 391 'does not exist' % (repoid, pullrequestid))
393 392
394 393 (pull_request_latest,
395 394 pull_request_at_ver,
396 395 pull_request_display_obj,
397 396 at_version) = PullRequestModel().get_pr_version(
398 397 pull_request.pull_request_id, version=None)
399 398
400 399 versions = pull_request_display_obj.versions()
401 400 ver_map = {
402 401 ver.pull_request_version_id: cnt
403 402 for cnt, ver in enumerate(versions, 1)
404 403 }
405 404
406 405 # GENERAL COMMENTS with versions #
407 406 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
408 407 q = q.order_by(ChangesetComment.comment_id.asc())
409 408 general_comments = q.all()
410 409
411 410 # INLINE COMMENTS with versions #
412 411 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
413 412 q = q.order_by(ChangesetComment.comment_id.asc())
414 413 inline_comments = q.all()
415 414
416 415 data = []
417 416 for comment in inline_comments + general_comments:
418 417 full_data = comment.get_api_data()
419 418 pr_version_id = None
420 419 if comment.pull_request_version_id:
421 420 pr_version_id = 'v{}'.format(
422 421 ver_map[comment.pull_request_version_id])
423 422
424 423 # sanitize some entries
425 424
426 425 full_data['pull_request_version'] = pr_version_id
427 426 full_data['comment_author'] = {
428 427 'username': full_data['comment_author'].username,
429 428 'full_name_or_username': full_data['comment_author'].full_name_or_username,
430 429 'active': full_data['comment_author'].active,
431 430 }
432 431
433 432 if full_data['comment_status']:
434 433 full_data['comment_status'] = {
435 434 'status': full_data['comment_status'][0].status,
436 435 'status_lbl': full_data['comment_status'][0].status_lbl,
437 436 }
438 437 else:
439 438 full_data['comment_status'] = {}
440 439
441 440 data.append(full_data)
442 441 return data
443 442
444 443
445 444 @jsonrpc_method()
446 445 def comment_pull_request(
447 446 request, apiuser, pullrequestid, repoid=Optional(None),
448 447 message=Optional(None), commit_id=Optional(None), status=Optional(None),
449 448 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
450 449 resolves_comment_id=Optional(None),
451 450 userid=Optional(OAttr('apiuser'))):
452 451 """
453 452 Comment on the pull request specified with the `pullrequestid`,
454 453 in the |repo| specified by the `repoid`, and optionally change the
455 454 review status.
456 455
457 456 :param apiuser: This is filled automatically from the |authtoken|.
458 457 :type apiuser: AuthUser
459 458 :param repoid: Optional repository name or repository ID.
460 459 :type repoid: str or int
461 460 :param pullrequestid: The pull request ID.
462 461 :type pullrequestid: int
463 462 :param commit_id: Specify the commit_id for which to set a comment. If
464 463 given commit_id is different than latest in the PR status
465 464 change won't be performed.
466 465 :type commit_id: str
467 466 :param message: The text content of the comment.
468 467 :type message: str
469 468 :param status: (**Optional**) Set the approval status of the pull
470 469 request. One of: 'not_reviewed', 'approved', 'rejected',
471 470 'under_review'
472 471 :type status: str
473 472 :param comment_type: Comment type, one of: 'note', 'todo'
474 473 :type comment_type: Optional(str), default: 'note'
475 474 :param userid: Comment on the pull request as this user
476 475 :type userid: Optional(str or int)
477 476
478 477 Example output:
479 478
480 479 .. code-block:: bash
481 480
482 481 id : <id_given_in_input>
483 482 result : {
484 483 "pull_request_id": "<Integer>",
485 484 "comment_id": "<Integer>",
486 485 "status": {"given": <given_status>,
487 486 "was_changed": <bool status_was_actually_changed> },
488 487 },
489 488 error : null
490 489 """
491 490 pull_request = get_pull_request_or_error(pullrequestid)
492 491 if Optional.extract(repoid):
493 492 repo = get_repo_or_error(repoid)
494 493 else:
495 494 repo = pull_request.target_repo
496 495
496 auth_user = apiuser
497 497 if not isinstance(userid, Optional):
498 498 if (has_superadmin_permission(apiuser) or
499 499 HasRepoPermissionAnyApi('repository.admin')(
500 500 user=apiuser, repo_name=repo.repo_name)):
501 501 apiuser = get_user_or_error(userid)
502 auth_user = apiuser.AuthUser()
502 503 else:
503 504 raise JSONRPCError('userid is not the same as your user')
504 505
505 506 if pull_request.is_closed():
506 507 raise JSONRPCError(
507 508 'pull request `%s` comment failed, pull request is closed' % (
508 509 pullrequestid,))
509 510
510 511 if not PullRequestModel().check_user_read(
511 512 pull_request, apiuser, api=True):
512 513 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
513 514 message = Optional.extract(message)
514 515 status = Optional.extract(status)
515 516 commit_id = Optional.extract(commit_id)
516 517 comment_type = Optional.extract(comment_type)
517 518 resolves_comment_id = Optional.extract(resolves_comment_id)
518 519
519 520 if not message and not status:
520 521 raise JSONRPCError(
521 522 'Both message and status parameters are missing. '
522 523 'At least one is required.')
523 524
524 525 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
525 526 status is not None):
526 527 raise JSONRPCError('Unknown comment status: `%s`' % status)
527 528
528 529 if commit_id and commit_id not in pull_request.revisions:
529 530 raise JSONRPCError(
530 531 'Invalid commit_id `%s` for this pull request.' % commit_id)
531 532
532 533 allowed_to_change_status = PullRequestModel().check_user_change_status(
533 534 pull_request, apiuser)
534 535
535 536 # if commit_id is passed re-validated if user is allowed to change status
536 537 # based on latest commit_id from the PR
537 538 if commit_id:
538 539 commit_idx = pull_request.revisions.index(commit_id)
539 540 if commit_idx != 0:
540 541 allowed_to_change_status = False
541 542
542 543 if resolves_comment_id:
543 544 comment = ChangesetComment.get(resolves_comment_id)
544 545 if not comment:
545 546 raise JSONRPCError(
546 547 'Invalid resolves_comment_id `%s` for this pull request.'
547 548 % resolves_comment_id)
548 549 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
549 550 raise JSONRPCError(
550 551 'Comment `%s` is wrong type for setting status to resolved.'
551 552 % resolves_comment_id)
552 553
553 554 text = message
554 555 status_label = ChangesetStatus.get_status_lbl(status)
555 556 if status and allowed_to_change_status:
556 557 st_message = ('Status change %(transition_icon)s %(status)s'
557 558 % {'transition_icon': '>', 'status': status_label})
558 559 text = message or st_message
559 560
560 561 rc_config = SettingsModel().get_all_settings()
561 562 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
562 563
563 564 status_change = status and allowed_to_change_status
564 565 comment = CommentsModel().create(
565 566 text=text,
566 567 repo=pull_request.target_repo.repo_id,
567 568 user=apiuser.user_id,
568 569 pull_request=pull_request.pull_request_id,
569 570 f_path=None,
570 571 line_no=None,
571 572 status_change=(status_label if status_change else None),
572 573 status_change_type=(status if status_change else None),
573 574 closing_pr=False,
574 575 renderer=renderer,
575 576 comment_type=comment_type,
576 577 resolves_comment_id=resolves_comment_id,
577 auth_user=apiuser
578 auth_user=auth_user
578 579 )
579 580
580 581 if allowed_to_change_status and status:
581 582 old_calculated_status = pull_request.calculated_review_status()
582 583 ChangesetStatusModel().set_status(
583 584 pull_request.target_repo.repo_id,
584 585 status,
585 586 apiuser.user_id,
586 587 comment,
587 588 pull_request=pull_request.pull_request_id
588 589 )
589 590 Session().flush()
590 591
591 592 Session().commit()
592 593
593 594 PullRequestModel().trigger_pull_request_hook(
594 595 pull_request, apiuser, 'comment',
595 596 data={'comment': comment})
596 597
597 598 if allowed_to_change_status and status:
598 599 # we now calculate the status of pull request, and based on that
599 600 # calculation we set the commits status
600 601 calculated_status = pull_request.calculated_review_status()
601 602 if old_calculated_status != calculated_status:
602 603 PullRequestModel().trigger_pull_request_hook(
603 604 pull_request, apiuser, 'review_status_change',
604 605 data={'status': calculated_status})
605 606
606 607 data = {
607 608 'pull_request_id': pull_request.pull_request_id,
608 609 'comment_id': comment.comment_id if comment else None,
609 610 'status': {'given': status, 'was_changed': status_change},
610 611 }
611 612 return data
612 613
613 614
614 615 @jsonrpc_method()
615 616 def create_pull_request(
616 617 request, apiuser, source_repo, target_repo, source_ref, target_ref,
617 618 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
618 619 description_renderer=Optional(''), reviewers=Optional(None)):
619 620 """
620 621 Creates a new pull request.
621 622
622 623 Accepts refs in the following formats:
623 624
624 625 * branch:<branch_name>:<sha>
625 626 * branch:<branch_name>
626 627 * bookmark:<bookmark_name>:<sha> (Mercurial only)
627 628 * bookmark:<bookmark_name> (Mercurial only)
628 629
629 630 :param apiuser: This is filled automatically from the |authtoken|.
630 631 :type apiuser: AuthUser
631 632 :param source_repo: Set the source repository name.
632 633 :type source_repo: str
633 634 :param target_repo: Set the target repository name.
634 635 :type target_repo: str
635 636 :param source_ref: Set the source ref name.
636 637 :type source_ref: str
637 638 :param target_ref: Set the target ref name.
638 639 :type target_ref: str
639 640 :param owner: user_id or username
640 641 :type owner: Optional(str)
641 642 :param title: Optionally Set the pull request title, it's generated otherwise
642 643 :type title: str
643 644 :param description: Set the pull request description.
644 645 :type description: Optional(str)
645 646 :type description_renderer: Optional(str)
646 647 :param description_renderer: Set pull request renderer for the description.
647 648 It should be 'rst', 'markdown' or 'plain'. If not give default
648 649 system renderer will be used
649 650 :param reviewers: Set the new pull request reviewers list.
650 651 Reviewer defined by review rules will be added automatically to the
651 652 defined list.
652 653 :type reviewers: Optional(list)
653 654 Accepts username strings or objects of the format:
654 655
655 656 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
656 657 """
657 658
658 659 source_db_repo = get_repo_or_error(source_repo)
659 660 target_db_repo = get_repo_or_error(target_repo)
660 661 if not has_superadmin_permission(apiuser):
661 662 _perms = ('repository.admin', 'repository.write', 'repository.read',)
662 663 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
663 664
664 665 owner = validate_set_owner_permissions(apiuser, owner)
665 666
666 667 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
667 668 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
668 669
669 670 source_scm = source_db_repo.scm_instance()
670 671