# -*- coding: utf-8 -*-
"""
(c) 2015-2017 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
"""
from __future__ import unicode_literals, absolute_import
import logging
import flask
import pygit2
from sqlalchemy.exc import SQLAlchemyError
import pagure
import pagure.exceptions
import pagure.lib.query
import pagure.lib.tasks
from pagure.api import (
API,
api_method,
api_login_required,
APIERROR,
get_authorized_api_project,
get_request_data,
get_page,
get_per_page,
)
from pagure.config import config as pagure_config
from pagure.utils import is_repo_committer, is_true
from pagure.api.utils import (
_get_repo,
_check_token,
_get_request,
_check_pull_request,
_check_pull_request_access,
)
_log = logging.getLogger(__name__)
@API.route("/<repo>/pull-requests")
@API.route("/<namespace>/<repo>/pull-requests")
@API.route("/fork/<username>/<repo>/pull-requests")
@API.route("/fork/<username>/<namespace>/<repo>/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/<repo>/pull-requests
GET /api/0/<namespace>/<repo>/pull-requests
::
GET /api/0/fork/<username>/<repo>/pull-requests
GET /api/0/fork/<username>/<namespace>/<repo>/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": "Open",
"title": "test pull-request",
"uid": "1431414800",
"updated_on": "1431414800",
"user": {
"fullname": "PY C",
"name": "pingou"
}
}
]
}
"""
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
status = flask.request.args.get("status", True)
assignee = flask.request.args.get("assignee", None)
author = flask.request.args.get("author", None)
status_text = ("%s" % status).lower()
requests = []
if status_text in ["0", "false"]:
requests = pagure.lib.query.search_pull_requests(
flask.g.session,
project_id=repo.id,
status=False,
assignee=assignee,
author=author,
)
elif status_text == "all":
requests = pagure.lib.query.search_pull_requests(
flask.g.session,
project_id=repo.id,
status=None,
assignee=assignee,
author=author,
)
else:
requests = pagure.lib.query.search_pull_requests(
flask.g.session,
project_id=repo.id,
assignee=assignee,
author=author,
status=status,
)
page = get_page()
per_page = get_per_page()
pagination_metadata = pagure.lib.query.get_pagination_metadata(
flask.request, page, per_page, len(requests)
)
start = (page - 1) * per_page
if start + per_page > len(requests):
requests_page = requests[start:]
else:
requests_page = requests[start : (start + per_page)]
jsonout = {
"total_requests": len(requests),
"requests": [
request.to_json(public=True, api=True) for request in requests_page
],
"args": {"status": status, "assignee": assignee, "author": author},
}
if pagination_metadata:
jsonout["args"]["page"] = page
jsonout["args"]["per_page"] = per_page
jsonout["pagination"] = pagination_metadata
return flask.jsonify(jsonout)
@API.route("/pull-requests/<uid>")
@api_method
def api_pull_request_by_uid_view(uid):
"""
Pull-request by UID information
-------------------------------
Retrieve information of a pull request specified by uid.
::
GET /api/0/pull-requests/<uid>
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": {
"close_status": [],
"custom_keys": [],
"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": "Open",
"title": "test pull-request",
"uid": "1431414800",
"updated_on": "1431414800",
"user": {
"fullname": "PY C",
"name": "pingou"
}
}
"""
request = _get_request(requestuid=uid)
# we don't really need the repo, but we need to make sure
# that we're allowed to access it
username = request.project.user.user if request.project.is_fork else None
repo = get_authorized_api_project(
flask.g.session,
request.project.name,
user=username,
namespace=request.project.namespace,
)
if repo is None:
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
if not repo.settings.get("pull_requests", True):
raise pagure.exceptions.APIError(
404, error_code=APIERROR.EPULLREQUESTSDISABLED
)
jsonout = flask.jsonify(request.to_json(public=True, api=True))
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>")
@API.route("/<namespace>/<repo>/pull-request/<int:requestid>")
@API.route("/fork/<username>/<repo>/pull-request/<int:requestid>")
@API.route("/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>")
@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/<repo>/pull-request/<request id>
GET /api/0/<namespace>/<repo>/pull-request/<request id>
::
GET /api/0/fork/<username>/<repo>/pull-request/<request id>
GET /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>
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": {
"close_status": [],
"custom_keys": [],
"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": "Open",
"title": "test pull-request",
"uid": "1431414800",
"updated_on": "1431414800",
"user": {
"fullname": "PY C",
"name": "pingou"
}
}
"""
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
request = _get_request(repo, requestid)
jsonout = flask.jsonify(request.to_json(public=True, api=True))
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>", methods=["POST"]
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>",
methods=["POST"],
)
@api_login_required(acls=["pull_request_update"])
@api_method
def api_pull_request_update(repo, requestid, username=None, namespace=None):
"""
Update pull-request information
-------------------------------
Update the title and initial comment of an existing pull-request.
::
POST /api/0/<repo>/pull-request/<request id>
POST /api/0/<namespace>/<repo>/pull-request/<request id>
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>
Input
^^^^^
+---------------------+--------+-------------+-----------------------------+
| Key | Type | Optionality | Description |
+=====================+========+=============+=============================+
| ``title`` | string | Mandatory | | The title to give to the |
| | | | pull-request |
+---------------------+--------+-------------+-----------------------------+
| ``initial_comment`` | string | Optional | | The initial comment or |
| | | | description of the |
| | | | 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": {
"close_status": [],
"custom_keys": [],
"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": "Open",
"title": "test pull-request",
"uid": "1431414800",
"updated_on": "1431414800",
"user": {
"fullname": "PY C",
"name": "pingou"
}
}
""" # noqa
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo, project_token=False)
request = _get_request(repo, requestid)
_check_pull_request_access(request, assignee=True)
form = pagure.forms.RequestPullForm(csrf_enabled=False)
if not form.validate_on_submit():
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
)
else:
request.title = form.title.data.strip()
request.initial_comment = form.initial_comment.data.strip()
flask.g.session.add(request)
if not request.private and not request.project.private:
pagure.lib.notify.log(
request.project,
topic="pull-request.initial_comment.edited",
msg={
"pullrequest": request.to_json(
public=True, with_comments=False
),
"project": request.project.to_json(public=True),
"agent": flask.g.fas_user.username,
},
)
try:
# Link the PR to issue(s) if there is such link
pagure.lib.query.link_pr_to_issue_on_description(
flask.g.session, request
)
flask.g.session.commit()
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
request = _get_request(repo, requestid)
jsonout = flask.jsonify(request.to_json(public=True, api=True))
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/merge", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/merge", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/merge",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/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 Pagure to merge a pull request.
This is an asynchronous call.
::
POST /api/0/<repo>/pull-request/<request id>/merge
POST /api/0/<namespace>/<repo>/pull-request/<request id>/merge
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/merge
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/merge
Sample response
^^^^^^^^^^^^^^^
::
wait=False:
{
"message": "Merging queued",
"taskid": "123-abcd"
}
wait=True:
{
"message": "Changes merged!"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo, project_token=False)
request = _get_request(repo, requestid)
if not is_repo_committer(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)
task = pagure.lib.tasks.merge_pull_request.delay(
repo.name, namespace, username, requestid, flask.g.fas_user.username
)
output = {"message": "Merging queued", "taskid": task.id}
if get_request_data().get("wait", True):
try:
task.get()
output = {"message": "Changes merged!"}
except pagure.exceptions.PagureException:
raise pagure.exceptions.APIError(
409, error_code=APIERROR.EPRCONFLICTS
)
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/rebase", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/rebase", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/rebase",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/rebase",
methods=["POST"],
)
@api_login_required(acls=["pull_request_rebase"])
@api_method
def api_pull_request_rebase(repo, requestid, username=None, namespace=None):
"""
Rebase a pull-request
---------------------
Instruct Pagure to rebase a pull request.
This is an asynchronous call.
::
POST /api/0/<repo>/pull-request/<request id>/rebase
POST /api/0/<namespace>/<repo>/pull-request/<request id>/rebase
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/rebase
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/rebase
Sample response
^^^^^^^^^^^^^^^
::
wait=False:
{
"message": "Rebasing queued",
"taskid": "123-abcd"
}
wait=True:
{
"message": "Pull-request rebased"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo)
request = _get_request(repo, requestid)
if not is_repo_committer(repo):
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
if not request.allow_rebase:
raise pagure.exceptions.APIError(
403, error_code=APIERROR.EREBASENOTALLOWED
)
task = pagure.lib.tasks.rebase_pull_request.delay(
repo.name,
namespace,
username,
requestid,
user_rebaser=flask.g.fas_user.username,
)
output = {"message": "Rebasing queued", "taskid": task.id}
if get_request_data().get("wait", True):
try:
task.get()
output = {"message": "Pull-request rebased"}
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("/<repo>/pull-request/<int:requestid>/close", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/close", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/close",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/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/<repo>/pull-request/<request id>/close
POST /api/0/<namespace>/<repo>/pull-request/<request id>/close
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/close
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/close
Sample response
^^^^^^^^^^^^^^^
::
{
"message": "Pull-request closed!"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo)
request = _get_request(repo, requestid)
if not is_repo_committer(repo):
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
try:
pagure.lib.query.close_pull_request(
flask.g.session, request, flask.g.fas_user.username, merged=False
)
flask.g.session.commit()
output["message"] = "Pull-request closed!"
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/comment", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/comment",
methods=["POST"],
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/comment",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/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/<repo>/pull-request/<request id>/comment
POST /api/0/<namespace>/<repo>/pull-request/<request id>/comment
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/comment
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/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"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo, project_token=False)
request = _get_request(repo, requestid)
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.query.add_pull_request_comment(
flask.g.session,
request=request,
commit=commit,
tree_id=tree_id,
filename=filename,
row=row,
comment=comment,
user=flask.g.fas_user.username,
)
flask.g.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
_log.exception(err)
flask.g.session.rollback()
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
else:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
)
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/flag", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/flag", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/flag",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/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/<repo>/pull-request/<request id>/flag
POST /api/0/<namespace>/<repo>/pull-request/<request id>/flag
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/flag
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/flag
Input
^^^^^
+---------------+---------+--------------+-----------------------------+
| Key | Type | Optionality | Description |
+===============+=========+==============+=============================+
| ``username`` | string | Mandatory | | The name of the |
| | | | application to be |
| | | | presented to users |
| | | | 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 |
+---------------+---------+--------------+-----------------------------+
| ``status`` | string | Optional | | The status of the task, |
| | | | can be any of: |
| | | | $$FLAG_STATUSES_COMMAS$$ |
| | | | If not provided it will |
| | | | be set to |
| | | | ``$$FLAG_SUCCESS$$`` if |
| | | | percent is higher than 0 |
| | | | ``$$FLAG_FAILURE$$`` if |
| | | | it is 0 and |
| | | | ``$$FLAG_PENDING$$`` |
| | | | if percent is not |
| | | | specified |
+---------------+---------+--------------+-----------------------------+
| ``percent`` | int | Optional | | A percentage of |
| | | | completion compared to |
| | | | the goal. The percentage |
| | | | also determine the |
| | | | background color of the |
| | | | flag on the pull-request |
| | | | page |
+---------------+---------+--------------+-----------------------------+
| ``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 |
+---------------+---------+--------------+-----------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"flag": {
"comment": "Tests failed",
"date_created": "1510742565",
"percent": 0,
"pull_request_uid": "62b49f00d489452994de5010565fab81",
"status": "error",
"url": "http://jenkins.cloud.fedoraproject.org/",
"user": {
"default_email": "bar@pingou.com",
"emails": ["bar@pingou.com", "foo@pingou.com"],
"fullname": "PY C",
"name": "pingou"},
"username": "Jenkins"},
"message": u"Flag added",
"uid": u"jenkins_build_pagure_100+seed"
}
::
{
"flag": {
"comment": "Tests failed",
"date_created": "1510742565",
"percent": 0,
"pull_request_uid": "62b49f00d489452994de5010565fab81",
"status": "error",
"url": "http://jenkins.cloud.fedoraproject.org/",
"user": {
"default_email": "bar@pingou.com",
"emails": ["bar@pingou.com", "foo@pingou.com"],
"fullname": "PY C",
"name": "pingou"},
"username": "Jenkins"},
"message": u"Flag updated",
"uid": u"jenkins_build_pagure_100+seed"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo, project_token=False)
request = _get_request(repo, requestid)
if "status" in get_request_data():
form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
else:
form = pagure.forms.AddPullRequestFlagFormV1(csrf_enabled=False)
if form.validate_on_submit():
username = form.username.data
percent = form.percent.data.strip() or None
comment = form.comment.data.strip()
url = form.url.data.strip()
uid = form.uid.data.strip() if form.uid.data else None
if "status" in get_request_data():
status = form.status.data.strip()
else:
if percent is None:
status = pagure_config["FLAG_PENDING"]
else:
status = (
pagure_config["FLAG_SUCCESS"]
if percent != "0"
else pagure_config["FLAG_FAILURE"]
)
try:
# New Flag
message, uid = pagure.lib.query.add_pull_request_flag(
flask.g.session,
request=request,
username=username,
status=status,
percent=percent,
comment=comment,
url=url,
uid=uid,
user=flask.g.fas_user.username,
token=flask.g.token.id,
)
flask.g.session.commit()
pr_flag = pagure.lib.query.get_pull_request_flag_by_uid(
flask.g.session, request, uid
)
output["message"] = message
output["uid"] = uid
output["flag"] = pr_flag.to_json()
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
_log.exception(err)
flask.g.session.rollback()
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
else:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
)
output["avatar_url"] = pagure.lib.query.avatar_url_from_email(
flask.g.fas_user.default_email, size=30
)
output["user"] = flask.g.fas_user.username
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/flag", methods=["GET"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/flag", methods=["GET"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/flag",
methods=["GET"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/flag",
methods=["GET"],
)
@api_method
def api_pull_request_get_flag(repo, requestid, username=None, namespace=None):
"""
Get flag(s) of a pull-request
-----------------------------
Retrieve the flags on a pull-request.
::
GET /api/0/<repo>/pull-request/<request id>/flag
GET /api/0/<namespace>/<repo>/pull-request/<request id>/flag
::
GET /api/0/fork/<username>/<repo>/pull-request/<request id>/flag
GET /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/flag
Sample response
^^^^^^^^^^^^^^^
::
{
"flags": [
{
"comment": "Tests are running in the AtomicCI pipeline",
"date_created": "1537560168",
"percent": null,
"pull_request_uid": "4fb1f8db8f114baeb943b6f10c5de015",
"status": "failure",
"url": "https://jenkins-continuous-infra.apps.ci.centos.org/job/continuous-infra-ci-pipeline-f26/...",
"user": {
"fullname": "Pierre-YvesChibon",
"name": "pingou"
},
"username": "AtomicCI"
},
{
"comment": "Built successfully",
"date_created": "1517565878",
"percent": 100,
"pull_request_uid": "4fb1f8db8f114baeb943b6f10c5de015",
"status": "success",
"url": "https://koji.fedoraproject.org/koji/...",
"user": {
"fullname": "Pierre-YvesChibon",
"name": "pingou"
},
"username": "simple-koji-ci"
}
]
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
request = _get_request(repo, requestid)
output = {"flags": []}
for flag in request.flags:
output["flags"].append(flag.to_json(public=True))
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/subscribe", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/subscribe",
methods=["POST"],
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/subscribe",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>"
"/subscribe",
methods=["POST"],
)
@api_login_required(acls=["pull_request_subscribe"])
@api_method
def api_subscribe_pull_request(repo, requestid, username=None, namespace=None):
"""
Subscribe to an pull-request
----------------------------
Allows someone to subscribe to or unsubscribe from the notifications
related to a pull-request.
::
POST /api/0/<repo>/pull-request/<request id>/subscribe
POST /api/0/<namespace>/<repo>/pull-request/<request id>/subscribe
::
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/subscribe
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/subscribe
Input
^^^^^
+--------------+----------+---------------+---------------------------+
| Key | Type | Optionality | Description |
+==============+==========+===============+===========================+
| ``status`` | boolean | Mandatory | The intended subscription |
| | | | status. ``true`` for |
| | | | subscribing, ``false`` |
| | | | for unsubscribing. |
+--------------+----------+---------------+---------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"message": "User subscribed",
"avatar_url": "https://image.png",
"user": "pingou"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo)
request = _get_request(repo, requestid)
form = pagure.forms.SubscribtionForm(csrf_enabled=False)
if form.validate_on_submit():
status = is_true(form.status.data)
try:
# Toggle subscribtion
message = pagure.lib.query.set_watch_obj(
flask.g.session,
user=flask.g.fas_user.username,
obj=request,
watch_status=status,
)
flask.g.session.commit()
output["message"] = message
user_obj = pagure.lib.query.get_user(
flask.g.session, flask.g.fas_user.username
)
output["avatar_url"] = pagure.lib.query.avatar_url_from_email(
user_obj.default_email, size=30
)
output["user"] = flask.g.fas_user.username
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.logger.exception(err)
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/new", methods=["POST"])
@API.route("/<namespace>/<repo>/pull-request/new", methods=["POST"])
@API.route("/fork/<username>/<repo>/pull-request/new", methods=["POST"])
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/new", methods=["POST"]
)
@api_login_required(acls=["pull_request_create"])
@api_method
def api_pull_request_create(repo, username=None, namespace=None):
"""
Create pull-request
-------------------
Open a new pull-request from this project to itself or its parent (if
this project is a fork).
::
POST /api/0/<repo>/pull-request/new
POST /api/0/<namespace>/<repo>/pull-request/new
::
POST /api/0/fork/<username>/<repo>/pull-request/new
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/new
Input
^^^^^
+-----------------------+----------+-------------+------------------------+
| Key | Type | Optionality | Description |
+=======================+==========+=============+========================+
| ``title`` | string | Mandatory | The title to give to |
| | | | this pull-request |
+-----------------------+----------+-------------+------------------------+
| ``branch_to`` | string | Mandatory | The name of the branch |
| | | | the submitted changes |
| | | | should be merged into. |
+-----------------------+----------+-------------+------------------------+
| ``branch_from`` | string | Mandatory | The name of the branch |
| | | | containing the changes |
| | | | to merge |
+-----------------------+----------+-------------+------------------------+
| ``repo_from`` | string | Optional | The name of the project|
| | | | the changes originate |
| | | | from. |
| | | | If not specified the |
| | | | repo_from is assumed |
| | | | to be the repo_to. |
+-----------------------+----------+-------------+------------------------+
| ``repo_from_username``| string | Optional | The username of the |
| | | | project the changes |
| | | | originate from. |
| | | | If not specified the |
| | | | repo_from is assumed |
| | | | to be the repo_to. |
+-----------------------+----------+-------------+------------------------+
|``repo_from_namespace``| string | Optional | The namespace of the |
| | | | project the changes |
| | | | originate from. |
| | | | If not specified the |
| | | | repo_from is assumed |
| | | | to be the repo_to. |
+-----------------------+----------+-------------+------------------------+
| ``initial_comment`` | string | Optional | The intial comment |
| | | | describing what these |
| | | | changes are about. |
+-----------------------+----------+-------------+------------------------+
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": {
"close_status": [],
"custom_keys": [],
"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": "Open",
"title": "test pull-request",
"uid": "1431414800",
"updated_on": "1431414800",
"user": {
"fullname": "PY C",
"name": "pingou"
}
}
""" # noqa
repo_to = _get_repo(repo, username, namespace)
req_data = get_request_data()
repo_from = req_data.get("repo_from")
repo_from_username = req_data.get("repo_from_username")
repo_from_namespace = req_data.get("repo_from_namespace")
if repo_from:
repo_from = _get_repo(
repo_from,
username=repo_from_username,
namespace=repo_from_namespace,
)
else:
repo_from = repo_to
_check_pull_request(repo_to)
_check_token(repo_from, project_token=False)
form = pagure.forms.RequestPullForm(csrf_enabled=False)
if not form.validate_on_submit():
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
)
branch_to = get_request_data().get("branch_to")
if not branch_to:
raise pagure.exceptions.APIError(
400,
error_code=APIERROR.EINVALIDREQ,
errors={"branch_to": ["This field is required."]},
)
branch_from = get_request_data().get("branch_from")
if not branch_from:
raise pagure.exceptions.APIError(
400,
error_code=APIERROR.EINVALIDREQ,
errors={"branch_from": ["This field is required."]},
)
if not repo_to.settings.get("pull_requests", True):
raise pagure.exceptions.APIError(
404, error_code=APIERROR.EPULLREQUESTSDISABLED
)
repo_committer = pagure.utils.is_repo_committer(repo_from)
if not repo_committer:
raise pagure.exceptions.APIError(
401, error_code=APIERROR.ENOTHIGHENOUGH
)
git_repo_from = pygit2.Repository(repo_from.repopath("main"))
git_repo_to = pygit2.Repository(repo_to.repopath("main"))
try:
diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info(
git_repo_from, git_repo_to, branch_from, branch_to
)
except pagure.exceptions.PagureException as err:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.EINVALIDREQ, errors=str(err)
)
if repo_to.settings.get(
"Enforce_signed-off_commits_in_pull-request", False
):
for commit in diff_commits:
if "signed-off-by" not in commit.message.lower():
raise pagure.exceptions.APIError(
400, error_code=APIERROR.ENOSIGNEDOFF
)
if orig_commit:
orig_commit = orig_commit.oid.hex
initial_comment = form.initial_comment.data.strip() or None
commit_start = commit_stop = None
if diff_commits:
commit_stop = diff_commits[0].oid.hex
commit_start = diff_commits[-1].oid.hex
request = pagure.lib.query.new_pull_request(
flask.g.session,
repo_to=repo_to,
branch_to=branch_to,
branch_from=branch_from,
repo_from=repo_from,
title=form.title.data,
initial_comment=initial_comment,
user=flask.g.fas_user.username,
commit_start=commit_start,
commit_stop=commit_stop,
)
try:
flask.g.session.commit()
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.logger.exception(err)
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
jsonout = flask.jsonify(request.to_json(public=True, api=True))
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/diffstats")
@API.route("/<namespace>/<repo>/pull-request/<int:requestid>/diffstats")
@API.route("/fork/<username>/<repo>/pull-request/<int:requestid>/diffstats")
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/"
"diffstats"
)
@api_method
def api_pull_request_diffstats(repo, requestid, username=None, namespace=None):
"""
Pull-request diff statistics
----------------------------
Retrieve the statistics about the diff of a specific pull request.
::
GET /api/0/<repo>/pull-request/<request id>/diffstats
GET /api/0/<namespace>/<repo>/pull-request/<request id>/diffstats
::
GET /api/0/fork/<username>/<repo>/pull-request/<request id>/diffstats
GET /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/diffstats
Sample response
^^^^^^^^^^^^^^^
::
{
"README.rst": {
"lines_added": 1,
"lines_removed": 1,
"old_path": "README.rst",
"status": "M"
},
"blame_file.txt": {
"lines_added": 0,
"lines_removed": 0,
"old_path": "blame_file",
"status": "R"
},
"test": {
"lines_added": 0,
"lines_removed": 8,
"old_path": "test",
"status": "D"
},
"test3": {
"lines_added": 3,
"lines_removed": 0,
"old_path": "test3",
"status": "A"
}
}
""" # noqa
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
request = _get_request(repo, requestid)
repopath = None
parentpath = pagure.utils.get_repo_path(request.project)
if request.remote:
repopath = pagure.utils.get_remote_repo_path(
request.remote_git, request.branch_from
)
elif request.project_from:
repopath = pagure.utils.get_repo_path(request.project_from)
repo_obj = None
if repopath:
repo_obj = pygit2.Repository(repopath)
orig_repo = pygit2.Repository(parentpath)
diff_commits = []
diff = None
# Closed pull-request
if request.status != "Open":
commitid = request.commit_stop
try:
for commit in repo_obj.walk(commitid, pygit2.GIT_SORT_NONE):
diff_commits.append(commit)
if commit.oid.hex == request.commit_start:
break
except KeyError:
# This happens when repo.walk() cannot find commitid
pass
if diff_commits:
# Ensure the first commit in the PR as a parent, otherwise
# point to it
start = diff_commits[-1].oid.hex
if diff_commits[-1].parents:
start = diff_commits[-1].parents[0].oid.hex
# If the start and the end commits are the same, it means we are,
# dealing with one commit that has no parent, so just diff that
# one commit
if start == diff_commits[0].oid.hex:
diff = diff_commits[0].tree.diff_to_tree(swap=True)
else:
diff = repo_obj.diff(
repo_obj.revparse_single(start),
repo_obj.revparse_single(diff_commits[0].oid.hex),
)
else:
try:
diff_commits, diff = pagure.lib.git.diff_pull_request(
flask.g.session, request, repo_obj, orig_repo
)
except pagure.exceptions.PagureException as err:
flask.flash("%s" % err, "error")
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
flask.flash(
"Could not update this pull-request in the database", "error"
)
if diff:
diff.find_similar()
output = {}
if diff:
for patch in diff:
stats = pagure.lib.git.get_stats_patch(patch)
new_path = stats["new_path"]
del stats["new_path"]
output[new_path] = stats
else:
raise pagure.exceptions.APIError(400, error_code=APIERROR.ENOPRSTATS)
jsonout = flask.jsonify(output)
return jsonout
@API.route("/<repo>/pull-request/<int:requestid>/assign", methods=["POST"])
@API.route(
"/<namespace>/<repo>/pull-request/<int:requestid>/assign", methods=["POST"]
)
@API.route(
"/fork/<username>/<repo>/pull-request/<int:requestid>/assign",
methods=["POST"],
)
@API.route(
"/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/assign",
methods=["POST"],
)
@api_login_required(acls=["pull_request_assign", "pull_request_update"])
@api_method
def api_pull_request_assign(repo, requestid, username=None, namespace=None):
"""
Assign a pull-request
---------------------
Assign a pull-request to someone.
::
POST /api/0/<repo>/pull-request/<issue id>/assign
POST /api/0/<namespace>/<repo>/pull-request/<issue id>/assign
::
POST /api/0/fork/<username>/<repo>/pull-request/<issue id>/assign
POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<issue id>/assign
Input
^^^^^
+--------------+----------+---------------+---------------------------+
| Key | Type | Optionality | Description |
+==============+==========+===============+===========================+
| ``assignee`` | string | Mandatory | | The username of the user|
| | | | to assign the PR to. |
+--------------+----------+---------------+---------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"message": "pull-request assigned"
}
""" # noqa
output = {}
repo = _get_repo(repo, username, namespace)
_check_pull_request(repo)
_check_token(repo)
request = _get_request(repo, requestid)
_check_pull_request_access(request, assignee=True)
form = pagure.forms.AssignIssueForm(csrf_enabled=False)
if form.validate_on_submit():
assignee = form.assignee.data or None
# Create our metadata comment object
try:
# New comment
message = pagure.lib.query.add_pull_request_assignee(
flask.g.session,
request=request,
assignee=assignee,
user=flask.g.fas_user.username,
)
flask.g.session.commit()
if message:
pagure.lib.query.add_metadata_update_notif(
session=flask.g.session,
obj=request,
messages=message,
user=flask.g.fas_user.username,
)
output["message"] = message
else:
output["message"] = "Nothing to change"
except pagure.exceptions.PagureException as err: # pragma: no cover
raise pagure.exceptions.APIError(
400, error_code=APIERROR.ENOCODE, error=str(err)
)
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
jsonout = flask.jsonify(output)
return jsonout