# vim: set fileencoding=utf-8 :
#
# (C) 2010,2012 Guido Günther <agx@sigxcpu.org>
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Based on the aa-create-git-repo and dom-new-git-repo shell scripts
"""Create a remote repo based on the current one"""

import sys
import os, os.path
import urlparse
import subprocess
import tty, termios
import re
from gbp.deb.changelog import ChangeLog, NoChangeLogError
from gbp.command_wrappers import (CommandExecFailed, GitCommand)
from gbp.config import (GbpOptionParserDebian, GbpOptionGroup)
from gbp.errors import GbpError
from gbp.git import GitRepositoryError
from gbp.deb.git import DebianGitRepository
import gbp.log

def print_config(remote, branches):
    """
    Print out the git config to push to the newly created repo.

    >>> print_config({'name': 'name', 'url': 'url'}, ['foo', 'bar'])
    [remote "name"]
            url = url
            fetch = +refs/heads/*:refs/remotes/name/*
            push = foo
            push = bar
    [branch "foo"]
            remote = name
            merge = refs/heads/foo
    [branch "bar"]
            remote = name
            merge = refs/heads/bar
    """

    print """[remote "%(name)s"]
        url = %(url)s
        fetch = +refs/heads/*:refs/remotes/%(name)s/*""" % remote

    for branch in branches:
        print "        push = %s" % branch

    for branch in branches:
        print """[branch "%s"]
        remote = %s
        merge = refs/heads/%s""" % (branch, remote['name'], branch)

def sort_dict(d):
    """Return a sorted list of (key, value) tuples"""
    s = []
    for key in sorted(d.iterkeys()):
        s.append((key, d[key]))
    return s

def parse_url(remote_url, name, pkg, template_dir=None):
    """
    Sanity check our remote URL

    >>> sort_dict(parse_url("ssh://host/path/%(pkg)s", "origin", "package"))
    [('base', ''), ('dir', '/path/package'), ('host', 'host'), ('name', 'origin'), ('pkg', 'package'), ('port', None), ('scheme', 'ssh'), ('template-dir', None), ('url', 'ssh://host/path/package')]

    >>> sort_dict(parse_url("ssh://host:22/path/repo.git", "origin", "package"))
    [('base', ''), ('dir', '/path/repo.git'), ('host', 'host'), ('name', 'origin'), ('pkg', 'package'), ('port', '22'), ('scheme', 'ssh'), ('template-dir', None), ('url', 'ssh://host:22/path/repo.git')]

    >>> sort_dict(parse_url("ssh://host:22/~/path/%(pkg)s.git", "origin", "package"))
    [('base', '~/'), ('dir', 'path/package.git'), ('host', 'host'), ('name', 'origin'), ('pkg', 'package'), ('port', '22'), ('scheme', 'ssh'), ('template-dir', None), ('url', 'ssh://host:22/~/path/package.git')]

    >>> sort_dict(parse_url("ssh://host:22/~user/path/%(pkg)s.git", "origin", "package", "/doesnot/exist"))
    [('base', '~user/'), ('dir', 'path/package.git'), ('host', 'host'), ('name', 'origin'), ('pkg', 'package'), ('port', '22'), ('scheme', 'ssh'), ('template-dir', '/doesnot/exist'), ('url', 'ssh://host:22/~user/path/package.git')]

    >>> parse_url("git://host/repo.git", "origin", "package")
    Traceback (most recent call last):
        ...
    GbpError: URL must use ssh protocol.
    >>> parse_url("ssh://host/path/repo", "origin", "package")
    Traceback (most recent call last):
        ...
    GbpError: URL needs to contain either a repository name or '%(pkg)s'
    >>> parse_url("ssh://host:asdf/path/%(pkg)s.git", "origin", "package")
    Traceback (most recent call last):
        ...
    GbpError: URL contains invalid port.
    >>> parse_url("ssh://host/~us er/path/%(pkg)s.git", "origin", "package")
    Traceback (most recent call last):
        ...
    GbpError: URL contains invalid ~username expansion.
    """
    frags = urlparse.urlparse(remote_url)
    if frags.scheme in ['ssh', 'git+ssh', '']:
        scheme = frags.scheme
    else:
        raise GbpError("URL must use ssh protocol.")

    if not '%(pkg)s' in remote_url and not remote_url.endswith(".git"):
        raise GbpError("URL needs to contain either a repository name or '%(pkg)s'")

    if ":" in frags.netloc:
        (host, port) = frags.netloc.split(":", 1)
        if not re.match(r"^[0-9]+$", port):
            raise GbpError("URL contains invalid port.")
    else:
        host = frags.netloc
        port = None

    if frags.path.startswith("/~"):
        m = re.match(r"/(~[a-zA-Z0-9_-]*/)(.*)", frags.path)
        if not m:
            raise GbpError("URL contains invalid ~username expansion.")
        base = m.group(1)
        path = m.group(2)
    else:
        base = ""
        path = frags.path

    remote = { 'pkg' : pkg,
               'url' : remote_url % { 'pkg': pkg },
               'dir' : path % { 'pkg': pkg },
               'base': base,
               'host': host,
               'port': port,
               'name': name,
               'scheme': scheme,
               'template-dir': template_dir}
    return remote


