Blob Blame Raw
# -*- coding: utf-8 -*-

"""
 (c) 2015 - Copyright Red Hat Inc

 Authors:
   Pierre-Yves Chibon <pingou@pingoured.fr>

"""

import flask

from sqlalchemy.exc import SQLAlchemyError

import pagure
import pagure.exceptions
import pagure.lib
import pagure.lib.git
from pagure import SESSION, APP, authenticated
from pagure.api import (API, api_method, APIERROR, api_login_required,
                        get_authorized_api_project, api_login_optional)


@API.route('/<repo>/git/tags')
@API.route('/<namespace>/<repo>/git/tags')
@API.route('/fork/<username>/<repo>/git/tags')
@API.route('/fork/<username>/<namespace>/<repo>/git/tags')
@api_method
def api_git_tags(repo, username=None, namespace=None):
    """
    Project git tags
    ----------------
    List the tags made on the project Git repository.

    ::

        GET /api/0/<repo>/git/tags
        GET /api/0/<namespace>/<repo>/git/tags

    ::

        GET /api/0/fork/<username>/<repo>/git/tags
        GET /api/0/fork/<username>/<namespace>/<repo>/git/tags

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_tags": 2,
          "tags": ["0.0.1", "0.0.2"]
        }

    """
    repo = get_authorized_api_project(
        SESSION, repo, user=username, namespace=namespace)
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    tags = pagure.lib.git.get_git_tags(repo)

    jsonout = flask.jsonify({
        'total_tags': len(tags),
        'tags': tags
    })
    return jsonout


@API.route('/<repo>/watchers')
@API.route('/<namespace>/<repo>/watchers')
@API.route('/fork/<username>/<repo>/watchers')
@API.route('/fork/<username>/<namespace>/<repo>/watchers')
@api_method
def api_project_watchers(repo, username=None, namespace=None):
    '''
    Project watchers
    ----------------
    List the watchers on the project.

    ::

        GET /api/0/<repo>/watchers
        GET /api/0/<namespace>/<repo>/watchers

    ::

        GET /api/0/fork/<username>/<repo>/watchers
        GET /api/0/fork/<username>/<namespace>/<repo>/watchers

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
            "total_watchers": 1,
            "watchers": {
                "mprahl": [
                    "issues",
                    "commits"
                ]
            }
        }
    '''
    repo = get_authorized_api_project(
        SESSION, repo, user=username, namespace=namespace)
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    implicit_watch_users = {repo.user.username}
    for access_type in repo.access_users.keys():
        implicit_watch_users = \
            implicit_watch_users | set(
                [user.username for user in repo.access_users[access_type]])
    for access_type in repo.access_groups.keys():
        group_names = [group.group_name
                       for group in repo.access_groups[access_type]]
        for group_name in group_names:
            group = pagure.lib.search_groups(SESSION, group_name=group_name)
            implicit_watch_users = \
                implicit_watch_users | set([
                    user.username for user in group.users])

    watching_users_to_watch_level = {}
    for implicit_watch_user in implicit_watch_users:
        user_watch_level = pagure.lib.get_watch_level_on_repo(
            SESSION, implicit_watch_user, repo)
        watching_users_to_watch_level[implicit_watch_user] = user_watch_level

    # Get the explicit watch statuses
    for watcher in repo.watchers:
        if watcher.watch_issues or watcher.watch_commits:
            watching_users_to_watch_level[watcher.user.username] = \
                pagure.lib.get_watch_level_on_repo(
                    SESSION, watcher.user.username, repo)
        else:
            if watcher.user.username in watching_users_to_watch_level:
                watching_users_to_watch_level.pop(watcher.user.username, None)

    return flask.jsonify({
        'total_watchers': len(watching_users_to_watch_level),
        'watchers': watching_users_to_watch_level
    })


