#!/usr/bin/env python
# Copyright 2011 OpenStack, LLC.
# All Rights Reserved.
#
# 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.

import commands
import optparse
import urllib
import json

from distutils.version import StrictVersion
from urlparse import urlparse


import os
import sys
import time
import re


version = "1.1"

VERBOSE = False
UPDATE = False
CONFIGDIR = os.path.expanduser("~/.config/git-review")
PYPI_URL = "http://pypi.python.org/pypi/git-review/json"
PYPI_CACHE_TIME = 60 * 60 * 24  # 24 hours


def run_command(cmd, status=False):
    if VERBOSE:
        print "Running:", cmd
    stat, out = commands.getstatusoutput(cmd)
    if status:
        return (stat, out)
    return out


def run_command_status(cmd):
    return run_command(cmd, True)


def update_latest_version(version_file_path):
    """ Cache the latest version of git-review for the upgrade check. """

    if not os.path.exists(CONFIGDIR):
        os.makedirs(CONFIGDIR)

    if os.path.exists(version_file_path) and not UPDATE:
        if (time.time() - os.path.getmtime(version_file_path)) < 28800:
            return

    latest_version = version
    try:
        latest_version = json.load(urllib.urlopen(PYPI_URL))['info']['version']
    except:
        pass

    with open(version_file_path, "w") as version_file:
        version_file.write(latest_version)


def latest_is_newer():
    """ Check if there is a new version of git-review. """

    version_file_path = os.path.join(CONFIGDIR, "latest-version")
    update_latest_version(version_file_path)

    latest_version = None
    with open(version_file_path, "r") as version_file:
        latest_version = StrictVersion(version_file.read())
    if latest_version > StrictVersion(version):
        return True
    return False


def set_hooks_commit_msg():
    """ Install the commit message hook if needed. """

    top_dir = run_command('git rev-parse --show-toplevel')
    target_file = os.path.join(top_dir, ".git/hooks/commit-msg")

    if os.path.exists(target_file) and os.access(target_file, os.X_OK):
        return

    (hostname, team, username, port, project_name) = \
        parse_git_show("gerrit", "Push")
    source_location = "https://%s/tools/hooks/commit-msg" % hostname

    if not os.path.exists(target_file) or UPDATE:
        if VERBOSE:
            print "Fetching source_location: ", source_location
        commit_msg = urllib.urlretrieve(source_location, target_file)

    if not os.access(target_file, os.X_OK):
        os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC)

    run_command("GIT_EDITOR=true git commit --amend")


def add_remote(username, hostname, port, project):
    """ Adds a gerrit remote. """

    if username is None:
        username = os.getenv("USERNAME")
    if port is None:
        port = 29418

    remote_url = "ssh://%s@%s:%s/%s.git" % (username, hostname, port, project)
    if VERBOSE:
        print "No remote set, testing %s" % remote_url

    ssh_cmd = "ssh -p%s -o StrictHostKeyChecking=no %s@%s gerrit ls-projects"
    cmd = ssh_cmd % (port, username, hostname)
    (status, ssh_outout) = run_command_status(cmd)
    if status == 0:
        if VERBOSE:
            print "%s@%s:%s worked." % (username, hostname, port)
        print "Creating a git remote called gerrit that maps to:"
        print "\t%s" % remote_url
        cmd = "git remote add -f gerrit %s" % remote_url
        (status, remote_output) = run_command_status(cmd)

    if status != 0:
        raise Exception("Error running %s" % cmd)


def split_hostname(fetch_url):

    parsed_url = urlparse(fetch_url)
    username = None
    hostname = parsed_url.netloc
    port = 22

    if "@" in hostname:
        (username, hostname) = hostname.split("@")
    if ":" in hostname:
        (hostname, port) = hostname.split(":")

    # Is origin an ssh location? Let's pull more info
    if parsed_url.scheme == "ssh":
        return (username, hostname, port)
    else:
        return (None, hostname, None)


