##// END OF EJS Templates
API: added pull-requests versions into returned API data...
dan -
r4197:01c1fb34 stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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