@API.route('/<repo>/git/urls')
@API.route('/<namespace>/<repo>/git/urls')
@API.route('/fork/<username>/<repo>/git/urls')
@API.route('/fork/<username>/<namespace>/<repo>/git/urls')
@api_login_optional()
@api_method
def api_project_git_urls(repo, username=None, namespace=None):
    '''
    Project Git URLs
    ----------------
    List the Git URLS on the project.

    ::

        GET /api/0/<repo>/git/urls
        GET /api/0/<namespace>/<repo>/git/urls

    ::

        GET /api/0/fork/<username>/<repo>/git/urls
        GET /api/0/fork/<username>/<namespace>/<repo>/git/urls

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
            "total_urls": 2,
            "urls": {
                "ssh": "ssh://git@pagure.io/mprahl-test123.git",
                "git": "https://pagure.io/mprahl-test123.git"
            }
        }
    '''
    repo = get_authorized_api_project(
        SESSION, repo, user=username, namespace=namespace)
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
    git_urls = {}
    if pagure.APP.config.get('GIT_URL_SSH'):
        git_urls['ssh'] = '{0}{1}.git'.format(
            pagure.APP.config['GIT_URL_SSH'], repo.fullname)
    if pagure.APP.config.get('GIT_URL_GIT'):
        git_urls['git'] = '{0}{1}.git'.format(
            pagure.APP.config['GIT_URL_GIT'], repo.fullname)

    return flask.jsonify({
        'total_urls': len(git_urls),
        "urls": git_urls
    })


@API.route('/<repo>/git/branches')
@API.route('/<namespace>/<repo>/git/branches')
@API.route('/fork/<username>/<repo>/git/branches')
@API.route('/fork/<username>/<namespace>/<repo>/git/branches')
@api_method
def api_git_branches(repo, username=None, namespace=None):
    '''
    List project branches
    ---------------------
    List the branches associated with a Pagure git repository

    ::

        GET /api/0/<repo>/git/branches
        GET /api/0/<namespace>/<repo>/git/branches

    ::

        GET /api/0/fork/<username>/<repo>/git/branches
        GET /api/0/fork/<username>/<namespace>/<repo>/git/branches

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_branches": 2,
          "branches": ["master", "dev"]
        }

    '''
    repo = get_authorized_api_project(
        SESSION, repo, user=username, namespace=namespace)
    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    branches = pagure.lib.git.get_git_branches(repo)

    return flask.jsonify(
        {
            'total_branches': len(branches),
            'branches': branches
        }
    )


