# Copyright (C) 2011-2023 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 # (only), as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError from rhodecode.api.utils import ( has_superadmin_permission, Optional, OAttr, get_repo_or_error, get_pull_request_or_error, get_commit_or_error, get_user_or_error, validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions) from rhodecode.lib import channelstream from rhodecode.lib.auth import (HasRepoPermissionAnyApi) from rhodecode.lib.base import vcs_operation_context from rhodecode.lib.utils2 import str2bool from rhodecode.lib.vcs.backends.base import unicode_to_reference from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import CommentsModel from rhodecode.model.db import ( Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers) from rhodecode.model.pull_request import PullRequestModel, MergeCheck from rhodecode.model.settings import SettingsModel from rhodecode.model.validation_schema import Invalid from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema log = logging.getLogger(__name__) @jsonrpc_method() def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None), merge_state=Optional(False)): """ Get a pull request based on the given ID. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional, repository name or repository ID from where the pull request was opened. :type repoid: str or int :param pullrequestid: ID of the requested pull request. :type pullrequestid: int :param merge_state: Optional calculate merge state for each repository. This could result in longer time to fetch the data :type merge_state: bool Example output: .. code-block:: bash "id": , "result": { "pull_request_id": "", "url": "", "title": "", "description": "<description>", "status" : "<status>", "created_on": "<date_time_created>", "updated_on": "<date_time_updated>", "versions": "<number_or_versions_of_pr>", "commit_ids": [ ... "<commit_id>", "<commit_id>", ... ], "review_status": "<review_status>", "mergeable": { "status": "<bool>", "message": "<message>", }, "source": { "clone_url": "<clone_url>", "repository": "<repository_name>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "target": { "clone_url": "<clone_url>", "repository": "<repository_name>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "merge": { "clone_url": "<clone_url>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "author": <user_obj>, "reviewers": [ ... { "user": "<user_obj>", "review_status": "<review_status>", } ... ] }, "error": null """ pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo if not PullRequestModel().check_user_read(pull_request, apiuser, api=True): raise JSONRPCError('repository `%s` or pull request `%s` ' 'does not exist' % (repoid, pullrequestid)) # NOTE(marcink): only calculate and return merge state if the pr state is 'created' # otherwise we can lock the repo on calculation of merge state while update/merge # is happening. pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED merge_state = Optional.extract(merge_state, binary=True) and pr_created data = pull_request.get_api_data(with_merge_state=merge_state) return data @jsonrpc_method() def get_pull_requests(request, apiuser, repoid, status=Optional('new'), merge_state=Optional(False)): """ Get all pull requests from the repository specified in `repoid`. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional repository name or repository ID. :type repoid: str or int :param status: Only return pull requests with the specified status. Valid options are. * ``new`` (default) * ``open`` * ``closed`` :type status: str :param merge_state: Optional calculate merge state for each repository. This could result in longer time to fetch the data :type merge_state: bool Example output: .. code-block:: bash "id": <id_given_in_input>, "result": [ ... { "pull_request_id": "<pull_request_id>", "url": "<url>", "title" : "<title>", "description": "<description>", "status": "<status>", "created_on": "<date_time_created>", "updated_on": "<date_time_updated>", "commit_ids": [ ... "<commit_id>", "<commit_id>", ... ], "review_status": "<review_status>", "mergeable": { "status": "<bool>", "message: "<message>", }, "source": { "clone_url": "<clone_url>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "target": { "clone_url": "<clone_url>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "merge": { "clone_url": "<clone_url>", "reference": { "name": "<name>", "type": "<type>", "commit_id": "<commit_id>", } }, "author": <user_obj>, "reviewers": [ ... { "user": "<user_obj>", "review_status": "<review_status>", } ... ] } ... ], "error": null """ repo = get_repo_or_error(repoid) if not has_superadmin_permission(apiuser): _perms = ( 'repository.admin', 'repository.write', 'repository.read',) validate_repo_permissions(apiuser, repoid, repo, _perms) status = Optional.extract(status) merge_state = Optional.extract(merge_state, binary=True) pull_requests = PullRequestModel().get_all(repo, statuses=[status], order_by='id', order_dir='desc') data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests] return data @jsonrpc_method() def merge_pull_request( request, apiuser, pullrequestid, repoid=Optional(None), userid=Optional(OAttr('apiuser'))): """ Merge the pull request specified by `pullrequestid` into its target repository. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional, repository name or repository ID of the target repository to which the |pr| is to be merged. :type repoid: str or int :param pullrequestid: ID of the pull request which shall be merged. :type pullrequestid: int :param userid: Merge the pull request as this user. :type userid: Optional(str or int) Example output: .. code-block:: bash "id": <id_given_in_input>, "result": { "executed": "<bool>", "failure_reason": "<int>", "merge_status_message": "<str>", "merge_commit_id": "<merge_commit_id>", "possible": "<bool>", "merge_ref": { "commit_id": "<commit_id>", "type": "<type>", "name": "<name>" } }, "error": null """ pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo auth_user = apiuser if not isinstance(userid, Optional): is_repo_admin = HasRepoPermissionAnyApi('repository.admin')( user=apiuser, repo_name=repo.repo_name) if has_superadmin_permission(apiuser) or is_repo_admin: apiuser = get_user_or_error(userid) auth_user = apiuser.AuthUser() else: raise JSONRPCError('userid is not the same as your user') if pull_request.pull_request_state != PullRequest.STATE_CREATED: raise JSONRPCError( 'Operation forbidden because pull request is in state {}, ' 'only state {} is allowed.'.format( pull_request.pull_request_state, PullRequest.STATE_CREATED)) with pull_request.set_state(PullRequest.STATE_UPDATING): check = MergeCheck.validate(pull_request, auth_user=auth_user, translator=request.translate) merge_possible = not check.failed if not merge_possible: error_messages = [] for err_type, error_msg in check.errors: error_msg = request.translate(error_msg) error_messages.append(error_msg) reasons = ','.join(error_messages) raise JSONRPCError( 'merge not possible for following reasons: {}'.format(reasons)) target_repo = pull_request.target_repo extras = vcs_operation_context( request.environ, repo_name=target_repo.repo_name, username=auth_user.username, action='push', scm=target_repo.repo_type) with pull_request.set_state(PullRequest.STATE_UPDATING): merge_response = PullRequestModel().merge_repo( pull_request, apiuser, extras=extras) if merge_response.executed: PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user) Session().commit() # In previous versions the merge response directly contained the merge # commit id. It is now contained in the merge reference object. To be # backwards compatible we have to extract it again. merge_response = merge_response.asdict() merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id return merge_response @jsonrpc_method() def get_pull_request_comments( request, apiuser, pullrequestid, repoid=Optional(None)): """ Get all comments of pull request specified with the `pullrequestid` :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional repository name or repository ID. :type repoid: str or int :param pullrequestid: The pull request ID. :type pullrequestid: int Example output: .. code-block:: bash id : <id_given_in_input> result : [ { "comment_author": { "active": true, "full_name_or_username": "Tom Gore", "username": "admin" }, "comment_created_on": "2017-01-02T18:43:45.533", "comment_f_path": null, "comment_id": 25, "comment_lineno": null, "comment_status": { "status": "under_review", "status_lbl": "Under Review" }, "comment_text": "Example text", "comment_type": null, "comment_last_version: 0, "pull_request_version": null, "comment_commit_id": None, "comment_pull_request_id": <pull_request_id> } ], error : null """ pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo if not PullRequestModel().check_user_read( pull_request, apiuser, api=True): raise JSONRPCError('repository `%s` or pull request `%s` ' 'does not exist' % (repoid, pullrequestid)) (pull_request_latest, pull_request_at_ver, pull_request_display_obj, at_version) = PullRequestModel().get_pr_version( pull_request.pull_request_id, version=None) versions = pull_request_display_obj.versions() ver_map = { ver.pull_request_version_id: cnt for cnt, ver in enumerate(versions, 1) } # GENERAL COMMENTS with versions # q = CommentsModel()._all_general_comments_of_pull_request(pull_request) q = q.order_by(ChangesetComment.comment_id.asc()) general_comments = q.all() # INLINE COMMENTS with versions # q = CommentsModel()._all_inline_comments_of_pull_request(pull_request) q = q.order_by(ChangesetComment.comment_id.asc()) inline_comments = q.all() data = [] for comment in inline_comments + general_comments: full_data = comment.get_api_data() pr_version_id = None if comment.pull_request_version_id: pr_version_id = 'v{}'.format( ver_map[comment.pull_request_version_id]) # sanitize some entries full_data['pull_request_version'] = pr_version_id full_data['comment_author'] = { 'username': full_data['comment_author'].username, 'full_name_or_username': full_data['comment_author'].full_name_or_username, 'active': full_data['comment_author'].active, } if full_data['comment_status']: full_data['comment_status'] = { 'status': full_data['comment_status'][0].status, 'status_lbl': full_data['comment_status'][0].status_lbl, } else: full_data['comment_status'] = {} data.append(full_data) return data @jsonrpc_method() def comment_pull_request( request, apiuser, pullrequestid, repoid=Optional(None), message=Optional(None), commit_id=Optional(None), status=Optional(None), comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE), resolves_comment_id=Optional(None), extra_recipients=Optional([]), userid=Optional(OAttr('apiuser')), send_email=Optional(True)): """ Comment on the pull request specified with the `pullrequestid`, in the |repo| specified by the `repoid`, and optionally change the review status. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional repository name or repository ID. :type repoid: str or int :param pullrequestid: The pull request ID. :type pullrequestid: int :param commit_id: Specify the commit_id for which to set a comment. If given commit_id is different than latest in the PR status change won't be performed. :type commit_id: str :param message: The text content of the comment. :type message: str :param status: (**Optional**) Set the approval status of the pull request. One of: 'not_reviewed', 'approved', 'rejected', 'under_review' :type status: str :param comment_type: Comment type, one of: 'note', 'todo' :type comment_type: Optional(str), default: 'note' :param resolves_comment_id: id of comment which this one will resolve :type resolves_comment_id: Optional(int) :param extra_recipients: list of user ids or usernames to add notifications for this comment. Acts like a CC for notification :type extra_recipients: Optional(list) :param userid: Comment on the pull request as this user :type userid: Optional(str or int) :param send_email: Define if this comment should also send email notification :type send_email: Optional(bool) Example output: .. code-block:: bash id : <id_given_in_input> result : { "pull_request_id": "<Integer>", "comment_id": "<Integer>", "status": {"given": <given_status>, "was_changed": <bool status_was_actually_changed> }, }, error : null """ _ = request.translate pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo db_repo_name = repo.repo_name auth_user = apiuser if not isinstance(userid, Optional): is_repo_admin = HasRepoPermissionAnyApi('repository.admin')( user=apiuser, repo_name=db_repo_name) if has_superadmin_permission(apiuser) or is_repo_admin: apiuser = get_user_or_error(userid) auth_user = apiuser.AuthUser() else: raise JSONRPCError('userid is not the same as your user') if pull_request.is_closed(): raise JSONRPCError(f'pull request `{pullrequestid}` comment failed, pull request is closed') if not PullRequestModel().check_user_read( pull_request, apiuser, api=True): raise JSONRPCError('repository `%s` does not exist' % (repoid,)) message = Optional.extract(message) status = Optional.extract(status) commit_id = Optional.extract(commit_id) comment_type = Optional.extract(comment_type) resolves_comment_id = Optional.extract(resolves_comment_id) extra_recipients = Optional.extract(extra_recipients) send_email = Optional.extract(send_email, binary=True) if not message and not status: raise JSONRPCError( 'Both message and status parameters are missing. ' 'At least one is required.') if status and status not in (st[0] for st in ChangesetStatus.STATUSES): raise JSONRPCError(f'Unknown comment status: `{status}`') if commit_id and commit_id not in pull_request.revisions: raise JSONRPCError(f'Invalid commit_id `{commit_id}` for this pull request.') allowed_to_change_status = PullRequestModel().check_user_change_status( pull_request, apiuser) # if commit_id is passed re-validated if user is allowed to change status # based on the latest commit_id from the PR if commit_id: commit_idx = pull_request.revisions.index(commit_id) if commit_idx != 0: log.warning('Resetting allowed_to_change_status = False because commit is NOT the latest in pull-request') allowed_to_change_status = False if resolves_comment_id: comment = ChangesetComment.get(resolves_comment_id) if not comment: raise JSONRPCError(f'Invalid resolves_comment_id `{resolves_comment_id}` for this pull request.') if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO: raise JSONRPCError(f'Comment `{resolves_comment_id}` is wrong type for setting status to resolved.') text = message status_label = ChangesetStatus.get_status_lbl(status) if status and allowed_to_change_status: st_message = ('Status change %(transition_icon)s %(status)s' % {'transition_icon': '>', 'status': status_label}) text = message or st_message rc_config = SettingsModel().get_all_settings() renderer = rc_config.get('rhodecode_markup_renderer', 'rst') status_change = status and allowed_to_change_status comment = CommentsModel().create( text=text, repo=pull_request.target_repo.repo_id, user=apiuser.user_id, pull_request=pull_request.pull_request_id, f_path=None, line_no=None, status_change=(status_label if status_change else None), status_change_type=(status if status_change else None), closing_pr=False, renderer=renderer, comment_type=comment_type, resolves_comment_id=resolves_comment_id, auth_user=auth_user, extra_recipients=extra_recipients, send_email=send_email ) is_inline = comment.is_inline if allowed_to_change_status and status: old_calculated_status = pull_request.calculated_review_status() ChangesetStatusModel().set_status( pull_request.target_repo.repo_id, status, apiuser.user_id, comment, pull_request=pull_request.pull_request_id ) Session().flush() Session().commit() PullRequestModel().trigger_pull_request_hook( pull_request, apiuser, 'comment', data={'comment': comment}) if allowed_to_change_status and status: # we now calculate the status of pull request, and based on that # calculation we set the commits status calculated_status = pull_request.calculated_review_status() if old_calculated_status != calculated_status: PullRequestModel().trigger_pull_request_hook( pull_request, apiuser, 'review_status_change', data={'status': calculated_status}) data = { 'pull_request_id': pull_request.pull_request_id, 'comment_id': comment.comment_id if comment else None, 'status': {'given': status, 'was_changed': status_change}, } comment_broadcast_channel = channelstream.comment_channel( db_repo_name, pull_request_obj=pull_request) comment_data = data comment_type = 'inline' if is_inline else 'general' channelstream.comment_channelstream_push( request, comment_broadcast_channel, apiuser, _('posted a new {} comment').format(comment_type), comment_data=comment_data) return data def _reviewers_validation(obj_list): schema = ReviewerListSchema() try: reviewer_objects = schema.deserialize(obj_list) except Invalid as err: raise JSONRPCValidationError(colander_exc=err) # validate users for reviewer_object in reviewer_objects: user = get_user_or_error(reviewer_object['username']) reviewer_object['user_id'] = user.user_id return reviewer_objects @jsonrpc_method() def create_pull_request( request, apiuser, source_repo, target_repo, source_ref, target_ref, owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''), description_renderer=Optional(''), reviewers=Optional(None), observers=Optional(None)): """ Creates a new pull request. Accepts refs in the following formats: * branch:<branch_name>:<sha> * branch:<branch_name> * bookmark:<bookmark_name>:<sha> (Mercurial only) * bookmark:<bookmark_name> (Mercurial only) :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param source_repo: Set the source repository name. :type source_repo: str :param target_repo: Set the target repository name. :type target_repo: str :param source_ref: Set the source ref name. :type source_ref: str :param target_ref: Set the target ref name. :type target_ref: str :param owner: user_id or username :type owner: Optional(str) :param title: Optionally Set the pull request title, it's generated otherwise :type title: str :param description: Set the pull request description. :type description: Optional(str) :type description_renderer: Optional(str) :param description_renderer: Set pull request renderer for the description. It should be 'rst', 'markdown' or 'plain'. If not give default system renderer will be used :param reviewers: Set the new pull request reviewers list. Reviewer defined by review rules will be added automatically to the defined list. :type reviewers: Optional(list) Accepts username strings or objects of the format: [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] :param observers: Set the new pull request observers list. Reviewer defined by review rules will be added automatically to the defined list. This feature is only available in RhodeCode EE :type observers: Optional(list) Accepts username strings or objects of the format: [{'username': 'nick', 'reasons': ['original author']}] """ source_db_repo = get_repo_or_error(source_repo) target_db_repo = get_repo_or_error(target_repo) if not has_superadmin_permission(apiuser): _perms = ('repository.admin', 'repository.write', 'repository.read',) validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms) owner = validate_set_owner_permissions(apiuser, owner) full_source_ref = resolve_ref_or_error(source_ref, source_db_repo) full_target_ref = resolve_ref_or_error(target_ref, target_db_repo) get_commit_or_error(full_source_ref, source_db_repo) get_commit_or_error(full_target_ref, target_db_repo) reviewer_objects = Optional.extract(reviewers) or [] observer_objects = Optional.extract(observers) or [] # serialize and validate passed in given reviewers if reviewer_objects: reviewer_objects = _reviewers_validation(reviewer_objects) if observer_objects: observer_objects = _reviewers_validation(reviewer_objects) get_default_reviewers_data, validate_default_reviewers, validate_observers = \ PullRequestModel().get_reviewer_functions() source_ref_obj = unicode_to_reference(full_source_ref) target_ref_obj = unicode_to_reference(full_target_ref) # recalculate reviewers logic, to make sure we can validate this default_reviewers_data = get_default_reviewers_data( owner, source_db_repo, source_ref_obj, target_db_repo, target_ref_obj, ) # now MERGE our given with the calculated from the default rules just_reviewers = [ x for x in default_reviewers_data['reviewers'] if x['role'] == PullRequestReviewers.ROLE_REVIEWER] reviewer_objects = just_reviewers + reviewer_objects try: reviewers = validate_default_reviewers( reviewer_objects, default_reviewers_data) except ValueError as e: raise JSONRPCError('Reviewers Validation: {}'.format(e)) # now MERGE our given with the calculated from the default rules just_observers = [ x for x in default_reviewers_data['reviewers'] if x['role'] == PullRequestReviewers.ROLE_OBSERVER] observer_objects = just_observers + observer_objects try: observers = validate_observers( observer_objects, default_reviewers_data) except ValueError as e: raise JSONRPCError('Observer Validation: {}'.format(e)) title = Optional.extract(title) if not title: title_source_ref = source_ref_obj.name title = PullRequestModel().generate_pullrequest_title( source=source_repo, source_ref=title_source_ref, target=target_repo ) diff_info = default_reviewers_data['diff_info'] common_ancestor_id = diff_info['ancestor'] # NOTE(marcink): reversed is consistent with how we open it in the WEB interface commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])] if not common_ancestor_id: raise JSONRPCError('no common ancestor found between specified references') if not commits: raise JSONRPCError('no commits found for merge between specified references') # recalculate target ref based on ancestor full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id)) # fetch renderer, if set fallback to plain in case of PR rc_config = SettingsModel().get_all_settings() default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain') description = Optional.extract(description) description_renderer = Optional.extract(description_renderer) or default_system_renderer pull_request = PullRequestModel().create( created_by=owner.user_id, source_repo=source_repo, source_ref=full_source_ref, target_repo=target_repo, target_ref=full_target_ref, common_ancestor_id=common_ancestor_id, revisions=commits, reviewers=reviewers, observers=observers, title=title, description=description, description_renderer=description_renderer, reviewer_data=default_reviewers_data, auth_user=apiuser ) Session().commit() data = { 'msg': 'Created new pull request `{}`'.format(title), 'pull_request_id': pull_request.pull_request_id, } return data @jsonrpc_method() def update_pull_request( request, apiuser, pullrequestid, repoid=Optional(None), title=Optional(''), description=Optional(''), description_renderer=Optional(''), reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)): """ Updates a pull request. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Optional repository name or repository ID. :type repoid: str or int :param pullrequestid: The pull request ID. :type pullrequestid: int :param title: Set the pull request title. :type title: str :param description: Update pull request description. :type description: Optional(str) :type description_renderer: Optional(str) :param description_renderer: Update pull request renderer for the description. It should be 'rst', 'markdown' or 'plain' :param reviewers: Update pull request reviewers list with new value. :type reviewers: Optional(list) Accepts username strings or objects of the format: [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}] :param observers: Update pull request observers list with new value. :type observers: Optional(list) Accepts username strings or objects of the format: [{'username': 'nick', 'reasons': ['should be aware about this PR']}] :param update_commits: Trigger update of commits for this pull request :type: update_commits: Optional(bool) Example output: .. code-block:: bash id : <id_given_in_input> result : { "msg": "Updated pull request `63`", "pull_request": <pull_request_object>, "updated_reviewers": { "added": [ "username" ], "removed": [] }, "updated_observers": { "added": [ "username" ], "removed": [] }, "updated_commits": { "added": [ "<sha1_hash>" ], "common": [ "<sha1_hash>", "<sha1_hash>", ], "removed": [] } } error : null """ pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo if not PullRequestModel().check_user_update( pull_request, apiuser, api=True): raise JSONRPCError( 'pull request `%s` update failed, no permission to update.' % ( pullrequestid,)) if pull_request.is_closed(): raise JSONRPCError( 'pull request `%s` update failed, pull request is closed' % ( pullrequestid,)) reviewer_objects = Optional.extract(reviewers) or [] observer_objects = Optional.extract(observers) or [] title = Optional.extract(title) description = Optional.extract(description) description_renderer = Optional.extract(description_renderer) # Update title/description title_changed = False if title or description: PullRequestModel().edit( pull_request, title or pull_request.title, description or pull_request.description, description_renderer or pull_request.description_renderer, apiuser) Session().commit() title_changed = True commit_changes = {"added": [], "common": [], "removed": []} # Update commits commits_changed = False if str2bool(Optional.extract(update_commits)): if pull_request.pull_request_state != PullRequest.STATE_CREATED: raise JSONRPCError( 'Operation forbidden because pull request is in state {}, ' 'only state {} is allowed.'.format( pull_request.pull_request_state, PullRequest.STATE_CREATED)) with pull_request.set_state(PullRequest.STATE_UPDATING): if PullRequestModel().has_valid_update_type(pull_request): db_user = apiuser.get_instance() update_response = PullRequestModel().update_commits( pull_request, db_user) commit_changes = update_response.changes or commit_changes Session().commit() commits_changed = True # Update reviewers # serialize and validate passed in given reviewers if reviewer_objects: reviewer_objects = _reviewers_validation(reviewer_objects) if observer_objects: observer_objects = _reviewers_validation(reviewer_objects) # re-use stored rules default_reviewers_data = pull_request.reviewer_data __, validate_default_reviewers, validate_observers = \ PullRequestModel().get_reviewer_functions() if reviewer_objects: try: reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data) except ValueError as e: raise JSONRPCError('Reviewers Validation: {}'.format(e)) else: reviewers = [] if observer_objects: try: observers = validate_default_reviewers(reviewer_objects, default_reviewers_data) except ValueError as e: raise JSONRPCError('Observer Validation: {}'.format(e)) else: observers = [] reviewers_changed = False reviewers_changes = {"added": [], "removed": []} if reviewers: old_calculated_status = pull_request.calculated_review_status() added_reviewers, removed_reviewers = \ PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance()) reviewers_changes['added'] = sorted( [get_user_or_error(n).username for n in added_reviewers]) reviewers_changes['removed'] = sorted( [get_user_or_error(n).username for n in removed_reviewers]) Session().commit() # trigger status changed if change in reviewers changes the status calculated_status = pull_request.calculated_review_status() if old_calculated_status != calculated_status: PullRequestModel().trigger_pull_request_hook( pull_request, apiuser, 'review_status_change', data={'status': calculated_status}) reviewers_changed = True observers_changed = False observers_changes = {"added": [], "removed": []} if observers: added_observers, removed_observers = \ PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance()) observers_changes['added'] = sorted( [get_user_or_error(n).username for n in added_observers]) observers_changes['removed'] = sorted( [get_user_or_error(n).username for n in removed_observers]) Session().commit() reviewers_changed = True # push changed to channelstream if commits_changed or reviewers_changed or observers_changed: pr_broadcast_channel = channelstream.pr_channel(pull_request) msg = 'Pull request was updated.' channelstream.pr_update_channelstream_push( request, pr_broadcast_channel, apiuser, msg) data = { 'msg': 'Updated pull request `{}`'.format(pull_request.pull_request_id), 'pull_request': pull_request.get_api_data(), 'updated_commits': commit_changes, 'updated_reviewers': reviewers_changes, 'updated_observers': observers_changes, } return data @jsonrpc_method() def close_pull_request( request, apiuser, pullrequestid, repoid=Optional(None), userid=Optional(OAttr('apiuser')), message=Optional('')): """ Close the pull request specified by `pullrequestid`. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser :param repoid: Repository name or repository ID to which the pull request belongs. :type repoid: str or int :param pullrequestid: ID of the pull request to be closed. :type pullrequestid: int :param userid: Close the pull request as this user. :type userid: Optional(str or int) :param message: Optional message to close the Pull Request with. If not specified it will be generated automatically. :type message: Optional(str) Example output: .. code-block:: bash "id": <id_given_in_input>, "result": { "pull_request_id": "<int>", "close_status": "<str:status_lbl>, "closed": "<bool>" }, "error": null """ _ = request.translate pull_request = get_pull_request_or_error(pullrequestid) if Optional.extract(repoid): repo = get_repo_or_error(repoid) else: repo = pull_request.target_repo is_repo_admin = HasRepoPermissionAnyApi('repository.admin')( user=apiuser, repo_name=repo.repo_name) if not isinstance(userid, Optional): if has_superadmin_permission(apiuser) or is_repo_admin: apiuser = get_user_or_error(userid) else: raise JSONRPCError('userid is not the same as your user') if pull_request.is_closed(): raise JSONRPCError( 'pull request `%s` is already closed' % (pullrequestid,)) # only owner or admin or person with write permissions allowed_to_close = PullRequestModel().check_user_update( pull_request, apiuser, api=True) if not allowed_to_close: raise JSONRPCError( 'pull request `%s` close failed, no permission to close.' % ( pullrequestid,)) # message we're using to close the PR, else it's automatically generated message = Optional.extract(message) # finally close the PR, with proper message comment comment, status = PullRequestModel().close_pull_request_with_comment( pull_request, apiuser, repo, message=message, auth_user=apiuser) status_lbl = ChangesetStatus.get_status_lbl(status) Session().commit() data = { 'pull_request_id': pull_request.pull_request_id, 'close_status': status_lbl, 'closed': True, } return data