##// END OF EJS Templates
api/pull-requests: trigger events for comments/review status changes.
marcink -
r3399:820d474e default
parent child Browse files
Show More
@@ -1,957 +1,980 b''
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)
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 """
146 146 Get all pull requests from the repository specified in `repoid`.
147 147
148 148 :param apiuser: This is filled automatically from the |authtoken|.
149 149 :type apiuser: AuthUser
150 150 :param repoid: Optional repository name or repository ID.
151 151 :type repoid: str or int
152 152 :param status: Only return pull requests with the specified status.
153 153 Valid options are.
154 154 * ``new`` (default)
155 155 * ``open``
156 156 * ``closed``
157 157 :type status: str
158 158
159 159 Example output:
160 160
161 161 .. code-block:: bash
162 162
163 163 "id": <id_given_in_input>,
164 164 "result":
165 165 [
166 166 ...
167 167 {
168 168 "pull_request_id": "<pull_request_id>",
169 169 "url": "<url>",
170 170 "title" : "<title>",
171 171 "description": "<description>",
172 172 "status": "<status>",
173 173 "created_on": "<date_time_created>",
174 174 "updated_on": "<date_time_updated>",
175 175 "commit_ids": [
176 176 ...
177 177 "<commit_id>",
178 178 "<commit_id>",
179 179 ...
180 180 ],
181 181 "review_status": "<review_status>",
182 182 "mergeable": {
183 183 "status": "<bool>",
184 184 "message: "<message>",
185 185 },
186 186 "source": {
187 187 "clone_url": "<clone_url>",
188 188 "reference":
189 189 {
190 190 "name": "<name>",
191 191 "type": "<type>",
192 192 "commit_id": "<commit_id>",
193 193 }
194 194 },
195 195 "target": {
196 196 "clone_url": "<clone_url>",
197 197 "reference":
198 198 {
199 199 "name": "<name>",
200 200 "type": "<type>",
201 201 "commit_id": "<commit_id>",
202 202 }
203 203 },
204 204 "merge": {
205 205 "clone_url": "<clone_url>",
206 206 "reference":
207 207 {
208 208 "name": "<name>",
209 209 "type": "<type>",
210 210 "commit_id": "<commit_id>",
211 211 }
212 212 },
213 213 "author": <user_obj>,
214 214 "reviewers": [
215 215 ...
216 216 {
217 217 "user": "<user_obj>",
218 218 "review_status": "<review_status>",
219 219 }
220 220 ...
221 221 ]
222 222 }
223 223 ...
224 224 ],
225 225 "error": null
226 226
227 227 """
228 228 repo = get_repo_or_error(repoid)
229 229 if not has_superadmin_permission(apiuser):
230 230 _perms = (
231 231 'repository.admin', 'repository.write', 'repository.read',)
232 232 validate_repo_permissions(apiuser, repoid, repo, _perms)
233 233
234 234 status = Optional.extract(status)
235 235 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
236 236 data = [pr.get_api_data() for pr in pull_requests]
237 237 return data
238 238
239 239
240 240 @jsonrpc_method()
241 241 def merge_pull_request(
242 242 request, apiuser, pullrequestid, repoid=Optional(None),
243 243 userid=Optional(OAttr('apiuser'))):
244 244 """
245 245 Merge the pull request specified by `pullrequestid` into its target
246 246 repository.
247 247
248 248 :param apiuser: This is filled automatically from the |authtoken|.
249 249 :type apiuser: AuthUser
250 250 :param repoid: Optional, repository name or repository ID of the
251 251 target repository to which the |pr| is to be merged.
252 252 :type repoid: str or int
253 253 :param pullrequestid: ID of the pull request which shall be merged.
254 254 :type pullrequestid: int
255 255 :param userid: Merge the pull request as this user.
256 256 :type userid: Optional(str or int)
257 257
258 258 Example output:
259 259
260 260 .. code-block:: bash
261 261
262 262 "id": <id_given_in_input>,
263 263 "result": {
264 264 "executed": "<bool>",
265 265 "failure_reason": "<int>",
266 266 "merge_commit_id": "<merge_commit_id>",
267 267 "possible": "<bool>",
268 268 "merge_ref": {
269 269 "commit_id": "<commit_id>",
270 270 "type": "<type>",
271 271 "name": "<name>"
272 272 }
273 273 },
274 274 "error": null
275 275 """
276 276 pull_request = get_pull_request_or_error(pullrequestid)
277 277 if Optional.extract(repoid):
278 278 repo = get_repo_or_error(repoid)
279 279 else:
280 280 repo = pull_request.target_repo
281 281
282 282 if not isinstance(userid, Optional):
283 283 if (has_superadmin_permission(apiuser) or
284 284 HasRepoPermissionAnyApi('repository.admin')(
285 285 user=apiuser, repo_name=repo.repo_name)):
286 286 apiuser = get_user_or_error(userid)
287 287 else:
288 288 raise JSONRPCError('userid is not the same as your user')
289 289
290 290 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
291 291 raise JSONRPCError(
292 292 'Operation forbidden because pull request is in state {}, '
293 293 'only state {} is allowed.'.format(
294 294 pull_request.pull_request_state, PullRequest.STATE_CREATED))
295 295
296 296 with pull_request.set_state(PullRequest.STATE_UPDATING):
297 297 check = MergeCheck.validate(
298 298 pull_request, auth_user=apiuser,
299 299 translator=request.translate)
300 300 merge_possible = not check.failed
301 301
302 302 if not merge_possible:
303 303 error_messages = []
304 304 for err_type, error_msg in check.errors:
305 305 error_msg = request.translate(error_msg)
306 306 error_messages.append(error_msg)
307 307
308 308 reasons = ','.join(error_messages)
309 309 raise JSONRPCError(
310 310 'merge not possible for following reasons: {}'.format(reasons))
311 311
312 312 target_repo = pull_request.target_repo
313 313 extras = vcs_operation_context(
314 314 request.environ, repo_name=target_repo.repo_name,
315 315 username=apiuser.username, action='push',
316 316 scm=target_repo.repo_type)
317 317 with pull_request.set_state(PullRequest.STATE_UPDATING):
318 318 merge_response = PullRequestModel().merge_repo(
319 319 pull_request, apiuser, extras=extras)
320 320 if merge_response.executed:
321 321 PullRequestModel().close_pull_request(
322 322 pull_request.pull_request_id, apiuser)
323 323
324 324 Session().commit()
325 325
326 326 # In previous versions the merge response directly contained the merge
327 327 # commit id. It is now contained in the merge reference object. To be
328 328 # backwards compatible we have to extract it again.
329 329 merge_response = merge_response.asdict()
330 330 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
331 331
332 332 return merge_response
333 333
334 334
335 335 @jsonrpc_method()
336 336 def get_pull_request_comments(
337 337 request, apiuser, pullrequestid, repoid=Optional(None)):
338 338 """
339 339 Get all comments of pull request specified with the `pullrequestid`
340 340
341 341 :param apiuser: This is filled automatically from the |authtoken|.
342 342 :type apiuser: AuthUser
343 343 :param repoid: Optional repository name or repository ID.
344 344 :type repoid: str or int
345 345 :param pullrequestid: The pull request ID.
346 346 :type pullrequestid: int
347 347
348 348 Example output:
349 349
350 350 .. code-block:: bash
351 351
352 352 id : <id_given_in_input>
353 353 result : [
354 354 {
355 355 "comment_author": {
356 356 "active": true,
357 357 "full_name_or_username": "Tom Gore",
358 358 "username": "admin"
359 359 },
360 360 "comment_created_on": "2017-01-02T18:43:45.533",
361 361 "comment_f_path": null,
362 362 "comment_id": 25,
363 363 "comment_lineno": null,
364 364 "comment_status": {
365 365 "status": "under_review",
366 366 "status_lbl": "Under Review"
367 367 },
368 368 "comment_text": "Example text",
369 369 "comment_type": null,
370 370 "pull_request_version": null
371 371 }
372 372 ],
373 373 error : null
374 374 """
375 375
376 376 pull_request = get_pull_request_or_error(pullrequestid)
377 377 if Optional.extract(repoid):
378 378 repo = get_repo_or_error(repoid)
379 379 else:
380 380 repo = pull_request.target_repo
381 381
382 382 if not PullRequestModel().check_user_read(
383 383 pull_request, apiuser, api=True):
384 384 raise JSONRPCError('repository `%s` or pull request `%s` '
385 385 'does not exist' % (repoid, pullrequestid))
386 386
387 387 (pull_request_latest,
388 388 pull_request_at_ver,
389 389 pull_request_display_obj,
390 390 at_version) = PullRequestModel().get_pr_version(
391 391 pull_request.pull_request_id, version=None)
392 392
393 393 versions = pull_request_display_obj.versions()
394 394 ver_map = {
395 395 ver.pull_request_version_id: cnt
396 396 for cnt, ver in enumerate(versions, 1)
397 397 }
398 398
399 399 # GENERAL COMMENTS with versions #
400 400 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
401 401 q = q.order_by(ChangesetComment.comment_id.asc())
402 402 general_comments = q.all()
403 403
404 404 # INLINE COMMENTS with versions #
405 405 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
406 406 q = q.order_by(ChangesetComment.comment_id.asc())
407 407 inline_comments = q.all()
408 408
409 409 data = []
410 410 for comment in inline_comments + general_comments:
411 411 full_data = comment.get_api_data()
412 412 pr_version_id = None
413 413 if comment.pull_request_version_id:
414 414 pr_version_id = 'v{}'.format(
415 415 ver_map[comment.pull_request_version_id])
416 416
417 417 # sanitize some entries
418 418
419 419 full_data['pull_request_version'] = pr_version_id
420 420 full_data['comment_author'] = {
421 421 'username': full_data['comment_author'].username,
422 422 'full_name_or_username': full_data['comment_author'].full_name_or_username,
423 423 'active': full_data['comment_author'].active,
424 424 }
425 425
426 426 if full_data['comment_status']:
427 427 full_data['comment_status'] = {
428 428 'status': full_data['comment_status'][0].status,
429 429 'status_lbl': full_data['comment_status'][0].status_lbl,
430 430 }
431 431 else:
432 432 full_data['comment_status'] = {}
433 433
434 434 data.append(full_data)
435 435 return data
436 436
437 437
438 438 @jsonrpc_method()
439 439 def comment_pull_request(
440 440 request, apiuser, pullrequestid, repoid=Optional(None),
441 441 message=Optional(None), commit_id=Optional(None), status=Optional(None),
442 442 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
443 443 resolves_comment_id=Optional(None),
444 444 userid=Optional(OAttr('apiuser'))):
445 445 """
446 446 Comment on the pull request specified with the `pullrequestid`,
447 447 in the |repo| specified by the `repoid`, and optionally change the
448 448 review status.
449 449
450 450 :param apiuser: This is filled automatically from the |authtoken|.
451 451 :type apiuser: AuthUser
452 452 :param repoid: Optional repository name or repository ID.
453 453 :type repoid: str or int
454 454 :param pullrequestid: The pull request ID.
455 455 :type pullrequestid: int
456 456 :param commit_id: Specify the commit_id for which to set a comment. If
457 457 given commit_id is different than latest in the PR status
458 458 change won't be performed.
459 459 :type commit_id: str
460 460 :param message: The text content of the comment.
461 461 :type message: str
462 462 :param status: (**Optional**) Set the approval status of the pull
463 463 request. One of: 'not_reviewed', 'approved', 'rejected',
464 464 'under_review'
465 465 :type status: str
466 466 :param comment_type: Comment type, one of: 'note', 'todo'
467 467 :type comment_type: Optional(str), default: 'note'
468 468 :param userid: Comment on the pull request as this user
469 469 :type userid: Optional(str or int)
470 470
471 471 Example output:
472 472
473 473 .. code-block:: bash
474 474
475 475 id : <id_given_in_input>
476 476 result : {
477 477 "pull_request_id": "<Integer>",
478 478 "comment_id": "<Integer>",
479 479 "status": {"given": <given_status>,
480 480 "was_changed": <bool status_was_actually_changed> },
481 481 },
482 482 error : null
483 483 """
484 484 pull_request = get_pull_request_or_error(pullrequestid)
485 485 if Optional.extract(repoid):
486 486 repo = get_repo_or_error(repoid)
487 487 else:
488 488 repo = pull_request.target_repo
489 489
490 490 if not isinstance(userid, Optional):
491 491 if (has_superadmin_permission(apiuser) or
492 492 HasRepoPermissionAnyApi('repository.admin')(
493 493 user=apiuser, repo_name=repo.repo_name)):
494 494 apiuser = get_user_or_error(userid)
495 495 else:
496 496 raise JSONRPCError('userid is not the same as your user')
497 497
498 498 if not PullRequestModel().check_user_read(
499 499 pull_request, apiuser, api=True):
500 500 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
501 501 message = Optional.extract(message)
502 502 status = Optional.extract(status)
503 503 commit_id = Optional.extract(commit_id)
504 504 comment_type = Optional.extract(comment_type)
505 505 resolves_comment_id = Optional.extract(resolves_comment_id)
506 506
507 507 if not message and not status:
508 508 raise JSONRPCError(
509 509 'Both message and status parameters are missing. '
510 510 'At least one is required.')
511 511
512 512 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
513 513 status is not None):
514 514 raise JSONRPCError('Unknown comment status: `%s`' % status)
515 515
516 516 if commit_id and commit_id not in pull_request.revisions:
517 517 raise JSONRPCError(
518 518 'Invalid commit_id `%s` for this pull request.' % commit_id)
519 519
520 520 allowed_to_change_status = PullRequestModel().check_user_change_status(
521 521 pull_request, apiuser)
522 522
523 523 # if commit_id is passed re-validated if user is allowed to change status
524 524 # based on latest commit_id from the PR
525 525 if commit_id:
526 526 commit_idx = pull_request.revisions.index(commit_id)
527 527 if commit_idx != 0:
528 528 allowed_to_change_status = False
529 529
530 530 if resolves_comment_id:
531 531 comment = ChangesetComment.get(resolves_comment_id)
532 532 if not comment:
533 533 raise JSONRPCError(
534 534 'Invalid resolves_comment_id `%s` for this pull request.'
535 535 % resolves_comment_id)
536 536 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
537 537 raise JSONRPCError(
538 538 'Comment `%s` is wrong type for setting status to resolved.'
539 539 % resolves_comment_id)
540 540
541 541 text = message
542 542 status_label = ChangesetStatus.get_status_lbl(status)
543 543 if status and allowed_to_change_status:
544 544 st_message = ('Status change %(transition_icon)s %(status)s'
545 545 % {'transition_icon': '>', 'status': status_label})
546 546 text = message or st_message
547 547
548 548 rc_config = SettingsModel().get_all_settings()
549 549 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
550 550
551 551 status_change = status and allowed_to_change_status
552 552 comment = CommentsModel().create(
553 553 text=text,
554 554 repo=pull_request.target_repo.repo_id,
555 555 user=apiuser.user_id,
556 556 pull_request=pull_request.pull_request_id,
557 557 f_path=None,
558 558 line_no=None,
559 559 status_change=(status_label if status_change else None),
560 560 status_change_type=(status if status_change else None),
561 561 closing_pr=False,
562 562 renderer=renderer,
563 563 comment_type=comment_type,
564 564 resolves_comment_id=resolves_comment_id,
565 565 auth_user=apiuser
566 566 )
567 567
568 568 if allowed_to_change_status and status:
569 old_calculated_status = pull_request.calculated_review_status()
569 570 ChangesetStatusModel().set_status(
570 571 pull_request.target_repo.repo_id,
571 572 status,
572 573 apiuser.user_id,
573 574 comment,
574 575 pull_request=pull_request.pull_request_id
575 576 )
576 577 Session().flush()
577 578
578 579 Session().commit()
580
581 PullRequestModel().trigger_pull_request_hook(
582 pull_request, apiuser, 'comment',
583 data={'comment': comment})
584
585 if allowed_to_change_status and status:
586 # we now calculate the status of pull request, and based on that
587 # calculation we set the commits status
588 calculated_status = pull_request.calculated_review_status()
589 if old_calculated_status != calculated_status:
590 PullRequestModel().trigger_pull_request_hook(
591 pull_request, apiuser, 'review_status_change',
592 data={'status': calculated_status})
593
579 594 data = {
580 595 'pull_request_id': pull_request.pull_request_id,
581 596 'comment_id': comment.comment_id if comment else None,
582 597 'status': {'given': status, 'was_changed': status_change},
583 598 }
584 599 return data
585 600
586 601
587 602 @jsonrpc_method()
588 603 def create_pull_request(
589 604 request, apiuser, source_repo, target_repo, source_ref, target_ref,
590 605 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
591 606 reviewers=Optional(None)):
592 607 """
593 608 Creates a new pull request.
594 609
595 610 Accepts refs in the following formats:
596 611
597 612 * branch:<branch_name>:<sha>
598 613 * branch:<branch_name>
599 614 * bookmark:<bookmark_name>:<sha> (Mercurial only)
600 615 * bookmark:<bookmark_name> (Mercurial only)
601 616
602 617 :param apiuser: This is filled automatically from the |authtoken|.
603 618 :type apiuser: AuthUser
604 619 :param source_repo: Set the source repository name.
605 620 :type source_repo: str
606 621 :param target_repo: Set the target repository name.
607 622 :type target_repo: str
608 623 :param source_ref: Set the source ref name.
609 624 :type source_ref: str
610 625 :param target_ref: Set the target ref name.
611 626 :type target_ref: str
612 627 :param title: Optionally Set the pull request title, it's generated otherwise
613 628 :type title: str
614 629 :param description: Set the pull request description.
615 630 :type description: Optional(str)
616 631 :type description_renderer: Optional(str)
617 632 :param description_renderer: Set pull request renderer for the description.
618 633 It should be 'rst', 'markdown' or 'plain'. If not give default
619 634 system renderer will be used
620 635 :param reviewers: Set the new pull request reviewers list.
621 636 Reviewer defined by review rules will be added automatically to the
622 637 defined list.
623 638 :type reviewers: Optional(list)
624 639 Accepts username strings or objects of the format:
625 640
626 641 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
627 642 """
628 643
629 644 source_db_repo = get_repo_or_error(source_repo)
630 645 target_db_repo = get_repo_or_error(target_repo)
631 646 if not has_superadmin_permission(apiuser):
632 647 _perms = ('repository.admin', 'repository.write', 'repository.read',)
633 648 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
634 649
635 650 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
636 651 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
637 652
638 653 source_scm = source_db_repo.scm_instance()
639 654 target_scm = target_db_repo.scm_instance()
640 655
641 656 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
642 657 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
643 658
644 659 ancestor = source_scm.get_common_ancestor(
645 660 source_commit.raw_id, target_commit.raw_id, target_scm)
646 661 if not ancestor:
647 662 raise JSONRPCError('no common ancestor found')
648 663
649 664 # recalculate target ref based on ancestor
650 665 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
651 666 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
652 667
653 668 commit_ranges = target_scm.compare(
654 669 target_commit.raw_id, source_commit.raw_id, source_scm,
655 670 merge=True, pre_load=[])
656 671
657 672 if not commit_ranges:
658 673 raise JSONRPCError('no commits found')
659 674
660 675 reviewer_objects = Optional.extract(reviewers) or []
661 676
662 677 # serialize and validate passed in given reviewers
663 678 if reviewer_objects:
664 679 schema = ReviewerListSchema()
665 680 try:
666 681 reviewer_objects = schema.deserialize(reviewer_objects)
667 682 except Invalid as err:
668 683 raise JSONRPCValidationError(colander_exc=err)
669 684
670 685 # validate users
671 686 for reviewer_object in reviewer_objects:
672 687 user = get_user_or_error(reviewer_object['username'])
673 688 reviewer_object['user_id'] = user.user_id
674 689
675 690 get_default_reviewers_data, validate_default_reviewers = \
676 691 PullRequestModel().get_reviewer_functions()
677 692
678 693 # recalculate reviewers logic, to make sure we can validate this
679 694 reviewer_rules = get_default_reviewers_data(
680 695 apiuser.get_instance(), source_db_repo,
681 696 source_commit, target_db_repo, target_commit)
682 697
683 698 # now MERGE our given with the calculated
684 699 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
685 700
686 701 try:
687 702 reviewers = validate_default_reviewers(
688 703 reviewer_objects, reviewer_rules)
689 704 except ValueError as e:
690 705 raise JSONRPCError('Reviewers Validation: {}'.format(e))
691 706
692 707 title = Optional.extract(title)
693 708 if not title:
694 709 title_source_ref = source_ref.split(':', 2)[1]
695 710 title = PullRequestModel().generate_pullrequest_title(
696 711 source=source_repo,
697 712 source_ref=title_source_ref,
698 713 target=target_repo
699 714 )
700 715 # fetch renderer, if set fallback to plain in case of PR
701 716 rc_config = SettingsModel().get_all_settings()
702 717 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
703 718 description = Optional.extract(description)
704 719 description_renderer = Optional.extract(description_renderer) or default_system_renderer
705 720
706 721 pull_request = PullRequestModel().create(
707 722 created_by=apiuser.user_id,
708 723 source_repo=source_repo,
709 724 source_ref=full_source_ref,
710 725 target_repo=target_repo,
711 726 target_ref=full_target_ref,
712 727 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
713 728 reviewers=reviewers,
714 729 title=title,
715 730 description=description,
716 731 description_renderer=description_renderer,
717 732 reviewer_data=reviewer_rules,
718 733 auth_user=apiuser
719 734 )
720 735
721 736 Session().commit()
722 737 data = {
723 738 'msg': 'Created new pull request `{}`'.format(title),
724 739 'pull_request_id': pull_request.pull_request_id,
725 740 }
726 741 return data
727 742
728 743
729 744 @jsonrpc_method()
730 745 def update_pull_request(
731 746 request, apiuser, pullrequestid, repoid=Optional(None),
732 747 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
733 748 reviewers=Optional(None), update_commits=Optional(None)):
734 749 """
735 750 Updates a pull request.
736 751
737 752 :param apiuser: This is filled automatically from the |authtoken|.
738 753 :type apiuser: AuthUser
739 754 :param repoid: Optional repository name or repository ID.
740 755 :type repoid: str or int
741 756 :param pullrequestid: The pull request ID.
742 757 :type pullrequestid: int
743 758 :param title: Set the pull request title.
744 759 :type title: str
745 760 :param description: Update pull request description.
746 761 :type description: Optional(str)
747 762 :type description_renderer: Optional(str)
748 763 :param description_renderer: Update pull request renderer for the description.
749 764 It should be 'rst', 'markdown' or 'plain'
750 765 :param reviewers: Update pull request reviewers list with new value.
751 766 :type reviewers: Optional(list)
752 767 Accepts username strings or objects of the format:
753 768
754 769 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
755 770
756 771 :param update_commits: Trigger update of commits for this pull request
757 772 :type: update_commits: Optional(bool)
758 773
759 774 Example output:
760 775
761 776 .. code-block:: bash
762 777
763 778 id : <id_given_in_input>
764 779 result : {
765 780 "msg": "Updated pull request `63`",
766 781 "pull_request": <pull_request_object>,
767 782 "updated_reviewers": {
768 783 "added": [
769 784 "username"
770 785 ],
771 786 "removed": []
772 787 },
773 788 "updated_commits": {
774 789 "added": [
775 790 "<sha1_hash>"
776 791 ],
777 792 "common": [
778 793 "<sha1_hash>",
779 794 "<sha1_hash>",
780 795 ],
781 796 "removed": []
782 797 }
783 798 }
784 799 error : null
785 800 """
786 801
787 802 pull_request = get_pull_request_or_error(pullrequestid)
788 803 if Optional.extract(repoid):
789 804 repo = get_repo_or_error(repoid)
790 805 else:
791 806 repo = pull_request.target_repo
792 807
793 808 if not PullRequestModel().check_user_update(
794 809 pull_request, apiuser, api=True):
795 810 raise JSONRPCError(
796 811 'pull request `%s` update failed, no permission to update.' % (
797 812 pullrequestid,))
798 813 if pull_request.is_closed():
799 814 raise JSONRPCError(
800 815 'pull request `%s` update failed, pull request is closed' % (
801 816 pullrequestid,))
802 817
803 818 reviewer_objects = Optional.extract(reviewers) or []
804 819
805 820 if reviewer_objects:
806 821 schema = ReviewerListSchema()
807 822 try:
808 823 reviewer_objects = schema.deserialize(reviewer_objects)
809 824 except Invalid as err:
810 825 raise JSONRPCValidationError(colander_exc=err)
811 826
812 827 # validate users
813 828 for reviewer_object in reviewer_objects:
814 829 user = get_user_or_error(reviewer_object['username'])
815 830 reviewer_object['user_id'] = user.user_id
816 831
817 832 get_default_reviewers_data, get_validated_reviewers = \
818 833 PullRequestModel().get_reviewer_functions()
819 834
820 835 # re-use stored rules
821 836 reviewer_rules = pull_request.reviewer_data
822 837 try:
823 838 reviewers = get_validated_reviewers(
824 839 reviewer_objects, reviewer_rules)
825 840 except ValueError as e:
826 841 raise JSONRPCError('Reviewers Validation: {}'.format(e))
827 842 else:
828 843 reviewers = []
829 844
830 845 title = Optional.extract(title)
831 846 description = Optional.extract(description)
832 847 description_renderer = Optional.extract(description_renderer)
833 848
834 849 if title or description:
835 850 PullRequestModel().edit(
836 851 pull_request,
837 852 title or pull_request.title,
838 853 description or pull_request.description,
839 854 description_renderer or pull_request.description_renderer,
840 855 apiuser)
841 856 Session().commit()
842 857
843 858 commit_changes = {"added": [], "common": [], "removed": []}
844 859 if str2bool(Optional.extract(update_commits)):
845 860
846 861 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
847 862 raise JSONRPCError(
848 863 'Operation forbidden because pull request is in state {}, '
849 864 'only state {} is allowed.'.format(
850 865 pull_request.pull_request_state, PullRequest.STATE_CREATED))
851 866
852 867 with pull_request.set_state(PullRequest.STATE_UPDATING):
853 868 if PullRequestModel().has_valid_update_type(pull_request):
854 869 update_response = PullRequestModel().update_commits(pull_request)
855 870 commit_changes = update_response.changes or commit_changes
856 871 Session().commit()
857 872
858 873 reviewers_changes = {"added": [], "removed": []}
859 874 if reviewers:
875 old_calculated_status = pull_request.calculated_review_status()
860 876 added_reviewers, removed_reviewers = \
861 877 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
862 878
863 879 reviewers_changes['added'] = sorted(
864 880 [get_user_or_error(n).username for n in added_reviewers])
865 881 reviewers_changes['removed'] = sorted(
866 882 [get_user_or_error(n).username for n in removed_reviewers])
867 883 Session().commit()
868 884
885 # trigger status changed if change in reviewers changes the status
886 calculated_status = pull_request.calculated_review_status()
887 if old_calculated_status != calculated_status:
888 PullRequestModel().trigger_pull_request_hook(
889 pull_request, apiuser, 'review_status_change',
890 data={'status': calculated_status})
891
869 892 data = {
870 893 'msg': 'Updated pull request `{}`'.format(
871 894 pull_request.pull_request_id),
872 895 'pull_request': pull_request.get_api_data(),
873 896 'updated_commits': commit_changes,
874 897 'updated_reviewers': reviewers_changes
875 898 }
876 899
877 900 return data
878 901
879 902
880 903 @jsonrpc_method()
881 904 def close_pull_request(
882 905 request, apiuser, pullrequestid, repoid=Optional(None),
883 906 userid=Optional(OAttr('apiuser')), message=Optional('')):
884 907 """
885 908 Close the pull request specified by `pullrequestid`.
886 909
887 910 :param apiuser: This is filled automatically from the |authtoken|.
888 911 :type apiuser: AuthUser
889 912 :param repoid: Repository name or repository ID to which the pull
890 913 request belongs.
891 914 :type repoid: str or int
892 915 :param pullrequestid: ID of the pull request to be closed.
893 916 :type pullrequestid: int
894 917 :param userid: Close the pull request as this user.
895 918 :type userid: Optional(str or int)
896 919 :param message: Optional message to close the Pull Request with. If not
897 920 specified it will be generated automatically.
898 921 :type message: Optional(str)
899 922
900 923 Example output:
901 924
902 925 .. code-block:: bash
903 926
904 927 "id": <id_given_in_input>,
905 928 "result": {
906 929 "pull_request_id": "<int>",
907 930 "close_status": "<str:status_lbl>,
908 931 "closed": "<bool>"
909 932 },
910 933 "error": null
911 934
912 935 """
913 936 _ = request.translate
914 937
915 938 pull_request = get_pull_request_or_error(pullrequestid)
916 939 if Optional.extract(repoid):
917 940 repo = get_repo_or_error(repoid)
918 941 else:
919 942 repo = pull_request.target_repo
920 943
921 944 if not isinstance(userid, Optional):
922 945 if (has_superadmin_permission(apiuser) or
923 946 HasRepoPermissionAnyApi('repository.admin')(
924 947 user=apiuser, repo_name=repo.repo_name)):
925 948 apiuser = get_user_or_error(userid)
926 949 else:
927 950 raise JSONRPCError('userid is not the same as your user')
928 951
929 952 if pull_request.is_closed():
930 953 raise JSONRPCError(
931 954 'pull request `%s` is already closed' % (pullrequestid,))
932 955
933 956 # only owner or admin or person with write permissions
934 957 allowed_to_close = PullRequestModel().check_user_update(
935 958 pull_request, apiuser, api=True)
936 959
937 960 if not allowed_to_close:
938 961 raise JSONRPCError(
939 962 'pull request `%s` close failed, no permission to close.' % (
940 963 pullrequestid,))
941 964
942 965 # message we're using to close the PR, else it's automatically generated
943 966 message = Optional.extract(message)
944 967
945 968 # finally close the PR, with proper message comment
946 969 comment, status = PullRequestModel().close_pull_request_with_comment(
947 970 pull_request, apiuser, repo, message=message, auth_user=apiuser)
948 971 status_lbl = ChangesetStatus.get_status_lbl(status)
949 972
950 973 Session().commit()
951 974
952 975 data = {
953 976 'pull_request_id': pull_request.pull_request_id,
954 977 'close_status': status_lbl,
955 978 'closed': True,
956 979 }
957 980 return data
@@ -1,97 +1,100 b''
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.tests.events.conftest import EventCatcher
24 24
25 25 from rhodecode.model.comment import CommentsModel
26 26 from rhodecode.model.pull_request import PullRequestModel
27 27 from rhodecode.events import (
28 28 PullRequestCreateEvent,
29 29 PullRequestUpdateEvent,
30 30 PullRequestCommentEvent,
31 31 PullRequestReviewEvent,
32 32 PullRequestMergeEvent,
33 33 PullRequestCloseEvent,
34 34 )
35 35
36 36 # TODO: dan: make the serialization tests complete json comparisons
37 37 @pytest.mark.backends("git", "hg")
38 38 @pytest.mark.parametrize('EventClass', [
39 39 PullRequestCreateEvent,
40 40 PullRequestUpdateEvent,
41 41 PullRequestReviewEvent,
42 42 PullRequestMergeEvent,
43 43 PullRequestCloseEvent,
44 44 ])
45 45 def test_pullrequest_events_serialized(EventClass, pr_util, config_stub):
46 46 pr = pr_util.create_pull_request()
47 event = EventClass(pr)
47 if EventClass == PullRequestReviewEvent:
48 event = EventClass(pr, 'approved')
49 else:
50 event = EventClass(pr)
48 51 data = event.as_dict()
49 52 assert data['name'] == EventClass.name
50 53 assert data['repo']['repo_name'] == pr.target_repo.repo_name
51 54 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
52 55 assert data['pullrequest']['url']
53 56 assert data['pullrequest']['permalink_url']
54 57
55 58
56 59 @pytest.mark.backends("git", "hg")
57 60 def test_create_pull_request_events(pr_util, config_stub):
58 61 with EventCatcher() as event_catcher:
59 62 pr_util.create_pull_request()
60 63
61 64 assert PullRequestCreateEvent in event_catcher.events_types
62 65
63 66
64 67 @pytest.mark.backends("git", "hg")
65 68 def test_pullrequest_comment_events_serialized(pr_util, config_stub):
66 69 pr = pr_util.create_pull_request()
67 70 comment = CommentsModel().get_comments(
68 71 pr.target_repo.repo_id, pull_request=pr)[0]
69 72 event = PullRequestCommentEvent(pr, comment)
70 73 data = event.as_dict()
71 74 assert data['name'] == PullRequestCommentEvent.name
72 75 assert data['repo']['repo_name'] == pr.target_repo.repo_name
73 76 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
74 77 assert data['pullrequest']['url']
75 78 assert data['pullrequest']['permalink_url']
76 79 assert data['comment']['text'] == comment.text
77 80
78 81
79 82 @pytest.mark.backends("git", "hg")
80 83 def test_close_pull_request_events(pr_util, user_admin, config_stub):
81 84 pr = pr_util.create_pull_request()
82 85
83 86 with EventCatcher() as event_catcher:
84 87 PullRequestModel().close_pull_request(pr, user_admin)
85 88
86 89 assert PullRequestCloseEvent in event_catcher.events_types
87 90
88 91
89 92 @pytest.mark.backends("git", "hg")
90 93 def test_close_pull_request_with_comment_events(pr_util, user_admin, config_stub):
91 94 pr = pr_util.create_pull_request()
92 95
93 96 with EventCatcher() as event_catcher:
94 97 PullRequestModel().close_pull_request_with_comment(
95 98 pr, user_admin, pr.target_repo)
96 99
97 100 assert PullRequestCloseEvent in event_catcher.events_types
General Comments 0
You need to be logged in to leave comments. Login now