@API.route('/projects')
@api_method
def api_projects():
    """
    List projects
    --------------
    Search projects given the specified criterias.

    ::

        GET /api/0/projects

    ::

        GET /api/0/projects?tags=fedora-infra

    ::

        GET /api/0/projects?page=1&per_page=50

    Parameters
    ^^^^^^^^^^

    +---------------+----------+---------------+--------------------------+
    | Key           | Type     | Optionality   | Description              |
    +===============+==========+===============+==========================+
    | ``tags``      | string   | Optional      | | Filters the projects   |
    |               |          |               |   returned by their tags |
    +---------------+----------+---------------+--------------------------+
    | ``pattern``   | string   | Optional      | | Filters the projects   |
    |               |          |               |   by the pattern string  |
    +---------------+----------+---------------+--------------------------+
    | ``username``  | string   | Optional      | | Filters the projects   |
    |               |          |               |   returned by the users  |
    |               |          |               |   having commit rights   |
    |               |          |               |   to it                  |
    +---------------+----------+---------------+--------------------------+
    | ``owner``     | string   | Optional      | | Filters the projects   |
    |               |          |               |   by ownership           |
    +---------------+----------+---------------+--------------------------+
    | ``namespace`` | string   | Optional      | | Filters the projects   |
    |               |          |               |   by namespace           |
    +---------------+----------+---------------+--------------------------+
    | ``fork``      | boolean  | Optional      | | Filters the projects   |
    |               |          |               |   returned depending if  |
    |               |          |               |   they are forks or not  |
    +---------------+----------+---------------+--------------------------+
    | ``short``     | boolean  | Optional      | | Whether to return the  |
    |               |          |               |   entrie project JSON    |
    |               |          |               |   or just a sub-set      |
    +---------------+----------+---------------+--------------------------+
    | ``page``      | int      | Optional      | | Specifies that         |
    |               |          |               |   pagination should be   |
    |               |          |               |   turned on and that     |
    |               |          |               |   this specific page     |
    |               |          |               |   should be displayed    |
    +---------------+----------+---------------+--------------------------+
    | ``per_page``  | int      | Optional      | | The number of projects |
    |               |          |               |   to return per page.    |
    |               |          |               |   The maximum is 100.    |
    +---------------+----------+---------------+--------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_projects": 2,
          "projects": [
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [
                  "some_user"
                ],
                "owner": [
                  "pingou"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1427441537",
              "date_modified": "1427441537",
              "description": "A web-based calendar for Fedora",
              "milestones": {},
              "namespace": null,
              "id": 7,
              "name": "fedocal",
              "fullname": "fedocal",
              "parent": null,
              "priorities": {},
              "tags": [],
              "user": {
                "fullname": "Pierre-Yves C",
                "name": "pingou"
              }
            },
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [],
                "owner": [
                  "pingou"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1431666007",
              "description": "An awesome messaging servicefor everyone",
              "id": 12,
              "milestones": {},
              "name": "fedmsg",
              "namespace": null,
              "fullname": "forks/pingou/fedmsg",
              "parent": {
                "date_created": "1433423298",
                "description": "An awesome messaging servicefor everyone",
                "id": 11,
                "name": "fedmsg",
                "fullname": "fedmsg",
                "parent": null,
                "user": {
                  "fullname": "Ralph B",
                  "name": "ralph"
                }
              },
              "priorities": {},
              "tags": [],
              "user": {
                "fullname": "Pierre-Yves C",
                "name": "pingou"
              }
            }
          ]
        }

    Sample Response With Pagination
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    ::

        {
          "args": {
            "fork": null,
            "namespace": null,
            "owner": null,
            "page": 1,
            "pattern": null,
            "per_page": 2,
            "short": false,
            "tags": [],
            "username": null
          },
          "pagination": {
            "first": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=1",
            "last": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=500",
            "next": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=2",
            "page": 1,
            "pages": 500,
            "per_page": 2,
            "prev": null
          },
          "projects": [
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [],
                "owner": [
                  "mprahl"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1498841289",
              "description": "test1",
              "fullname": "test1",
              "id": 1,
              "milestones": {},
              "name": "test1",
              "namespace": null,
              "parent": null,
              "priorities": {},
              "tags": [],
              "user": {
                "fullname": "Matt Prahl",
                "name": "mprahl"
              }
            },
            {
              "access_groups": {
                "admin": [],
                "commit": [],
                "ticket": []
              },
              "access_users": {
                "admin": [],
                "commit": [],
                "owner": [
                  "mprahl"
                ],
                "ticket": []
              },
              "close_status": [],
              "custom_keys": [],
              "date_created": "1499795310",
              "description": "test2",
              "fullname": "test2",
              "id": 2,
              "milestones": {},
              "name": "test2",
              "namespace": null,
              "parent": null,
              "priorities": {},
              "tags": [],
              "user": {
                "fullname": "Matt Prahl",
                "name": "mprahl"
              }
            }
          ],
          "total_projects": 1000
        }
    """
    tags = flask.request.values.getlist('tags')
    username = flask.request.values.get('username', None)
    fork = flask.request.values.get('fork', None)
    namespace = flask.request.values.get('namespace', None)
    owner = flask.request.values.get('owner', None)
    pattern = flask.request.values.get('pattern', None)
    short = flask.request.values.get('short', None)
    page = flask.request.values.get('page', None)
    per_page = flask.request.values.get('per_page', None)

    if str(fork).lower() in ['1', 'true']:
        fork = True
    elif str(fork).lower() in ['0', 'false']:
        fork = False
    if str(short).lower() in ['1', 'true']:
        short = True
    else:
        short = False

    private = False
    if authenticated() and username == flask.g.fas_user.username:
        private = flask.g.fas_user.username

    project_count = pagure.lib.search_projects(
        SESSION, username=username, fork=fork, tags=tags, pattern=pattern,
        private=private, namespace=namespace, owner=owner, count=True)
    # Pagination code inspired by Flask-SQLAlchemy
    pagination_metadata = None
    query_start = None
    query_limit = None
    if page:
        try:
            page = int(page)
        except (TypeError, ValueError):
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.EINVALIDREQ)

        if page < 1:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.EINVALIDREQ)

        if per_page:
            try:
                per_page = int(per_page)
            except (TypeError, ValueError):
                raise pagure.exceptions.APIError(
                    400, error_code=APIERROR.EINVALIDREQ)

            if per_page < 1 or per_page > 100:
                raise pagure.exceptions.APIError(
                    400, error_code=APIERROR.EINVALIDPERPAGEVALUE)
        else:
            per_page = 20

        pagination_metadata = pagure.lib.get_pagination_metadata(
            flask.request, page, per_page, project_count)
        query_start = (page - 1) * per_page
        query_limit = per_page

    projects = pagure.lib.search_projects(
        SESSION, username=username, fork=fork, tags=tags, pattern=pattern,
        private=private, namespace=namespace, owner=owner, limit=query_limit,
        start=query_start)

    if not projects:
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ENOPROJECTS)

    if not short:
        projects = [p.to_json(api=True, public=True) for p in projects]
    else:
        projects = [
            {
                'name': p.name,
                'namespace': p.namespace,
                'fullname': p.fullname.replace('forks/', 'fork/', 1)
                if p.fullname.startswith('forks/') else p.fullname,
                'description': p.description,
            }
            for p in projects
        ]

    jsonout = {
        'total_projects': project_count,
        'projects': projects,
        'args': {
            'tags': tags,
            'username': username,
            'fork': fork,
            'pattern': pattern,
            'namespace': namespace,
            'owner': owner,
            'short': short,
        }
    }
    if pagination_metadata:
        jsonout['args']['page'] = page
        jsonout['args']['per_page'] = per_page
        jsonout['pagination'] = pagination_metadata
    return flask.jsonify(jsonout)


