#!/usr/bin/env python
# Copyright (c) 2011 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# This is designed to be called by a gerrit hook.  It searched new
# patchsets for strings like "bug FOO" and updates corresponding Launchpad
# bugs status.

from launchpadlib.launchpad import Launchpad
from launchpadlib.uris import LPNET_SERVICE_ROOT
import os
import argparse
import re
import subprocess


BASE_DIR = '/home/gerrit2/review_site'
GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR',
                                '~/.launchpadlib/cache'))
GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS',
                                '~/.launchpadlib/creds'))


def add_change_proposed_message(bugtask, change_url, project, branch):
    subject = 'Fix proposed to %s (%s)' % (short_project(project), branch)
    body = 'Fix proposed to branch: %s\nReview: %s' % (branch, change_url)
    bugtask.bug.newMessage(subject=subject, content=body)


def add_change_merged_message(bugtask, change_url, project, commit,
                              submitter, branch, git_log):
    subject = 'Fix merged to %s (%s)' % (short_project(project), branch)
    git_url = 'http://github.com/%s/commit/%s' % (project, commit)
    body = '''Reviewed:  %s
Committed: %s
Submitter: %s
Branch:    %s\n''' % (change_url, git_url, submitter, branch)
    body = body + '\n' + git_log
    bugtask.bug.newMessage(subject=subject, content=body)


def set_in_progress(bugtask, launchpad, uploader, change_url):
    """Set bug In progress with assignee being the uploader"""

    # Retrieve uploader from Launchpad. Use email as search key if
    # provided, and only set if there is a clear match.
    try:
        searchkey = uploader[uploader.rindex("(") + 1:-1]
    except ValueError:
        searchkey = uploader
    persons = launchpad.people.findPerson(text=searchkey)
    if len(persons) == 1:
        bugtask.assignee = persons[0]

    bugtask.status = "In Progress"
    bugtask.lp_save()


def set_fix_committed(bugtask):
    """Set bug fix committed"""

    bugtask.status = "Fix Committed"
    bugtask.lp_save()


def set_fix_released(bugtask):
    """Set bug fix released"""

    bugtask.status = "Fix Released"
    bugtask.lp_save()


def release_fixcommitted(bugtask):
    """Set bug FixReleased if it was FixCommitted"""

    if bugtask.status == u'Fix Committed':
        set_fix_released(bugtask)


def tag_in_branchname(bugtask, branch):
    """Tag bug with in-branch-name tag (if name is appropriate)"""

    lp_bug = bugtask.bug
    branch_name = branch.replace('/', '-')
    if branch_name.replace('-', '').isalnum():
        lp_bug.tags = lp_bug.tags + ["in-%s" % branch_name]
        lp_bug.tags.append("in-%s" % branch_name)
        lp_bug.lp_save()


def short_project(full_project_name):
    """Return the project part of the git repository name"""
    return full_project_name.split('/')[-1]


def git2lp(full_project_name):
    """Convert Git repo name to Launchpad project"""
    project_map = {
        'openstack/python-cinderclient': 'cinder',
        'openstack/python-glanceclient': 'glance',
        'openstack/python-keystoneclient': 'keystone',
        'openstack/python-melangeclient': 'melange',
        'openstack/python-novaclient': 'nova',
        'openstack/python-quantumclient': 'quantum',
        'openstack/openstack-ci-puppet': 'openstack-ci',
        'openstack-ci/devstack-gate': 'openstack-ci',
        'openstack-ci/lodgeit': 'openstack-ci',
        'openstack-ci/meetbot': 'openstack-ci',
        }
    return project_map.get(full_project_name, short_project(full_project_name))


def is_direct_release(full_project_name):
    """Test against a list of projects who directly release changes."""
    return full_project_name in [
        'openstack-ci/devstack-gate',
        'openstack-ci/lodgeit',
        'openstack-ci/meetbot',
        'openstack-dev/devstack',
        'openstack/openstack-ci',
        'openstack/openstack-ci-puppet',
        'openstack/openstack-manuals',
        ]


def process_bugtask(launchpad, bugtask, git_log, args):
    """Apply changes to bugtask, based on hook / branch..."""

    if args.hook == "change-merged":
        if args.branch == 'master':
            if is_direct_release(args.project):
                set_fix_released(bugtask)
            else:
                set_fix_committed(bugtask)
        elif args.branch == 'milestone-proposed':
            release_fixcommitted(bugtask)
        elif args.branch.startswith('stable/'):
            series = args.branch[7:]
            # Look for a related task matching the series
            for reltask in bugtask.related_tasks:
                if reltask.bug_target_name.endswith("/" + series):
                    # Use fixcommitted if there is any
                    set_fix_committed(reltask)
                    break
            else:
                # Use tagging if there isn't any
                tag_in_branchname(bugtask, args.branch)

        add_change_merged_message(bugtask, args.change_url, args.project,
                                  args.commit, args.submitter, args.branch,
                                  git_log)

    if args.hook == "patchset-created":
        if args.branch == 'master':
            set_in_progress(bugtask, launchpad, args.uploader, args.change_url)
        elif args.branch.startswith('stable/'):
            series = args.branch[7:]
            for reltask in bugtask.related_tasks:
                if reltask.bug_target_name.endswith("/" + series):
                    set_in_progress(reltask, launchpad,
                                    args.uploader, args.change_url)
                    break

        if args.patchset == '1':
            add_change_proposed_message(bugtask, args.change_url,
                                        args.project, args.branch)


def find_bugs(launchpad, git_log, args):
    """Find bugs referenced in the git log and return related bugtasks"""

    bug_regexp = r'([Bb]ug|[Ll][Pp])[\s#:]*(\d+)'
    tokens = re.split(bug_regexp, git_log)

    # Extract unique bug tasks
    bugtasks = {}
    for token in tokens:
        if re.match('^\d+$', token) and (token not in bugtasks):
            try:
                lp_bug = launchpad.bugs[token]
                for lp_task in lp_bug.bug_tasks:
                    if lp_task.bug_target_name == git2lp(args.project):
                        bugtasks[token] = lp_task
                        break
            except KeyError:
                # Unknown bug
                pass

    return bugtasks.values()


def extract_git_log(args):
    """Extract git log of all merged commits"""
    cmd = ['git',
           '--git-dir=' + BASE_DIR + '/git/' + args.project + '.git',
           'log', '--no-merges', args.commit + '^1..' + args.commit]
    return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('hook')
    #common
    parser.add_argument('--change', default=None)
    parser.add_argument('--change-url', default=None)
    parser.add_argument('--project', default=None)
    parser.add_argument('--branch', default=None)
    parser.add_argument('--commit', default=None)
    #change-merged
    parser.add_argument('--submitter', default=None)
    #patchset-created
    parser.add_argument('--uploader', default=None)
    parser.add_argument('--patchset', default=None)

    args = parser.parse_args()

    # Connect to Launchpad
    launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT,
                                     GERRIT_CACHE_DIR,
                                     credentials_file=GERRIT_CREDENTIALS,
                                     version='devel')

    # Get git log
    git_log = extract_git_log(args)

    # Process bugtasks found in git log
    for bugtask in find_bugs(launchpad, git_log, args):
        process_bugtask(launchpad, bugtask, git_log, args)


if __name__ == '__main__':
    main()