#!/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

import os
import sys
import time

version = "1.0"

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 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(hostname="review.openstack.org"):
    """ Install the commit message hook if needed. """

    top_dir = commands.getoutput('git rev-parse --show-toplevel')
    target_file = os.path.join(top_dir, ".git/hooks/commit-msg")
    source_location = "https://%s/tools/hooks/commit-msg" % hostname

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

    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)

    commands.getoutput("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) = commands.getstatusoutput(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) = commands.getstatusoutput(cmd)

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


def split_hostname(fetch_url):

    from urlparse import urlparse

    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 check_remote(remote):
    """Check that a Gerrit Git remote repo exists, if not, set one."""

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

        for current_remote in commands.getoutput("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"
        commands.getoutput("git remote update %s")
        return

    fetch_url = ""
    for line in commands.getoutput("git remote show -n origin").split("\n"):
        if line.strip().startswith("Fetch URL"):
            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

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

    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

    return hostname


def rebase_changes(branch, remote):

    cmd = "GIT_EDITOR=true git rebase -i %s/%s" % (remote, branch)
    (status, output) = commands.getstatusoutput(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) = commands.getstatusoutput(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():

    import re

    log_output = commands.getoutput("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 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 match.group(2)

    for branch in commands.getoutput("git branch").split("\n"):
        if branch.startswith('*'):
            return branch.split()[1].strip()


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("-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

    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"

    needs_update = latest_is_newer()

    hostname = check_remote(remote)

    set_hooks_commit_msg(hostname)

    if UPDATE:
        cmd = "git fetch %s %s" % (remote, branch)
        (status, output) = commands.getstatusoutput(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" % (remote, drier, branch, topic)
    (status, output) = commands.getstatusoutput(cmd)
    print output
    print_exit_message(status, needs_update)


if __name__ == "__main__":
    main()