@API.route('/<repo>')
@API.route('/<namespace>/<repo>')
@API.route('/fork/<username>/<repo>')
@API.route('/fork/<username>/<namespace>/<repo>')
@api_method
def api_project(repo, username=None, namespace=None):
    """
    Project information
    -------------------
    Return information about a specific project

    ::

        GET /api/0/<repo>
        GET /api/0/<namespace>/<repo>

    ::

        GET /api/0/fork/<username>/<repo>
        GET /api/0/fork/<username>/<namespace>/<repo>

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "total_tags": 2,
          "tags": ["0.0.1", "0.0.2"]
        }

    """
    repo = get_authorized_api_project(
        SESSION, repo, user=username, namespace=namespace)

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    jsonout = flask.jsonify(repo.to_json(api=True, public=True))
    return jsonout


@API.route('/new/', methods=['POST'])
@API.route('/new', methods=['POST'])
@api_login_required(acls=['create_project'])
@api_method
def api_new_project():
    """
    Create a new project
    --------------------
    Create a new project on this pagure instance.

    This is an asynchronous call.

    ::

        POST /api/0/new


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``name``         | string  | Mandatory    | | The name of the new     |
    |                  |         |              |   project.                |
    +------------------+---------+--------------+---------------------------+
    | ``description``  | string  | Mandatory    | | A short description of  |
    |                  |         |              |   the new project.        |
    +------------------+---------+--------------+---------------------------+
    | ``namespace``    | string  | Optional     | | The namespace of the    |
    |                  |         |              |   project to fork.        |
    +------------------+---------+--------------+---------------------------+
    | ``url``          | string  | Optional     | | An url providing more   |
    |                  |         |              |   information about the   |
    |                  |         |              |   project.                |
    +------------------+---------+--------------+---------------------------+
    | ``avatar_email`` | string  | Optional     | | An email address for the|
    |                  |         |              |   avatar of the project.  |
    +------------------+---------+--------------+---------------------------+
    | ``create_readme``| boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   there should be a readme|
    |                  |         |              |   added to the project on |
    |                  |         |              |   creation.               |
    +------------------+---------+--------------+---------------------------+
    | ``private``      | boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   the project to create   |
    |                  |         |              |   is private.             |
    |                  |         |              |   Note: not all pagure    |
    |                  |         |              |   instance support private|
    |                  |         |              |   projects, confirm this  |
    |                  |         |              |   with your administrators|
    +------------------+---------+--------------+---------------------------+
    | ``wait``         | boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   this API call should    |
    |                  |         |              |   return a taskid or if it|
    |                  |         |              |   should wait for the task|
    |                  |         |              |   to finish.              |
    +------------------+---------+--------------+---------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        wait=False:
        {
          'message': 'Project creation queued',
          'taskid': '123-abcd'
        }

        wait=True:
        {
          'message': 'Project creation queued'
        }

    """
    user = pagure.lib.search_user(SESSION, username=flask.g.fas_user.username)
    output = {}

    if not pagure.APP.config.get('ENABLE_NEW_PROJECTS', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ENEWPROJECTDISABLED)

    namespaces = APP.config['ALLOWED_PREFIX'][:]
    if user:
        namespaces.extend([grp for grp in user.groups])

    form = pagure.forms.ProjectForm(
        namespaces=namespaces, csrf_enabled=False)
    if form.validate_on_submit():
        name = form.name.data
        description = form.description.data
        namespace = form.namespace.data
        url = form.url.data
        avatar_email = form.avatar_email.data
        create_readme = form.create_readme.data

        if namespace:
            namespace = namespace.strip()

        private = False
        if pagure.APP.config.get('PRIVATE_PROJECTS', False):
            private = form.private.data

        try:
            taskid = pagure.lib.new_project(
                SESSION,
                name=name,
                namespace=namespace,
                description=description,
                private=private,
                url=url,
                avatar_email=avatar_email,
                user=flask.g.fas_user.username,
                blacklist=APP.config['BLACKLISTED_PROJECTS'],
                allowed_prefix=APP.config['ALLOWED_PREFIX'],
                gitfolder=APP.config['GIT_FOLDER'],
                docfolder=APP.config['DOCS_FOLDER'],
                ticketfolder=APP.config['TICKETS_FOLDER'],
                requestfolder=APP.config['REQUESTS_FOLDER'],
                add_readme=create_readme,
                userobj=user,
                prevent_40_chars=APP.config.get(
                    'OLD_VIEW_COMMIT_ENABLED', False),
                user_ns=APP.config.get('USER_NAMESPACE', False),
            )
            SESSION.commit()
            output = {'message': 'Project creation queued',
                      'taskid': taskid}

            if flask.request.form.get('wait', True):
                result = pagure.lib.tasks.get_result(taskid).get()
                project = pagure.lib._get_project(
                    SESSION, name=result['repo'],
                    namespace=result['namespace'],
                    case=APP.config.get('CASE_SENSITIVE', False))
                output = {'message': 'Project "%s" created' % project.fullname}
        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, errors=form.errors)

    jsonout = flask.jsonify(output)
    return jsonout