def build_remote_script(remote, branch):
    """
    Create the script that will be run on the remote side
    >>> build_remote_script({'base': 'base', 'dir': 'dir', 'pkg': 'pkg', 'template-dir': None}, 'branch')
    '\\nset -e\\numask 002\\nif [ -d base"dir" ]; then\\n  echo "Repository at "basedir" already exists - giving up."\\n  exit 1\\nfi\\nmkdir -p base"dir"\\ncd base"dir"\\ngit init --bare --shared\\necho "pkg packaging" > description\\necho "ref: refs/heads/branch" > HEAD\\n'
    >>> build_remote_script({'base': 'base', 'dir': 'dir', 'pkg': 'pkg', 'template-dir': '/doesnot/exist'}, 'branch')
    '\\nset -e\\numask 002\\nif [ -d base"dir" ]; then\\n  echo "Repository at "basedir" already exists - giving up."\\n  exit 1\\nfi\\nmkdir -p base"dir"\\ncd base"dir"\\ngit init --bare --shared --template=/doesnot/exist\\necho "pkg packaging" > description\\necho "ref: refs/heads/branch" > HEAD\\n'
    """
    args = remote
    args['branch'] = branch
    args['git-init-args'] = '--bare --shared'
    if args['template-dir']:
        args['git-init-args'] += (' --template=%s'
                                    % args['template-dir'])
    remote_script_pattern = ['',
      'set -e',
      'umask 002',
      'if [ -d %(base)s"%(dir)s" ]; then',
      '  echo "Repository at \"%(base)s%(dir)s\" already exists - giving up."',
      '  exit 1',
      'fi',
      'mkdir -p %(base)s"%(dir)s"',
      'cd %(base)s"%(dir)s"',
      'git init %(git-init-args)s',
      'echo "%(pkg)s packaging" > description',
      'echo "ref: refs/heads/%(branch)s" > HEAD',
      '' ]
    remote_script = '\n'.join(remote_script_pattern) % args
    return remote_script


def build_cmd(remote):
    """
    Build the command we pass the script to

    >>> build_cmd({'scheme': ''})
    ['sh']
    >>> build_cmd({'scheme': 'ssh', 'host': 'host', 'port': 80})
    ['ssh', '-p', 80, 'host', 'sh']
    """
    cmd = []
    if remote["scheme"]:
        cmd.append('ssh')
        if remote["port"]:
            cmd.extend(['-p', remote['port']])
        cmd.append(remote["host"])
    cmd.append('sh')
    return cmd


def read_yn():
    fd = sys.stdin.fileno()
    try:
        old_settings = termios.tcgetattr(fd)
    except termios.error:
        old_settings = None

    try:
        if old_settings:
            tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
    finally:
        if old_settings:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

    if ch in ( 'y', 'Y' ):
        return True
    else:
        return False


def setup_branch_tracking(repo, remote, branches):
    repo.add_remote_repo(name=remote['name'], url=remote['url'], fetch=True)
    gitTrackRemote = GitCommand('branch', ['--set-upstream'])
    for branch in branches:
        gitTrackRemote(['%s' % branch, '%s/%s' % (remote['name'], branch)])


def push_branches(remote, branches):
    gitPush = GitCommand('push')
    gitPush([remote['url']] + branches)
    gitPush([remote['url'], '--tags'])


