Blob Blame Raw
#! /usr/bin/env python2


"""Progit specific hook to add comment on issues if the commits fixes or
relates to an issue.
"""

import os
import re
import sys
import subprocess


if 'PROGIT_CONFIG' not in os.environ \
        and os.path.exists('/etc/progit/progit.cfg'):
    print 'Using configuration file `/etc/progit/progit.cfg`'
    os.environ['PROGIT_CONFIG'] = '/etc/progit/progit.cfg'


import progit
import progit.exceptions


FIXES = [
    re.compile('fixe?[sd]?:?\s?#(\d+)', re.I),
    re.compile('.*\s+fixe?[sd]?:?\s?#(\d+)', re.I),
    re.compile('fixe?[sd]?:?\s?https?://.*/(\w+)/issue/(\d+)', re.I),
    re.compile('.*\s+fixe?[sd]?:?\s?https?://.*/(\w+)/issue/(\d+)', re.I),
]

RELATES = [
    re.compile('relate[sd]?:?\s?(?:to)?\s?#(\d+)', re.I),
    re.compile('.*\s+relate[sd]?:?\s?#(\d+)', re.I),
    re.compile(
        'relate[sd]?:?\s?(?:to)?\s?https?://.*/(\w+)/issue/(\d+)', re.I),
    re.compile('.*\s+relate[sd]?:?\s?https?://.*/(\w+)/issue/(\d+)', re.I),
]


def read_git_output(args, input=None, keepends=False, **kw):
    """Read the output of a Git command."""

    return read_output(['git'] + args, input=input, keepends=keepends, **kw)


def read_git_lines(args, keepends=False, **kw):
    """Return the lines output by Git command.

    Return as single lines, with newlines stripped off."""

    return read_git_output(args, keepends=True, **kw).splitlines(keepends)


def read_output(cmd, input=None, keepends=False, **kw):
    if input:
        stdin = subprocess.PIPE
    else:
        stdin = None
    p = subprocess.Popen(
        cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
        )
    (out, err) = p.communicate(input)
    retcode = p.wait()
    if retcode:
        print 'ERROR: %s =-- %s' % (cmd, retcode)
    if not keepends:
        out = out.rstrip('\n\r')
    return out


def generate_revision_change_log(new_commits_list):

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

        line = line.strip()

        print '*', line
        for motif in FIXES:
            if motif.match(line):
                print 'fixes', motif.match(line).groups()
                project = None
                if len(motif.match(line).groups()) >= 2:
                    issueid = motif.match(line).group(2)
                    project = motif.match(line).group(1)
                else:
                    issueid = motif.match(line).group(1)
                fixes_commit(commitid, issueid, project)
        for motif in RELATES:
            if motif.match(line):
                print 'relates to', motif.match(line).groups()
                project = None
                if len(motif.match(line).groups()) >= 2:
                    issueid = motif.match(line).group(2)
                    project = motif.match(line).group(1)
                else:
                    issueid = motif.match(line).group(1)
                relates_commit(commitid, issueid, project)


def relates_commit(commitid, issueid, project=None):
    ''' Add a comment to an issue that this commit relates to it. '''
    repo = project or get_repo_name()
    username = get_username()

    repo = progit.lib.get_project(progit.SESSION, repo, user=username)
    if not repo:
        repo = progit.lib.get_project(
            progit.SESSION, get_repo_name(), user=username)
    issue = progit.lib.search_issues(
        progit.SESSION, repo=repo, issueid=issueid)

    if issue is None or issue.project != repo:
        return

    comment = ''' Commit [%s](../%s) relates to this ticket''' % (
        commitid[:8], commitid[:8])

    try:
        message = progit.lib.add_issue_comment(
            progit.SESSION,
            issue=issue,
            comment=comment,
            user=get_pusher(commitid),
            ticketfolder=progit.APP.config['TICKETS_FOLDER'],
        )
        progit.SESSION.commit()
    except progit.exceptions.ProgitException as err:
        print err
    except SQLAlchemyError, err:  # pragma: no cover
        progit.SESSION.rollback()
        progit.APP.logger.exception(err)


def fixes_commit(commitid, issueid, project=None):
    ''' Add a comment to an issue that this commit fixes it and update
    the status if the commit is in the master branch. '''
    repo = project or get_repo_name()
    username = get_username()

    repo = progit.lib.get_project(progit.SESSION, repo, user=username)
    if not repo:
        repo = progit.lib.get_project(
            progit.SESSION, get_repo_name(), user=username)
    issue = progit.lib.search_issues(
        progit.SESSION, repo, issueid=issueid)

    if issue is None or issue.project != repo:
        return

    comment = ''' Commit [%s](../%s) fixes this ticket''' % (
        commitid[:8], commitid[:8])

    try:
        message = progit.lib.add_issue_comment(
            progit.SESSION,
            issue=issue,
            comment=comment,
            user=get_pusher(commitid),
            ticketfolder=progit.APP.config['TICKETS_FOLDER'],
        )
        progit.SESSION.commit()
    except progit.exceptions.ProgitException as err:
        print err
    except SQLAlchemyError, err:  # pragma: no cover
        progit.SESSION.rollback()
        progit.APP.logger.exception(err)

    branches = [
        item.replace('* ', '') for item in read_git_lines(
            ['branch', '--contains', commitid],
            keepends=False)
    ]

    if 'master' in branches:
        try:
            progit.lib.edit_issue(
                progit.SESSION,
                issue,
                ticketfolder=progit.APP.config['TICKETS_FOLDER'],
                status='Fixed')
            progit.SESSION.commit()
        except progit.exceptions.ProgitException as err:
            print err
        except SQLAlchemyError, err:  # pragma: no cover
            progit.SESSION.rollback()
            progit.APP.logger.exception(err)


def get_commits_id(fromrev, torev):
    ''' Retrieve the list commit between two revisions and return the list
    of their identifier.
    '''
    cmd = ['rev-list', '%s...%s' % (torev, fromrev)]
    return read_git_lines(cmd)


def get_repo_name():
    ''' Return the name of the git repo based on its path.
    '''
    repo = os.path.basename(os.getcwd()).split('.git')[0]
    return repo


def get_username():
    ''' Return the username of the git repo based on its path.
    '''
    username = None
    repo = os.path.abspath(os.path.join(os.getcwd(), '..'))
    if progit.APP.config['FORK_FOLDER'] in repo:
        username = repo.split(progit.APP.config['FORK_FOLDER'])[1]
    return username


def get_pusher(commit):
    ''' Return the name of the person that pushed the commit. '''
    user = None
    output = read_git_lines(
        ['show', '--pretty=format:"%ae"', commit], keepends=False)
    if output:
        user = output[0].replace('"', '')
    if not user:
        user = os.environ.get('GL_USER', os.environ.get('USER', None))
    return user


def run_as_post_receive_hook():

    changes = []
    for line in sys.stdin:
        print line
        (oldrev, newrev, refname) = line.strip().split(' ', 2)

        print '  -- Old rev'
        print oldrev
        print '  -- New rev'
        print newrev
        print '  -- Ref name'
        print refname

        generate_revision_change_log(get_commits_id(oldrev, newrev))

    print 'repo:', get_repo_name()


def main(args):
        run_as_post_receive_hook()


if __name__ == '__main__':
    main(sys.argv[1:])