@API.route('/<repo>', methods=['PATCH'])
@API.route('/<namespace>/<repo>', methods=['PATCH'])
@api_login_required(acls=['modify_project'])
@api_method
def api_modify_project(repo, namespace=None):
    """
    Modify a project
    ----------------
    Modify an existing project on this Pagure instance.

    ::

        PATCH /api/0/<repo>


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``main_admin``   | string  | Mandatory    | | The new main admin of   |
    |                  |         |              |   the project.            |
    +------------------+---------+--------------+---------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "access_groups": {
            "admin": [],
            "commit": [],
            "ticket": []
          },
          "access_users": {
            "admin": [],
            "commit": [],
            "owner": [
              "testuser1"
            ],
            "ticket": []
          },
          "close_status": [],
          "custom_keys": [],
          "date_created": "1496326387",
          "description": "Test",
          "fullname": "test-project2",
          "id": 2,
          "milestones": {},
          "name": "test-project2",
          "namespace": null,
          "parent": null,
          "priorities": {},
          "tags": [],
          "user": {
            "default_email": "testuser1@domain.local",
            "emails": [],
            "fullname": "Test User1",
            "name": "testuser1"
          }
        }

    """
    project = get_authorized_api_project(
        SESSION, repo, namespace=namespace)
    if not project:
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ENOPROJECT)

    admins = project.get_project_users('admin')
    if flask.g.fas_user not in admins and flask.g.fas_user != project.user:
        raise pagure.exceptions.APIError(
            401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED)

    valid_keys = ['main_admin']
    # Set force to True to ignore the mimetype. Set silent so that None is
    # returned if it's invalid JSON.
    json = flask.request.get_json(force=True, silent=True)
    if not json:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    # Check to make sure there aren't parameters we don't support
    for key in json.keys():
        if key not in valid_keys:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.EINVALIDREQ)

    if 'main_admin' in json:
        if flask.g.fas_user != project.user:
            raise pagure.exceptions.APIError(
                401, error_code=APIERROR.ENOTMAINADMIN)
        # If the main_admin is already set correctly, don't do anything
        if flask.g.fas_user.username == project.user:
            return flask.jsonify(project.to_json(public=False, api=True))

        try:
            new_main_admin = pagure.lib.get_user(SESSION, json['main_admin'])
        except pagure.exceptions.PagureException:
            raise pagure.exceptions.APIError(400, error_code=APIERROR.ENOUSER)

        pagure.lib.set_project_owner(SESSION, project, new_main_admin)

    try:
        SESSION.commit()
    except SQLAlchemyError:  # pragma: no cover
        SESSION.rollback()
        raise pagure.exceptions.APIError(
            400, error_code=APIERROR.EDBERROR)

    return flask.jsonify(project.to_json(public=False, api=True))