def map_known_locations(hostname, team, project):
    # Assume that if we don't know about it, it's a proper gerrit location
    if VERBOSE:
        print "Mapping %s, %s, %s to a gerrit" % (hostname, team, project)

    if hostname == "github.com":
        # Welp, OBVIOUSLY _this_ isn't a gerrit
        if team is not None and team in ("openstack", "openstack-ci"):
            return ("review.openstack.org", "%s/%s" % (team, project))

        os_github_url = "http://github.com/api/v2/json/repos/show/openstack"
        os_projects_file = os.path.join(CONFIGDIR, "openstack.json")
        os_json = json.load(urllib.urlopen(os_github_url))
        os_projects = []
        if os_json.get('repositories', None) is not None:
            os_projects = [repo['name'] for repo in os_json['repositories']]

        if project in os_projects:
            return ("review.openstack.org", "openstack/%s" % project)
        else:
            raise Exception("No possible way to guess given the input")
    return hostname


def parse_git_show(remote, verb):
    fetch_url = ""
    for line in run_command("git remote show -n %s" % remote).split("\n"):
        if line.strip().startswith("%s" % verb):
            fetch_url = ":".join(line.split(":")[1:]).strip()

    project_name = fetch_url.split("/")[-1]
    if project_name.endswith(".git"):
        project_name = project_name[:-4]

    hostname = None
    team = None
    username = None
    port = None

    if VERBOSE:
        print "Found origin %s URL:" % verb, fetch_url

    # Special-case git@github urls - the rest can be parsed with urlparse
    if fetch_url.startswith("git@github.com"):
        hostname = "github.com"
    else:
        (username, hostname, port) = split_hostname(fetch_url)

    if hostname == "github.com":
        team = fetch_url.split("/")[-2]
        if team.startswith("git@github.com"):
            team = team.split(':')[1]

    return (hostname, team, username, port, project_name)


def check_remote(remote):
    """Check that a Gerrit Git remote repo exists, if not, set one."""

    if remote in run_command("git remote").split("\n"):

        for current_remote in run_command("git branch -a").split("\n"):
            if current_remote.strip() == "remotes/%s/master" % (remote) \
                and not UPDATE:
                return
        # We have the remote, but aren't set up to fetch. Fix it
        if VERBOSE:
            print "Setting up gerrit branch tracking for better rebasing"
        output = run_command("git remote update %s" % remote)
        if VERBOSE:
            print output
        return

    (hostname, team, username, port, project_name) = \
        parse_git_show("origin", "Fetch")

    # Gerrit remote not present, try to add it
    try:
        (hostname, project) = map_known_locations(hostname, team, project_name)
        add_remote(username, hostname, port, project)
    except:
        print sys.exc_info()[2]
        print "We don't know where your gerrit is. Please manually create "
        print "a remote named gerrit and try again."
        raise


def rebase_changes(branch, remote):

    remote_branch = "remotes/%s/%s" % (remote, branch)
    needs_remote = True
    for current_remote in run_command("git branch -a").split("\n"):
        if current_remote.strip() == remote_branch:
            needs_remote = False
    if needs_remote:
        cmd = "git remote update %s" % remote
        (status, output) = run_command_status(cmd)
        if VERBOSE:
            print output
        if status != 0:
            print "Problem running '%s'" % cmd
            if not VERBOSE:
                print output
            return False

    cmd = "GIT_EDITOR=true git rebase -i %s" % remote_branch
    (status, output) = run_command_status(cmd)
    if status != 0:
        print "Errors running %s" % cmd
        print output
        return False
    return True


def assert_diverge(branch, remote):

    cmd = "git diff %s/%s..HEAD" % (remote, branch)
    (status, output) = run_command_status(cmd)
    if len(output) == 0:
        print "No changes between HEAD and %s/%s." % (remote, branch)
        print "Submitting for review would be pointless."
        sys.exit(1)
    if status != 0:
        print "Had trouble running %s" % cmd
        sys.exit(1)


def get_topic():

    branch_name = None
    for branch in run_command("git branch").split("\n"):
        if branch.startswith('*'):
            branch_name = branch.split()[1].strip()

    branch_parts = branch_name.split("/")
    if len(branch_parts) >= 3 and branch_parts[0] == "review":
        return "/".join(branch_parts[2:])

    log_output = run_command("git show --format='%s %b'")
    bug_re = r'\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)'

    match = re.search(bug_re, log_output)
    if match is not None:
        return "bug/%s" % match.group(2)

    bp_re = r'\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)'
    match = re.search(bp_re, log_output)
    if match is not None:
        return "bp/%s" % match.group(2)

    return branch_name


