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

"""
 (c) 2014-2018 - Copyright Red Hat Inc

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

"""

from __future__ import unicode_literals, absolute_import

import logging

import pygit2
import sqlalchemy as sa
import wtforms

try:
    from flask_wtf import FlaskForm
except ImportError:
    from flask_wtf import Form as FlaskForm
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import relation
from sqlalchemy.orm import backref

import pagure.config
import pagure.lib.query
import pagure.lib.git
from pagure.hooks import BaseHook, BaseRunner
from pagure.lib.model import BASE, Project


_log = logging.getLogger(__name__)
pagure_config = pagure.config.reload_config()


class PagureTable(BASE):
    """ Stores information about the pagure hook deployed on a project.

    Table -- hook_pagure
    """

    __tablename__ = "hook_pagure"

    id = sa.Column(sa.Integer, primary_key=True)
    project_id = sa.Column(
        sa.Integer,
        sa.ForeignKey("projects.id", onupdate="CASCADE", ondelete="CASCADE"),
        nullable=False,
        unique=True,
        index=True,
    )

    active = sa.Column(sa.Boolean, nullable=False, default=False)

    project = relation(
        "Project",
        remote_side=[Project.id],
        backref=backref(
            "pagure_hook",
            cascade="delete, delete-orphan",
            single_parent=True,
            uselist=False,
        ),
    )


def generate_revision_change_log(
    session, project, username, repodir, new_commits_list
):

    print("Detailed log of new commits:\n\n")
    commitid = None
    for line in pagure.lib.git.read_git_lines(
        ["log", "--no-walk"] + new_commits_list + ["--"], repodir
    ):
        if line.startswith("commit"):
            commitid = line.split("commit ")[-1]

        line = line.strip()
        print("*", line)
        for issue_or_pr in pagure.lib.link.get_relation(
            session,
            project.name,
            project.username if project.is_fork else None,
            project.namespace,
            line,
            "fixes",
            include_prs=True,
        ):
            if pagure_config.get("HOOK_DEBUG", False):
                print(commitid, relation)
            fixes_relation(
                session,
                username,
                commitid,
                issue_or_pr,
                pagure_config.get("APP_URL"),
            )

        for issue in pagure.lib.link.get_relation(
            session,
            project.name,
            project.username if project.is_fork else None,
            project.namespace,
            line,
            "relates",
        ):
            if pagure_config.get("HOOK_DEBUG", False):
                print(commitid, issue)
            relates_commit(
                session,
                username,
                commitid,
                issue,
                pagure_config.get("APP_URL"),
            )


def relates_commit(session, username, commitid, issue, app_url=None):
    """ Add a comment to an issue that this commit relates to it. """

    url = "../%s" % commitid[:8]
    if app_url:
        if app_url.endswith("/"):
            app_url = app_url[:-1]
        project = issue.project.fullname
        if issue.project.is_fork:
            project = "fork/%s" % project
        url = "%s/%s/c/%s" % (app_url, project, commitid[:8])

    comment = """ Commit [%s](%s) relates to this ticket""" % (
        commitid[:8],
        url,
    )

    try:
        pagure.lib.query.add_issue_comment(
            session, issue=issue, comment=comment, user=username
        )
        session.commit()
    except pagure.exceptions.PagureException as err:
        print(err)
    except SQLAlchemyError as err:  # pragma: no cover
        session.rollback()
        _log.exception(err)


def fixes_relation(session, username, commitid, relation, app_url=None):
    """ Add a comment to an issue or PR that this commit fixes it and update
    the status if the commit is in the master branch. """

    url = "../c/%s" % commitid[:8]
    if app_url:
        if app_url.endswith("/"):
            app_url = app_url[:-1]
        project = relation.project.fullname
        if relation.project.is_fork:
            project = "fork/%s" % project
        url = "%s/%s/c/%s" % (app_url, project, commitid[:8])

    comment = """ Commit [%s](%s) fixes this %s""" % (
        commitid[:8],
        url,
        relation.isa,
    )

    try:
        if relation.isa == "issue":
            pagure.lib.query.add_issue_comment(
                session, issue=relation, comment=comment, user=username
            )
        elif relation.isa == "pull-request":
            pagure.lib.query.add_pull_request_comment(
                session,
                request=relation,
                commit=None,
                tree_id=None,
                filename=None,
                row=None,
                comment=comment,
                user=username,
            )
        session.commit()
    except pagure.exceptions.PagureException as err:
        print(err)
    except SQLAlchemyError as err:  # pragma: no cover
        session.rollback()
        _log.exception(err)

    try:
        if relation.isa == "issue":
            pagure.lib.query.edit_issue(
                session,
                relation,
                user=username,
                status="Closed",
                close_status="Fixed",
            )
        elif relation.isa == "pull-request":
            pagure.lib.query.close_pull_request(
                session, relation, user=username, merged=True
            )
        session.commit()
    except pagure.exceptions.PagureException as err:
        print(err)
    except SQLAlchemyError as err:  # pragma: no cover
        session.rollback()
        print("ERROR", err)
        _log.exception(err)


class PagureRunner(BaseRunner):
    """ Runner for the pagure's specific git hook. """

    @staticmethod
    def post_receive(session, username, project, repotype, repodir, changes):
        """ Run the default post-receive hook.

        For args, see BaseRunner.runhook.
        """

        if repotype != "main":
            print("The pagure hook only runs on the main git repo.")
            return

        for refname in changes:
            (oldrev, newrev) = changes[refname]

            # Retrieve the default branch
            repo_obj = pygit2.Repository(repodir)
            default_branch = None
            if not repo_obj.is_empty and not repo_obj.head_is_unborn:
                default_branch = repo_obj.head.shorthand

            # Skip all branch but the default one
            refname = refname.replace("refs/heads/", "")
            if refname != default_branch:
                continue

            if set(newrev) == set(["0"]):
                print(
                    "Deleting a reference/branch, so we won't run the "
                    "pagure hook"
                )
                return

            generate_revision_change_log(
                session,
                project,
                username,
                repodir,
                pagure.lib.git.get_revs_between(
                    oldrev, newrev, repodir, refname
                ),
            )
            session.close()


class PagureForm(FlaskForm):
    """ Form to configure the pagure hook. """

    active = wtforms.BooleanField("Active", [wtforms.validators.Optional()])


DESCRIPTION = """
Pagure specific hook to add a comment to issues or pull requests if the pushed
commits fix them
or relate to them. This is determined based on the commit message.

To reference an issue/PR you need to use one of recognized keywords followed by
a reference to the issue or PR, separated by whitespace and and optional colon.
Such references can be either:

 * The issue/PR number preceded by the `#` symbol
 * The full URL of the issue or PR

If using the full URL, it is possible to reference issues in other projects.

The recognized keywords are:

 * fix/fixed/fixes
 * relate/related/relates
 * merge/merges/merged
 * close/closes/closed

Examples:

 * Fixes #21
 * related: https://pagure.io/myproject/issue/32
 * this commit merges #74
 * Merged: https://pagure.io/myproject/pull-request/74

Capitalization does not matter; neither does the colon between keyword and
number.


"""


class PagureHook(BaseHook):
    """ Pagure hook. """

    name = "Pagure"
    description = DESCRIPTION
    form = PagureForm
    db_object = PagureTable
    backref = "pagure_hook"
    form_fields = ["active"]
    runner = PagureRunner