@API.route('/fork/', methods=['POST'])
@API.route('/fork', methods=['POST'])
@api_login_required(acls=['fork_project'])
@api_method
def api_fork_project():
    """
    Fork a project
    --------------------
    Fork a project on this pagure instance.

    This is an asynchronous call.

    ::

        POST /api/0/<repo>/fork


    Input
    ^^^^^

    +------------------+---------+--------------+---------------------------+
    | Key              | Type    | Optionality  | Description               |
    +==================+=========+==============+===========================+
    | ``repo``         | string  | Mandatory    | | The name of the project |
    |                  |         |              |   to fork.                |
    +------------------+---------+--------------+---------------------------+
    | ``namespace``    | string  | Optional     | | The namespace of the    |
    |                  |         |              |   project to fork.        |
    +------------------+---------+--------------+---------------------------+
    | ``username``     | string  | Optional     | | The username of the user|
    |                  |         |              |   of the fork.            |
    +------------------+---------+--------------+---------------------------+
    | ``wait``         | boolean | Optional     | | A boolean to specify if |
    |                  |         |              |   this API call should    |
    |                  |         |              |   return a taskid or if it|
    |                  |         |              |   should wait for the task|
    |                  |         |              |   to finish.              |
    +------------------+---------+--------------+---------------------------+


    Sample response
    ^^^^^^^^^^^^^^^

    ::

        wait=False:
        {
          "message": "Project forking queued",
          "taskid": "123-abcd"
        }

        wait=True:
        {
          "message": 'Repo "test" cloned to "pingou/test"
        }

    """
    output = {}

    form = pagure.forms.ForkRepoForm(csrf_enabled=False)
    if form.validate_on_submit():
        repo = form.repo.data
        username = form.username.data or None
        namespace = form.namespace.data.strip() or None

        repo = get_authorized_api_project(
            SESSION, repo, user=username, namespace=namespace)
        if repo is None:
            raise pagure.exceptions.APIError(
                404, error_code=APIERROR.ENOPROJECT)

        try:
            taskid = pagure.lib.fork_project(
                SESSION,
                user=flask.g.fas_user.username,
                repo=repo,
                gitfolder=APP.config['GIT_FOLDER'],
                docfolder=APP.config['DOCS_FOLDER'],
                ticketfolder=APP.config['TICKETS_FOLDER'],
                requestfolder=APP.config['REQUESTS_FOLDER'],
            )
            SESSION.commit()
            output = {'message': 'Project forking queued',
                      'taskid': taskid}

            if flask.request.form.get('wait', True):
                pagure.lib.tasks.get_result(taskid).get()
                output = {'message': 'Repo "%s" cloned to "%s/%s"'
                          % (repo.fullname, flask.g.fas_user.username,
                             repo.fullname)}
        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, errors=form.errors)

    jsonout = flask.jsonify(output)
    return jsonout