def download_review(review):

    (hostname, team, username, port, project_name) = \
        parse_git_show("gerrit", "Push")

    ssh_cmds = ["ssh"]
    if port is not None:
        ssh_cmds.extend(["-p", port])
    if username is not None:
        ssh_cmds.append(["-l", username])
    ssh_cmd = " ".join(ssh_cmds)

    query_string = "--format=JSON --current-patch-set change:%s" % review
    review_info = None
    (status, output) = run_command_status("%s gerrit query %s"
                                           % (ssh_cmd, query_string))

    if status != 0:
        print "Could not fetch review information from gerrit"
        print output
        return status
    try:
        review_info = json.loads(output.split("\n")[0])
    except:
        if VERBOSE:
            print output
        print "Could not find a gerrit review with id: %s" % review
        return 1

    topic = review_info['topic']
    if topic == "master":
        topic = review
    author = re.sub('\W+', '_', review_info['owner']['name']).lower()
    branch_name = "review/%s/%s" % (author, topic)
    revision = review_info['currentPatchSet']['revision']
    refspec = review_info['currentPatchSet']['ref']

    print "Downloading %s from gerrit into %s" % (refspec, branch_name)
    checkout_cmd = "git checkout -b %s remotes/gerrit/master" % branch_name
    (status, output) = run_command_status(checkout_cmd)
    if status != 0:
        if output.endswith("already exists"):
            print "Branch already exists - reusing"
            checkout_cmd = "git checkout %s" % branch_name
            (status, output) = run_command_status(checkout_cmd)
            if status != 0:
                print output
                return status
        else:
            print output
            return status

    (status, output) = run_command_status("git pull gerrit %s" % refspec)
    if status != 0:
        print output
        return status

    (status, output) = run_command_status("git reset --hard %s" % revision)
    if status != 0:
        print output
        return status
    return 0


def print_exit_message(status, needs_update):

    if needs_update:
        print """
***********************************************************
A new version of git-review is availble on PyPI. Please
update your copy with:

  pip install -U git-review

to ensure proper behavior with gerrit. Thanks!
***********************************************************
"""
    sys.exit(status)


def main():

    usage = "git review [OPTIONS] ... [BRANCH]"
    parser = optparse.OptionParser(usage=usage)
    parser.add_option("-t", "--topic", dest="topic",
                      help="Topic to submit branch to")
    parser.add_option("-n", "--dry-run", dest="dry", action="store_true",
                      help="Don't actually submit the branch for review")
    parser.add_option("-r", "--remote", dest="remote",
                      help="git remote to use for gerrit")
    parser.add_option("-R", "--no-rebase", dest="rebase",
                      action="store_false",
                      help="Don't rebase changes before submitting.")
    parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
                      help="Output more information about what's going on")
    parser.add_option("-d", "--download", dest="download",
                      help="Download the contents of an existing gerrit "
                           "review into a branch")
    parser.add_option("-u", "--update", dest="update", action="store_true",
                      help="Force updates from remote locations")
    parser.set_defaults(dry=False, rebase=True, verbose=False, update=False,
                        remote="gerrit")

    branch = "master"
    (options, args) = parser.parse_args()
    if len(args) > 0:
        branch = args[0]
    global VERBOSE
    global UPDATE
    VERBOSE = options.verbose
    UPDATE = options.update
    remote = options.remote

    needs_update = latest_is_newer()
    check_remote(remote)

    if options.download is not None:
        print_exit_message(download_review(options.download), needs_update)
    else:
        topic = options.topic
        if topic is None:
            topic = get_topic()
        if VERBOSE:
            print "Found topic '%s' from parsing changes." % topic

        drier = ""
        if options.dry:
            drier = "echo -e Please use the following command " \
                    "to send your commits to review:\n\n"

        set_hooks_commit_msg()

        cmd = "git fetch %s %s" % (remote, branch)
        (status, output) = run_command_status(cmd)

        if options.rebase:
            if not rebase_changes(branch, remote):
                print_exit_message(1, needs_update)
        assert_diverge(branch, remote)

        cmd = "%s git push %s HEAD:refs/for/%s/%s" % (drier, remote, branch,
                                                      topic)
        (status, output) = run_command_status(cmd)
        print output
    print_exit_message(status, needs_update)


if __name__ == "__main__":
    main()