def parse_args(argv, sections=[]):
    """
    Parse the command line arguments and config files.

    @param argv: the command line arguments
    @type argv: C{list} of C{str}
    @param sections: additional sections to add to the config file parser
        besides the command name
    @type sections: C{list} of C{str}
    """

    # We simpley handle the template section as an additional config file
    # section to parse, this makes e.g. --help work as expected:
    for arg in argv:
        if arg.startswith('--remote-config='):
            sections = ['remote-config %s' % arg.split('=',1)[1]]
            break
    else:
        sections = []

    parser = GbpOptionParserDebian(command=os.path.basename(argv[0]), prefix='',
                                   usage='%prog [options] - '
                                   'create a remote repository',
                                   sections=sections)
    branch_group = GbpOptionGroup(parser,
                                  "branch options",
                                  "branch layout and tracking options")
    branch_group.add_config_file_option(option_name="remote-url-pattern",
                                        dest="remote_url")
    parser.add_option_group(branch_group)
    branch_group.add_config_file_option(option_name="upstream-branch",
                                        dest="upstream_branch")
    branch_group.add_config_file_option(option_name="debian-branch",
                                        dest="packaging_branch")
    branch_group.add_boolean_config_file_option(option_name="pristine-tar",
                                                dest="pristine_tar")
    branch_group.add_boolean_config_file_option(option_name="track",
                                                dest='track')
    parser.add_option("-v", "--verbose",
                      action="store_true",
                      dest="verbose",
                      default=False,
                      help="verbose command execution")
    parser.add_config_file_option(option_name="color",
                                  dest="color",
                                  type='tristate')
    parser.add_config_file_option(option_name="color-scheme",
                                  dest="color_scheme")
    parser.add_option("--remote-name",
                      dest="name",
                      default="origin",
                      help="The name of the remote, default is 'origin'")
    parser.add_config_file_option(option_name="template-dir",
                                  dest="template_dir")
    parser.add_config_file_option(option_name="remote-config",
                                  dest="remote_config")

    (options, args) = parser.parse_args(argv)

    return options, args


def main(argv):
    retval = 0
    changelog = 'debian/changelog'
    cmd = []

    try:
        options, args = parse_args(argv)
    except Exception as e:
        print >>sys.stderr, "%s" % e
        return 1

    gbp.log.setup(options.color, options.verbose, options.color_scheme)
    try:
        repo = DebianGitRepository(os.path.curdir)
    except GitRepositoryError:
        gbp.log.err("%s is not a git repository" % (os.path.abspath('.')))
        return 1

    try:
        branches = []

        for branch in [ options.packaging_branch, options.upstream_branch ]:
            if repo.has_branch(branch):
                branches += [ branch ]

        if repo.has_pristine_tar_branch() and options.pristine_tar:
            branches += [ repo.pristine_tar_branch ]

        try:
            cp = ChangeLog(filename=changelog)
            pkg = cp['Source']
        except NoChangeLogError:
            pkg = None

        if not pkg:
            gbp.log.warn("Couldn't parse changelog, will use directory name.")
            pkg = os.path.basename(os.path.abspath(os.path.curdir))
            pkg = os.path.splitext(pkg)[0]

        remote = parse_url(options.remote_url,
                           options.name,
                           pkg,
                           options.template_dir)
        if repo.has_remote_repo(options.name):
            raise GbpError("You already have a remote name '%s' defined for this repository." % options.name)

        gbp.log.info("Shall I create a repository for '%(pkg)s' at '%(url)s' now? (y/n)?" % remote)
        if not read_yn():
            raise GbpError("Aborted.")

        remote_script = build_remote_script(remote, branches[0])
        if options.verbose:
            print remote_script

        cmd = build_cmd(remote)
        if options.verbose:
            print cmd

        proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
        proc.communicate(remote_script)
        if proc.returncode:
            raise GbpError("Error creating remote repository")

        push_branches(remote, branches)
        if options.track:
            setup_branch_tracking(repo, remote, branches)
        else:
            gbp.log.info("You can now add:")
            print_config(remote, branches)
            gbp.log.info("to your .git/config to 'gbp-pull' and 'git push' in the future.")

    except CommandExecFailed:
        retval = 1
    except (GbpError, GitRepositoryError) as err:
        if len(err.__str__()):
            gbp.log.err(err)
        retval = 1

    return retval

if __name__ == '__main__':
    sys.exit(main(sys.argv))

# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
