# -*- coding: utf-8 -*- """ (c) 2015 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon """ import flask from sqlalchemy.exc import SQLAlchemyError import pagure import pagure.exceptions import pagure.lib from pagure import APP, SESSION, is_repo_admin from pagure.api import API, api_method, api_login_required, APIERROR @API.route('//pull-requests') @API.route('///pull-requests') @API.route('/fork///pull-requests') @API.route('/fork////pull-requests') @api_method def api_pull_request_views(repo, username=None, namespace=None): """ List project's Pull-Requests ---------------------------- Retrieve pull requests of a project. :: GET /api/0//pull-requests GET /api/0///pull-requests :: GET /api/0/fork///pull-requests GET /api/0/fork////pull-requests Parameters ^^^^^^^^^^ +---------------+----------+--------------+----------------------------+ | Key | Type | Optionality | Description | +===============+==========+==============+============================+ | ``status`` | string | Optional | | Filter the status of | | | | | pull requests. Default: | | | | | ``True`` (opened pull | | | | | requests), can be ``0`` | | | | | or ``closed`` for closed | | | | | requests or ``Merged`` | | | | | for merged requests. | | | | | ``All`` returns closed, | | | | | merged and open requests.| +---------------+----------+--------------+----------------------------+ | ``assignee`` | string | Optional | | Filter the assignee of | | | | | pull requests | +---------------+----------+--------------+----------------------------+ | ``author`` | string | Optional | | Filter the author of | | | | | pull requests | +---------------+----------+--------------+----------------------------+ Sample response ^^^^^^^^^^^^^^^ :: { "args": { "assignee": null, "author": null, "status": true }, "total_requests": 1, "requests": [ { "assignee": null, "branch": "master", "branch_from": "master", "closed_at": null, "closed_by": null, "comments": [], "commit_start": null, "commit_stop": null, "date_created": "1431414800", "id": 1, "project": { "date_created": "1431414800", "description": "test project #1", "id": 1, "name": "test", "parent": null, "user": { "fullname": "PY C", "name": "pingou" } }, "repo_from": { "date_created": "1431414800", "description": "test project #1", "id": 1, "name": "test", "parent": null, "user": { "fullname": "PY C", "name": "pingou" } }, "status": true, "title": "test pull-request", "uid": "1431414800", "updated_on": "1431414800", "user": { "fullname": "PY C", "name": "pingou" } } ] } """ repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) status = flask.request.args.get('status', True) assignee = flask.request.args.get('assignee', None) author = flask.request.args.get('author', None) requests = [] if str(status).lower() in ['0', 'false', 'closed']: requests = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, status=False, assignee=assignee, author=author) elif str(status).lower() == 'all': requests = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, status=None, assignee=assignee, author=author) else: requests = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, assignee=assignee, author=author, status=status) jsonout = flask.jsonify({ 'total_requests': len(requests), 'requests': [ request.to_json(public=True, api=True) for request in requests], 'args': { 'status': status, 'assignee': assignee, 'author': author, } }) return jsonout @API.route('//pull-request/') @API.route('///pull-request/') @API.route('/fork///pull-request/') @API.route('/fork////pull-request/') @api_method def api_pull_request_view(repo, requestid, username=None, namespace=None): """ Pull-request information ------------------------ Retrieve information of a specific pull request. :: GET /api/0//pull-request/ GET /api/0///pull-request/ :: GET /api/0/fork///pull-request/ GET /api/0/fork////pull-request/ Sample response ^^^^^^^^^^^^^^^ :: { "assignee": null, "branch": "master", "branch_from": "master", "closed_at": null, "closed_by": null, "comments": [], "commit_start": null, "commit_stop": null, "date_created": "1431414800", "id": 1, "project": { "date_created": "1431414800", "description": "test project #1", "id": 1, "name": "test", "parent": null, "user": { "fullname": "PY C", "name": "pingou" } }, "repo_from": { "date_created": "1431414800", "description": "test project #1", "id": 1, "name": "test", "parent": null, "user": { "fullname": "PY C", "name": "pingou" } }, "status": true, "title": "test pull-request", "uid": "1431414800", "updated_on": "1431414800", "user": { "fullname": "PY C", "name": "pingou" } } """ repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) request = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, requestid=requestid) if not request: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ) jsonout = flask.jsonify(request.to_json(public=True, api=True)) return jsonout @API.route('//pull-request//merge', methods=['POST']) @API.route( '///pull-request//merge', methods=['POST']) @API.route('/fork///pull-request//merge', methods=['POST']) @API.route( '/fork////pull-request//merge', methods=['POST']) @api_login_required(acls=['pull_request_merge']) @api_method def api_pull_request_merge(repo, requestid, username=None, namespace=None): """ Merge a pull-request -------------------- Instruct Paugre to merge a pull request. :: POST /api/0//pull-request//merge POST /api/0///pull-request//merge :: POST /api/0/fork///pull-request//merge POST /api/0/fork////pull-request//merge Sample response ^^^^^^^^^^^^^^^ :: { "message": "Changes merged!" } """ output = {} repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) if repo != flask.g.token.project: raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) request = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, requestid=requestid) if not request: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ) if not is_repo_admin(repo): raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE) if repo.settings.get('Only_assignee_can_merge_pull-request', False): if not request.assignee: raise pagure.exceptions.APIError( 403, error_code=APIERROR.ENOTASSIGNED) if request.assignee.username != flask.g.fas_user.username: raise pagure.exceptions.APIError( 403, error_code=APIERROR.ENOTASSIGNEE) threshold = repo.settings.get('Minimum_score_to_merge_pull-request', -1) if threshold > 0 and int(request.score) < int(threshold): raise pagure.exceptions.APIError(403, error_code=APIERROR.EPRSCORE) try: message = pagure.lib.git.merge_pull_request( SESSION, request, flask.g.fas_user.username, APP.config['REQUESTS_FOLDER']) output['message'] = message except pagure.exceptions.PagureException as err: raise pagure.exceptions.APIError( 400, error_code=APIERROR.ENOCODE, error=str(err)) jsonout = flask.jsonify(output) return jsonout @API.route('//pull-request//close', methods=['POST']) @API.route( '///pull-request//close', methods=['POST']) @API.route('/fork///pull-request//close', methods=['POST']) @API.route( '/fork////pull-request//close', methods=['POST']) @api_login_required(acls=['pull_request_close']) @api_method def api_pull_request_close(repo, requestid, username=None, namespace=None): """ Close a pull-request -------------------- Instruct Pagure to close a pull request. :: POST /api/0//pull-request//close POST /api/0///pull-request//close :: POST /api/0/fork///pull-request//close POST /api/0/fork////pull-request//close Sample response ^^^^^^^^^^^^^^^ :: { "message": "Pull-request closed!" } """ output = {} repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) if repo != flask.g.token.project: raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) request = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, requestid=requestid) if not request: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ) if not is_repo_admin(repo): raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE) try: pagure.lib.close_pull_request( SESSION, request, flask.g.fas_user.username, requestfolder=APP.config['REQUESTS_FOLDER'], merged=False) SESSION.commit() output['message'] = 'Pull-request closed!' except SQLAlchemyError as err: # pragma: no cover SESSION.rollback() APP.logger.exception(err) raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) jsonout = flask.jsonify(output) return jsonout @API.route('//pull-request//comment', methods=['POST']) @API.route('///pull-request//comment', methods=['POST']) @API.route('/fork///pull-request//comment', methods=['POST']) @API.route( '/fork////pull-request//comment', methods=['POST']) @api_login_required(acls=['pull_request_comment']) @api_method def api_pull_request_add_comment( repo, requestid, username=None, namespace=None): """ Comment on a pull-request ------------------------- Add comment to a pull request. :: POST /api/0//pull-request//comment POST /api/0///pull-request//comment :: POST /api/0/fork///pull-request//comment POST /api/0/fork////pull-request//comment Input ^^^^^ +---------------+---------+--------------+-----------------------------+ | Key | Type | Optionality | Description | +===============+=========+==============+=============================+ | ``comment`` | string | Mandatory | | The comment to add | | | | | to the pull request | +---------------+---------+--------------+-----------------------------+ | ``commit`` | string | Optional | | The hash of the specific | | | | | commit you wish to | | | | | comment on | +---------------+---------+--------------+-----------------------------+ | ``filename`` | string | Optional | | The filename of the | | | | | specific file you wish | | | | | to comment on | +---------------+---------+--------------+-----------------------------+ | ``row`` | int | Optional | | Used in combination | | | | | with filename to comment | | | | | on a specific row | | | | | of a file | +---------------+---------+--------------+-----------------------------+ | ``tree_id`` | string | Optional | | The identifier of the | | | | | git tree as it was when | | | | | the comment was added | +---------------+---------+--------------+-----------------------------+ Sample response ^^^^^^^^^^^^^^^ :: { "message": "Comment added" } """ repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) output = {} if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) if repo.fullname != flask.g.token.project.fullname: raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) request = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, requestid=requestid) if not request: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ) form = pagure.forms.AddPullRequestCommentForm(csrf_enabled=False) if form.validate_on_submit(): comment = form.comment.data commit = form.commit.data or None filename = form.filename.data or None tree_id = form.tree_id.data or None row = form.row.data or None try: # New comment message = pagure.lib.add_pull_request_comment( SESSION, request=request, commit=commit, tree_id=tree_id, filename=filename, row=row, comment=comment, user=flask.g.fas_user.username, requestfolder=APP.config['REQUESTS_FOLDER'], ) SESSION.commit() output['message'] = message except pagure.exceptions.PagureException as err: raise pagure.exceptions.APIError( 400, error_code=APIERROR.ENOCODE, error=str(err)) except SQLAlchemyError as err: # pragma: no cover APP.logger.exception(err) SESSION.rollback() raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) else: raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ) jsonout = flask.jsonify(output) return jsonout @API.route('//pull-request//flag', methods=['POST']) @API.route('///pull-request//flag', methods=['POST']) @API.route('/fork///pull-request//flag', methods=['POST']) @API.route( '/fork////pull-request//flag', methods=['POST']) @api_login_required(acls=['pull_request_flag']) @api_method def api_pull_request_add_flag(repo, requestid, username=None, namespace=None): """ Flag a pull-request ------------------- Add or edit flags on a pull-request. :: POST /api/0//pull-request//flag POST /api/0///pull-request//flag :: POST /api/0/fork///pull-request//flag POST /api/0/fork////pull-request//flag Input ^^^^^ +---------------+---------+--------------+-----------------------------+ | Key | Type | Optionality | Description | +===============+=========+==============+=============================+ | ``username`` | string | Mandatory | | The name of the | | | | | application to be | | | | | presented to users | | | | | on the pull request page | +---------------+---------+--------------+-----------------------------+ | ``percent`` | int | Mandatory | | A percentage of | | | | | completion compared to | | | | | the goal. The percentage | | | | | also determine the | | | | | background color of the | | | | | flag on the pull-request | | | | | page | +---------------+---------+--------------+-----------------------------+ | ``comment`` | string | Mandatory | | A short message | | | | | summarizing the | | | | | presented results | +---------------+---------+--------------+-----------------------------+ | ``url`` | string | Mandatory | | A URL to the result | | | | | of this flag | +---------------+---------+--------------+-----------------------------+ | ``uid`` | string | Optional | | A unique identifier used | | | | | to identify a flag on a | | | | | pull-request. If the | | | | | provided UID matches an | | | | | existing one, then the | | | | | API call will update the | | | | | existing one rather than | | | | | create a new one. | | | | | Maximum Length: 32 | | | | | characters. Default: an | | | | | auto generated UID | +---------------+---------+--------------+-----------------------------+ | ``commit`` | string | Optional | | The hash of the commit | | | | | you use | +---------------+---------+--------------+-----------------------------+ Sample response ^^^^^^^^^^^^^^^ :: { "message": "Flag added" } :: { "message": "Flag updated" } """ repo = pagure.lib.get_project( SESSION, repo, user=username, namespace=namespace) output = {} if repo is None: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) if not repo.settings.get('pull_requests', True): raise pagure.exceptions.APIError( 404, error_code=APIERROR.EPULLREQUESTSDISABLED) if repo.fullname != flask.g.token.project.fullname: raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) request = pagure.lib.search_pull_requests( SESSION, project_id=repo.id, requestid=requestid) if not request: raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ) form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False) if form.validate_on_submit(): username = form.username.data percent = form.percent.data comment = form.comment.data.strip() url = form.url.data.strip() uid = form.uid.data.strip() if form.uid.data else None try: # New Flag message = pagure.lib.add_pull_request_flag( SESSION, request=request, username=username, percent=percent, comment=comment, url=url, uid=uid, user=flask.g.fas_user.username, requestfolder=APP.config['REQUESTS_FOLDER'], ) SESSION.commit() output['message'] = message except pagure.exceptions.PagureException as err: raise pagure.exceptions.APIError( 400, error_code=APIERROR.ENOCODE, error=str(err)) except SQLAlchemyError as err: # pragma: no cover APP.logger.exception(err) SESSION.rollback() raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) else: raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ) jsonout = flask.jsonify(output) return jsonout