##// END OF EJS Templates
api: pull-requests added option to fetch comments from a pull requests....
marcink -
r2394:7f509c97 default
parent child Browse files
Show More
@@ -0,0 +1,82 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import pytest
23 import urlobject
24
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok)
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.utils2 import safe_unicode
29
30 pytestmark = pytest.mark.backends("git", "hg")
31
32
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequestComments(object):
35
36 def test_api_get_pull_request_comments(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
38
39 pull_request = pr_util.create_pull_request(mergeable=True)
40 id_, params = build_data(
41 self.apikey, 'get_pull_request_comments',
42 pullrequestid=pull_request.pull_request_id)
43
44 response = api_call(self.app, params)
45
46 assert response.status == '200 OK'
47 resp_date = response.json['result'][0]['comment_created_on']
48 resp_comment_id = response.json['result'][0]['comment_id']
49
50 expected = [
51 {'comment_author': {'active': True,
52 'full_name_or_username': 'RhodeCode Admin',
53 'username': 'test_admin'},
54 'comment_created_on': resp_date,
55 'comment_f_path': None,
56 'comment_id': resp_comment_id,
57 'comment_lineno': None,
58 'comment_status': {'status': 'under_review',
59 'status_lbl': 'Under Review'},
60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
61 'comment_type': 'note',
62 'pull_request_version': None}
63 ]
64 assert_ok(id_, expected, response.body)
65
66 def test_api_get_pull_request_comments_repo_error(self, pr_util):
67 pull_request = pr_util.create_pull_request()
68 id_, params = build_data(
69 self.apikey, 'get_pull_request_comments',
70 repoid=666, pullrequestid=pull_request.pull_request_id)
71 response = api_call(self.app, params)
72
73 expected = 'repository `666` does not exist'
74 assert_error(id_, expected, given=response.body)
75
76 def test_api_get_pull_request_comments_pull_request_error(self):
77 id_, params = build_data(
78 self.apikey, 'get_pull_request_comments', pullrequestid=666)
79 response = api_call(self.app, params)
80
81 expected = 'pull request `666` does not exist'
82 assert_error(id_, expected, given=response.body)
@@ -1,780 +1,883 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 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)
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
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, repoid, pullrequestid):
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: Repository name or repository ID from where the pull
53 53 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 get_repo_or_error(repoid)
125 125 pull_request = get_pull_request_or_error(pullrequestid)
126 126 if not PullRequestModel().check_user_read(
127 127 pull_request, apiuser, api=True):
128 128 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
129 129 data = pull_request.get_api_data()
130 130 return data
131 131
132 132
133 133 @jsonrpc_method()
134 134 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
135 135 """
136 136 Get all pull requests from the repository specified in `repoid`.
137 137
138 138 :param apiuser: This is filled automatically from the |authtoken|.
139 139 :type apiuser: AuthUser
140 140 :param repoid: Repository name or repository ID.
141 141 :type repoid: str or int
142 142 :param status: Only return pull requests with the specified status.
143 143 Valid options are.
144 144 * ``new`` (default)
145 145 * ``open``
146 146 * ``closed``
147 147 :type status: str
148 148
149 149 Example output:
150 150
151 151 .. code-block:: bash
152 152
153 153 "id": <id_given_in_input>,
154 154 "result":
155 155 [
156 156 ...
157 157 {
158 158 "pull_request_id": "<pull_request_id>",
159 159 "url": "<url>",
160 160 "title" : "<title>",
161 161 "description": "<description>",
162 162 "status": "<status>",
163 163 "created_on": "<date_time_created>",
164 164 "updated_on": "<date_time_updated>",
165 165 "commit_ids": [
166 166 ...
167 167 "<commit_id>",
168 168 "<commit_id>",
169 169 ...
170 170 ],
171 171 "review_status": "<review_status>",
172 172 "mergeable": {
173 173 "status": "<bool>",
174 174 "message: "<message>",
175 175 },
176 176 "source": {
177 177 "clone_url": "<clone_url>",
178 178 "reference":
179 179 {
180 180 "name": "<name>",
181 181 "type": "<type>",
182 182 "commit_id": "<commit_id>",
183 183 }
184 184 },
185 185 "target": {
186 186 "clone_url": "<clone_url>",
187 187 "reference":
188 188 {
189 189 "name": "<name>",
190 190 "type": "<type>",
191 191 "commit_id": "<commit_id>",
192 192 }
193 193 },
194 194 "merge": {
195 195 "clone_url": "<clone_url>",
196 196 "reference":
197 197 {
198 198 "name": "<name>",
199 199 "type": "<type>",
200 200 "commit_id": "<commit_id>",
201 201 }
202 202 },
203 203 "author": <user_obj>,
204 204 "reviewers": [
205 205 ...
206 206 {
207 207 "user": "<user_obj>",
208 208 "review_status": "<review_status>",
209 209 }
210 210 ...
211 211 ]
212 212 }
213 213 ...
214 214 ],
215 215 "error": null
216 216
217 217 """
218 218 repo = get_repo_or_error(repoid)
219 219 if not has_superadmin_permission(apiuser):
220 220 _perms = (
221 221 'repository.admin', 'repository.write', 'repository.read',)
222 222 validate_repo_permissions(apiuser, repoid, repo, _perms)
223 223
224 224 status = Optional.extract(status)
225 225 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
226 226 data = [pr.get_api_data() for pr in pull_requests]
227 227 return data
228 228
229 229
230 230 @jsonrpc_method()
231 231 def merge_pull_request(
232 232 request, apiuser, repoid, pullrequestid,
233 233 userid=Optional(OAttr('apiuser'))):
234 234 """
235 235 Merge the pull request specified by `pullrequestid` into its target
236 236 repository.
237 237
238 238 :param apiuser: This is filled automatically from the |authtoken|.
239 239 :type apiuser: AuthUser
240 240 :param repoid: The Repository name or repository ID of the
241 241 target repository to which the |pr| is to be merged.
242 242 :type repoid: str or int
243 243 :param pullrequestid: ID of the pull request which shall be merged.
244 244 :type pullrequestid: int
245 245 :param userid: Merge the pull request as this user.
246 246 :type userid: Optional(str or int)
247 247
248 248 Example output:
249 249
250 250 .. code-block:: bash
251 251
252 252 "id": <id_given_in_input>,
253 253 "result": {
254 254 "executed": "<bool>",
255 255 "failure_reason": "<int>",
256 256 "merge_commit_id": "<merge_commit_id>",
257 257 "possible": "<bool>",
258 258 "merge_ref": {
259 259 "commit_id": "<commit_id>",
260 260 "type": "<type>",
261 261 "name": "<name>"
262 262 }
263 263 },
264 264 "error": null
265 265 """
266 266 repo = get_repo_or_error(repoid)
267 267 if not isinstance(userid, Optional):
268 268 if (has_superadmin_permission(apiuser) or
269 269 HasRepoPermissionAnyApi('repository.admin')(
270 270 user=apiuser, repo_name=repo.repo_name)):
271 271 apiuser = get_user_or_error(userid)
272 272 else:
273 273 raise JSONRPCError('userid is not the same as your user')
274 274
275 275 pull_request = get_pull_request_or_error(pullrequestid)
276 276
277 277 check = MergeCheck.validate(
278 278 pull_request, user=apiuser, translator=request.translate)
279 279 merge_possible = not check.failed
280 280
281 281 if not merge_possible:
282 282 error_messages = []
283 283 for err_type, error_msg in check.errors:
284 284 error_msg = request.translate(error_msg)
285 285 error_messages.append(error_msg)
286 286
287 287 reasons = ','.join(error_messages)
288 288 raise JSONRPCError(
289 289 'merge not possible for following reasons: {}'.format(reasons))
290 290
291 291 target_repo = pull_request.target_repo
292 292 extras = vcs_operation_context(
293 293 request.environ, repo_name=target_repo.repo_name,
294 294 username=apiuser.username, action='push',
295 295 scm=target_repo.repo_type)
296 296 merge_response = PullRequestModel().merge(
297 297 pull_request, apiuser, extras=extras)
298 298 if merge_response.executed:
299 299 PullRequestModel().close_pull_request(
300 300 pull_request.pull_request_id, apiuser)
301 301
302 302 Session().commit()
303 303
304 304 # In previous versions the merge response directly contained the merge
305 305 # commit id. It is now contained in the merge reference object. To be
306 306 # backwards compatible we have to extract it again.
307 307 merge_response = merge_response._asdict()
308 308 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
309 309
310 310 return merge_response
311 311
312 312
313 313 @jsonrpc_method()
314 def get_pull_request_comments(
315 request, apiuser, pullrequestid, repoid=Optional(None)):
316 """
317 Get all comments of pull request specified with the `pullrequestid`
318
319 :param apiuser: This is filled automatically from the |authtoken|.
320 :type apiuser: AuthUser
321 :param repoid: Optional repository name or repository ID.
322 :type repoid: str or int
323 :param pullrequestid: The pull request ID.
324 :type pullrequestid: int
325
326 Example output:
327
328 .. code-block:: bash
329
330 id : <id_given_in_input>
331 result : [
332 {
333 "comment_author": {
334 "active": true,
335 "full_name_or_username": "Tom Gore",
336 "username": "admin"
337 },
338 "comment_created_on": "2017-01-02T18:43:45.533",
339 "comment_f_path": null,
340 "comment_id": 25,
341 "comment_lineno": null,
342 "comment_status": {
343 "status": "under_review",
344 "status_lbl": "Under Review"
345 },
346 "comment_text": "Example text",
347 "comment_type": null,
348 "pull_request_version": null
349 }
350 ],
351 error : null
352 """
353
354 pull_request = get_pull_request_or_error(pullrequestid)
355 if Optional.extract(repoid):
356 repo = get_repo_or_error(repoid)
357 else:
358 repo = pull_request.target_repo
359
360 if not PullRequestModel().check_user_read(
361 pull_request, apiuser, api=True):
362 raise JSONRPCError('repository `%s` or pull request `%s` '
363 'does not exist' % (repoid, pullrequestid))
364
365 (pull_request_latest,
366 pull_request_at_ver,
367 pull_request_display_obj,
368 at_version) = PullRequestModel().get_pr_version(
369 pull_request.pull_request_id, version=None)
370
371 versions = pull_request_display_obj.versions()
372 ver_map = {
373 ver.pull_request_version_id: cnt
374 for cnt, ver in enumerate(versions, 1)
375 }
376
377 # GENERAL COMMENTS with versions #
378 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
379 q = q.order_by(ChangesetComment.comment_id.asc())
380 general_comments = q.all()
381
382 # INLINE COMMENTS with versions #
383 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
384 q = q.order_by(ChangesetComment.comment_id.asc())
385 inline_comments = q.all()
386
387 data = []
388 for comment in inline_comments + general_comments:
389 full_data = comment.get_api_data()
390 pr_version_id = None
391 if comment.pull_request_version_id:
392 pr_version_id = 'v{}'.format(
393 ver_map[comment.pull_request_version_id])
394
395 # sanitize some entries
396
397 full_data['pull_request_version'] = pr_version_id
398 full_data['comment_author'] = {
399 'username': full_data['comment_author'].username,
400 'full_name_or_username': full_data['comment_author'].full_name_or_username,
401 'active': full_data['comment_author'].active,
402 }
403
404 if full_data['comment_status']:
405 full_data['comment_status'] = {
406 'status': full_data['comment_status'][0].status,
407 'status_lbl': full_data['comment_status'][0].status_lbl,
408 }
409 else:
410 full_data['comment_status'] = {}
411
412 data.append(full_data)
413 return data
414
415
416 @jsonrpc_method()
314 417 def comment_pull_request(
315 418 request, apiuser, repoid, pullrequestid, message=Optional(None),
316 419 commit_id=Optional(None), status=Optional(None),
317 420 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
318 421 resolves_comment_id=Optional(None),
319 422 userid=Optional(OAttr('apiuser'))):
320 423 """
321 424 Comment on the pull request specified with the `pullrequestid`,
322 425 in the |repo| specified by the `repoid`, and optionally change the
323 426 review status.
324 427
325 428 :param apiuser: This is filled automatically from the |authtoken|.
326 429 :type apiuser: AuthUser
327 430 :param repoid: The repository name or repository ID.
328 431 :type repoid: str or int
329 432 :param pullrequestid: The pull request ID.
330 433 :type pullrequestid: int
331 434 :param commit_id: Specify the commit_id for which to set a comment. If
332 435 given commit_id is different than latest in the PR status
333 436 change won't be performed.
334 437 :type commit_id: str
335 438 :param message: The text content of the comment.
336 439 :type message: str
337 440 :param status: (**Optional**) Set the approval status of the pull
338 441 request. One of: 'not_reviewed', 'approved', 'rejected',
339 442 'under_review'
340 443 :type status: str
341 444 :param comment_type: Comment type, one of: 'note', 'todo'
342 445 :type comment_type: Optional(str), default: 'note'
343 446 :param userid: Comment on the pull request as this user
344 447 :type userid: Optional(str or int)
345 448
346 449 Example output:
347 450
348 451 .. code-block:: bash
349 452
350 453 id : <id_given_in_input>
351 454 result : {
352 455 "pull_request_id": "<Integer>",
353 456 "comment_id": "<Integer>",
354 457 "status": {"given": <given_status>,
355 458 "was_changed": <bool status_was_actually_changed> },
356 459 },
357 460 error : null
358 461 """
359 462 repo = get_repo_or_error(repoid)
360 463 if not isinstance(userid, Optional):
361 464 if (has_superadmin_permission(apiuser) or
362 465 HasRepoPermissionAnyApi('repository.admin')(
363 466 user=apiuser, repo_name=repo.repo_name)):
364 467 apiuser = get_user_or_error(userid)
365 468 else:
366 469 raise JSONRPCError('userid is not the same as your user')
367 470
368 471 pull_request = get_pull_request_or_error(pullrequestid)
369 472 if not PullRequestModel().check_user_read(
370 473 pull_request, apiuser, api=True):
371 474 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
372 475 message = Optional.extract(message)
373 476 status = Optional.extract(status)
374 477 commit_id = Optional.extract(commit_id)
375 478 comment_type = Optional.extract(comment_type)
376 479 resolves_comment_id = Optional.extract(resolves_comment_id)
377 480
378 481 if not message and not status:
379 482 raise JSONRPCError(
380 483 'Both message and status parameters are missing. '
381 484 'At least one is required.')
382 485
383 486 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
384 487 status is not None):
385 488 raise JSONRPCError('Unknown comment status: `%s`' % status)
386 489
387 490 if commit_id and commit_id not in pull_request.revisions:
388 491 raise JSONRPCError(
389 492 'Invalid commit_id `%s` for this pull request.' % commit_id)
390 493
391 494 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 495 pull_request, apiuser)
393 496
394 497 # if commit_id is passed re-validated if user is allowed to change status
395 498 # based on latest commit_id from the PR
396 499 if commit_id:
397 500 commit_idx = pull_request.revisions.index(commit_id)
398 501 if commit_idx != 0:
399 502 allowed_to_change_status = False
400 503
401 504 if resolves_comment_id:
402 505 comment = ChangesetComment.get(resolves_comment_id)
403 506 if not comment:
404 507 raise JSONRPCError(
405 508 'Invalid resolves_comment_id `%s` for this pull request.'
406 509 % resolves_comment_id)
407 510 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
408 511 raise JSONRPCError(
409 512 'Comment `%s` is wrong type for setting status to resolved.'
410 513 % resolves_comment_id)
411 514
412 515 text = message
413 516 status_label = ChangesetStatus.get_status_lbl(status)
414 517 if status and allowed_to_change_status:
415 518 st_message = ('Status change %(transition_icon)s %(status)s'
416 519 % {'transition_icon': '>', 'status': status_label})
417 520 text = message or st_message
418 521
419 522 rc_config = SettingsModel().get_all_settings()
420 523 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
421 524
422 525 status_change = status and allowed_to_change_status
423 526 comment = CommentsModel().create(
424 527 text=text,
425 528 repo=pull_request.target_repo.repo_id,
426 529 user=apiuser.user_id,
427 530 pull_request=pull_request.pull_request_id,
428 531 f_path=None,
429 532 line_no=None,
430 533 status_change=(status_label if status_change else None),
431 534 status_change_type=(status if status_change else None),
432 535 closing_pr=False,
433 536 renderer=renderer,
434 537 comment_type=comment_type,
435 538 resolves_comment_id=resolves_comment_id
436 539 )
437 540
438 541 if allowed_to_change_status and status:
439 542 ChangesetStatusModel().set_status(
440 543 pull_request.target_repo.repo_id,
441 544 status,
442 545 apiuser.user_id,
443 546 comment,
444 547 pull_request=pull_request.pull_request_id
445 548 )
446 549 Session().flush()
447 550
448 551 Session().commit()
449 552 data = {
450 553 'pull_request_id': pull_request.pull_request_id,
451 554 'comment_id': comment.comment_id if comment else None,
452 555 'status': {'given': status, 'was_changed': status_change},
453 556 }
454 557 return data
455 558
456 559
457 560 @jsonrpc_method()
458 561 def create_pull_request(
459 562 request, apiuser, source_repo, target_repo, source_ref, target_ref,
460 563 title, description=Optional(''), reviewers=Optional(None)):
461 564 """
462 565 Creates a new pull request.
463 566
464 567 Accepts refs in the following formats:
465 568
466 569 * branch:<branch_name>:<sha>
467 570 * branch:<branch_name>
468 571 * bookmark:<bookmark_name>:<sha> (Mercurial only)
469 572 * bookmark:<bookmark_name> (Mercurial only)
470 573
471 574 :param apiuser: This is filled automatically from the |authtoken|.
472 575 :type apiuser: AuthUser
473 576 :param source_repo: Set the source repository name.
474 577 :type source_repo: str
475 578 :param target_repo: Set the target repository name.
476 579 :type target_repo: str
477 580 :param source_ref: Set the source ref name.
478 581 :type source_ref: str
479 582 :param target_ref: Set the target ref name.
480 583 :type target_ref: str
481 584 :param title: Set the pull request title.
482 585 :type title: str
483 586 :param description: Set the pull request description.
484 587 :type description: Optional(str)
485 588 :param reviewers: Set the new pull request reviewers list.
486 589 Reviewer defined by review rules will be added automatically to the
487 590 defined list.
488 591 :type reviewers: Optional(list)
489 592 Accepts username strings or objects of the format:
490 593
491 594 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
492 595 """
493 596
494 597 source_db_repo = get_repo_or_error(source_repo)
495 598 target_db_repo = get_repo_or_error(target_repo)
496 599 if not has_superadmin_permission(apiuser):
497 600 _perms = ('repository.admin', 'repository.write', 'repository.read',)
498 601 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
499 602
500 603 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
501 604 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
502 605 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
503 606 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
504 607 source_scm = source_db_repo.scm_instance()
505 608 target_scm = target_db_repo.scm_instance()
506 609
507 610 commit_ranges = target_scm.compare(
508 611 target_commit.raw_id, source_commit.raw_id, source_scm,
509 612 merge=True, pre_load=[])
510 613
511 614 ancestor = target_scm.get_common_ancestor(
512 615 target_commit.raw_id, source_commit.raw_id, source_scm)
513 616
514 617 if not commit_ranges:
515 618 raise JSONRPCError('no commits found')
516 619
517 620 if not ancestor:
518 621 raise JSONRPCError('no common ancestor found')
519 622
520 623 reviewer_objects = Optional.extract(reviewers) or []
521 624
522 625 if reviewer_objects:
523 626 schema = ReviewerListSchema()
524 627 try:
525 628 reviewer_objects = schema.deserialize(reviewer_objects)
526 629 except Invalid as err:
527 630 raise JSONRPCValidationError(colander_exc=err)
528 631
529 632 # validate users
530 633 for reviewer_object in reviewer_objects:
531 634 user = get_user_or_error(reviewer_object['username'])
532 635 reviewer_object['user_id'] = user.user_id
533 636
534 637 get_default_reviewers_data, get_validated_reviewers = \
535 638 PullRequestModel().get_reviewer_functions()
536 639
537 640 reviewer_rules = get_default_reviewers_data(
538 641 apiuser.get_instance(), source_db_repo,
539 642 source_commit, target_db_repo, target_commit)
540 643
541 644 # specified rules are later re-validated, thus we can assume users will
542 645 # eventually provide those that meet the reviewer criteria.
543 646 if not reviewer_objects:
544 647 reviewer_objects = reviewer_rules['reviewers']
545 648
546 649 try:
547 650 reviewers = get_validated_reviewers(
548 651 reviewer_objects, reviewer_rules)
549 652 except ValueError as e:
550 653 raise JSONRPCError('Reviewers Validation: {}'.format(e))
551 654
552 655 pull_request_model = PullRequestModel()
553 656 pull_request = pull_request_model.create(
554 657 created_by=apiuser.user_id,
555 658 source_repo=source_repo,
556 659 source_ref=full_source_ref,
557 660 target_repo=target_repo,
558 661 target_ref=full_target_ref,
559 662 revisions=reversed(
560 663 [commit.raw_id for commit in reversed(commit_ranges)]),
561 664 reviewers=reviewers,
562 665 title=title,
563 666 description=Optional.extract(description)
564 667 )
565 668
566 669 Session().commit()
567 670 data = {
568 671 'msg': 'Created new pull request `{}`'.format(title),
569 672 'pull_request_id': pull_request.pull_request_id,
570 673 }
571 674 return data
572 675
573 676
574 677 @jsonrpc_method()
575 678 def update_pull_request(
576 679 request, apiuser, repoid, pullrequestid, title=Optional(''),
577 680 description=Optional(''), reviewers=Optional(None),
578 681 update_commits=Optional(None)):
579 682 """
580 683 Updates a pull request.
581 684
582 685 :param apiuser: This is filled automatically from the |authtoken|.
583 686 :type apiuser: AuthUser
584 687 :param repoid: The repository name or repository ID.
585 688 :type repoid: str or int
586 689 :param pullrequestid: The pull request ID.
587 690 :type pullrequestid: int
588 691 :param title: Set the pull request title.
589 692 :type title: str
590 693 :param description: Update pull request description.
591 694 :type description: Optional(str)
592 695 :param reviewers: Update pull request reviewers list with new value.
593 696 :type reviewers: Optional(list)
594 697 Accepts username strings or objects of the format:
595 698
596 699 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
597 700
598 701 :param update_commits: Trigger update of commits for this pull request
599 702 :type: update_commits: Optional(bool)
600 703
601 704 Example output:
602 705
603 706 .. code-block:: bash
604 707
605 708 id : <id_given_in_input>
606 709 result : {
607 710 "msg": "Updated pull request `63`",
608 711 "pull_request": <pull_request_object>,
609 712 "updated_reviewers": {
610 713 "added": [
611 714 "username"
612 715 ],
613 716 "removed": []
614 717 },
615 718 "updated_commits": {
616 719 "added": [
617 720 "<sha1_hash>"
618 721 ],
619 722 "common": [
620 723 "<sha1_hash>",
621 724 "<sha1_hash>",
622 725 ],
623 726 "removed": []
624 727 }
625 728 }
626 729 error : null
627 730 """
628 731
629 732 repo = get_repo_or_error(repoid)
630 733 pull_request = get_pull_request_or_error(pullrequestid)
631 734 if not PullRequestModel().check_user_update(
632 735 pull_request, apiuser, api=True):
633 736 raise JSONRPCError(
634 737 'pull request `%s` update failed, no permission to update.' % (
635 738 pullrequestid,))
636 739 if pull_request.is_closed():
637 740 raise JSONRPCError(
638 741 'pull request `%s` update failed, pull request is closed' % (
639 742 pullrequestid,))
640 743
641 744 reviewer_objects = Optional.extract(reviewers) or []
642 745
643 746 if reviewer_objects:
644 747 schema = ReviewerListSchema()
645 748 try:
646 749 reviewer_objects = schema.deserialize(reviewer_objects)
647 750 except Invalid as err:
648 751 raise JSONRPCValidationError(colander_exc=err)
649 752
650 753 # validate users
651 754 for reviewer_object in reviewer_objects:
652 755 user = get_user_or_error(reviewer_object['username'])
653 756 reviewer_object['user_id'] = user.user_id
654 757
655 758 get_default_reviewers_data, get_validated_reviewers = \
656 759 PullRequestModel().get_reviewer_functions()
657 760
658 761 # re-use stored rules
659 762 reviewer_rules = pull_request.reviewer_data
660 763 try:
661 764 reviewers = get_validated_reviewers(
662 765 reviewer_objects, reviewer_rules)
663 766 except ValueError as e:
664 767 raise JSONRPCError('Reviewers Validation: {}'.format(e))
665 768 else:
666 769 reviewers = []
667 770
668 771 title = Optional.extract(title)
669 772 description = Optional.extract(description)
670 773 if title or description:
671 774 PullRequestModel().edit(
672 775 pull_request, title or pull_request.title,
673 776 description or pull_request.description, apiuser)
674 777 Session().commit()
675 778
676 779 commit_changes = {"added": [], "common": [], "removed": []}
677 780 if str2bool(Optional.extract(update_commits)):
678 781 if PullRequestModel().has_valid_update_type(pull_request):
679 782 update_response = PullRequestModel().update_commits(
680 783 pull_request)
681 784 commit_changes = update_response.changes or commit_changes
682 785 Session().commit()
683 786
684 787 reviewers_changes = {"added": [], "removed": []}
685 788 if reviewers:
686 789 added_reviewers, removed_reviewers = \
687 790 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
688 791
689 792 reviewers_changes['added'] = sorted(
690 793 [get_user_or_error(n).username for n in added_reviewers])
691 794 reviewers_changes['removed'] = sorted(
692 795 [get_user_or_error(n).username for n in removed_reviewers])
693 796 Session().commit()
694 797
695 798 data = {
696 799 'msg': 'Updated pull request `{}`'.format(
697 800 pull_request.pull_request_id),
698 801 'pull_request': pull_request.get_api_data(),
699 802 'updated_commits': commit_changes,
700 803 'updated_reviewers': reviewers_changes
701 804 }
702 805
703 806 return data
704 807
705 808
706 809 @jsonrpc_method()
707 810 def close_pull_request(
708 811 request, apiuser, repoid, pullrequestid,
709 812 userid=Optional(OAttr('apiuser')), message=Optional('')):
710 813 """
711 814 Close the pull request specified by `pullrequestid`.
712 815
713 816 :param apiuser: This is filled automatically from the |authtoken|.
714 817 :type apiuser: AuthUser
715 818 :param repoid: Repository name or repository ID to which the pull
716 819 request belongs.
717 820 :type repoid: str or int
718 821 :param pullrequestid: ID of the pull request to be closed.
719 822 :type pullrequestid: int
720 823 :param userid: Close the pull request as this user.
721 824 :type userid: Optional(str or int)
722 825 :param message: Optional message to close the Pull Request with. If not
723 826 specified it will be generated automatically.
724 827 :type message: Optional(str)
725 828
726 829 Example output:
727 830
728 831 .. code-block:: bash
729 832
730 833 "id": <id_given_in_input>,
731 834 "result": {
732 835 "pull_request_id": "<int>",
733 836 "close_status": "<str:status_lbl>,
734 837 "closed": "<bool>"
735 838 },
736 839 "error": null
737 840
738 841 """
739 842 _ = request.translate
740 843
741 844 repo = get_repo_or_error(repoid)
742 845 if not isinstance(userid, Optional):
743 846 if (has_superadmin_permission(apiuser) or
744 847 HasRepoPermissionAnyApi('repository.admin')(
745 848 user=apiuser, repo_name=repo.repo_name)):
746 849 apiuser = get_user_or_error(userid)
747 850 else:
748 851 raise JSONRPCError('userid is not the same as your user')
749 852
750 853 pull_request = get_pull_request_or_error(pullrequestid)
751 854
752 855 if pull_request.is_closed():
753 856 raise JSONRPCError(
754 857 'pull request `%s` is already closed' % (pullrequestid,))
755 858
756 859 # only owner or admin or person with write permissions
757 860 allowed_to_close = PullRequestModel().check_user_update(
758 861 pull_request, apiuser, api=True)
759 862
760 863 if not allowed_to_close:
761 864 raise JSONRPCError(
762 865 'pull request `%s` close failed, no permission to close.' % (
763 866 pullrequestid,))
764 867
765 868 # message we're using to close the PR, else it's automatically generated
766 869 message = Optional.extract(message)
767 870
768 871 # finally close the PR, with proper message comment
769 872 comment, status = PullRequestModel().close_pull_request_with_comment(
770 873 pull_request, apiuser, repo, message=message)
771 874 status_lbl = ChangesetStatus.get_status_lbl(status)
772 875
773 876 Session().commit()
774 877
775 878 data = {
776 879 'pull_request_id': pull_request.pull_request_id,
777 880 'close_status': status_lbl,
778 881 'closed': True,
779 882 }
780 883 return data
General Comments 0
You need to be logged in to leave comments. Login now