#!/usr/bin/env python
#
# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.

"""Publish a local Git repository on a remote host.

This script wraps my usual workflow so I don't have to remember it ;).
"""

import logging as _logging
import os as _os
import os.path as _os_path
import shutil as _shutil
import subprocess as _subprocess
import tempfile as _tempfile
try:  # Python 3
    import urllib.parse as _urllib_parse
except ImportError:  # Python 2
    import urlparse as _urllib_parse


__version__ = '0.3'

_LOG = _logging.getLogger('git-publish')
_LOG.addHandler(_logging.StreamHandler())
_LOG.setLevel(_logging.WARNING)

PUBLIC_BRANCH_NAME = 'public'


def parse_remote(remote):
    """

    >>> parse_remote('ssh://example.com/~/')
    (None, 'example.com', 22, '~')
    >>> parse_remote('ssh://jdoe@example.com:2222/a/b/c')
    ('jdoe', 'example.com', 2222, '/a/b/c')
    """
    _LOG.debug("parse {} using Git's URL syntax".format(remote))
    p = _urllib_parse.urlparse(remote)
    assert p.scheme == 'ssh', p.scheme
    assert p.params == '', p.params
    assert p.query == '', p.query
    assert p.fragment == '', p.fragment

    _LOG.debug('extract username, host, and port from {}'.format(p.netloc))
    userhost_port = p.netloc.split(':', 1)
    if len(userhost_port) > 2:
        _LOG.error("more than one ':' in netloc: {}".format(p.netloc))
        raise ValueError(p.netloc)
    elif len(userhost_port) == 2:
        port = int(userhost_port[1])
    else:
        port = 22
    userhost = userhost_port[0]

    _LOG.debug('extract username and host from {}'.format(userhost))
    user_host = userhost.split('@', 1)
    if len(user_host) > 2:
        _LOG.error("more than one '@' in netloc: {}".format(p.netloc))
        raise ValueError(userhost)
    elif len(user_host) == 2:
        user,host = user_host
    else:
        user = None
        host = user_host[0]

    _LOG.debug('extract path from {}'.format(p.path))
    basedir = p.path.rstrip('/')
    if basedir.startswith('/~'):
        basedir = basedir[1:]  # allow user expansion on the remote host

    _LOG.debug('user: {}, host: {}, port: {}, basedir: {}'.format(
            user, host, port, basedir))
    return (user, host, port, basedir)

def touch(path):
    with open(path, 'a') as f:
        pass

def git(args, repo=None):
    """

    >>> print(git(['help']))  # doctest: +ELLIPSIS
    usage: git ...
    """
    if repo is None:
        repo='.'
    _LOG.debug('{}: git {}'.format(repo, args))
    output = _subprocess.check_output(['git'] + args, cwd=repo)
    _LOG.debug(output)
    return output

def has_remote(repo, remote):
    for line in git(repo=repo, args=['remote']).splitlines():
        if line == remote:
            return True
    return False

def make_bare_local_checkout(repo):
    bare = _tempfile.mkdtemp(prefix='git-publish-')
    _LOG.debug('make a bare local checkout of {} in {}'.format(repo, bare))
    git(args=['clone', '--bare', repo, bare])
    _LOG.debug('locally configure the bare checkout')
    _shutil.copy(_os_path.join(repo, '.git', 'description'), bare)
    touch(_os_path.join(bare, 'git-daemon-export-ok'))
    _shutil.move(_os_path.join(bare, 'hooks', 'post-update.sample'),
                 _os_path.join(bare, 'hooks', 'post-update'))
    git(repo=bare, args=['--bare', 'update-server-info'])
    _os.chmod(bare, 0o0755)
    return bare

def recursive_copy(source, user, host, port, path):
    source = source.rstrip('/') + '/'
    path = path.rstrip('/') + '/'
    target = '{}@{}:{}'.format(user, host, path)
    _LOG.debug('copy {} to {}'.format(source, target))
    _subprocess.check_call(
        ['rsync', '-az', '--delete', '--rsh', 'ssh -p{:d}'.format(port),
         source, target])

def add_remote(repo, remote, user, host, port=22, path=None):
    url = 'ssh://{}@{}:{:d}/{}'.format(user, host, port, path)
    _LOG.debug('add {} remote to {} pointing to {}'.format(remote, repo, url))
    git(repo=repo, args=['remote', 'add', remote, url])
    git(repo=repo, args=['fetch', remote])
    git(repo=repo, args=[
            'branch', '--set-upstream', 'master', '{}/master'.format(remote)])

def publish(repo, host, basedir='.', port=22, user=None, name=None):
    if user is None:
        user = _os.getlogin()
    repo = _os_path.abspath(_os_path.expanduser(repo))
    if name is None:
        name = _os_path.basename(repo)
    target_dir = _os_path.join(basedir, '{}.git'.format(name))
    _LOG.info('publishing {} at {}@{}:{:d}/{}'.format(
            repo, user, host, port, target_dir))
    if has_remote(repo=repo, remote=PUBLIC_BRANCH_NAME):
        _LOG.info('{} already published'.format(name))
        return
    bare = make_bare_local_checkout(repo)
    recursive_copy(
        source=bare, user=user, host=host, port=port, path=target_dir)
    _LOG.debug('cleanup {}'.format(bare))
    _shutil.rmtree(bare)
    add_remote(
        repo=repo, remote=PUBLIC_BRANCH_NAME, user=user, host=host, port=port,
        path=target_dir)


if __name__ == '__main__':
    from argparse import ArgumentParser
    import sys

    parser = ArgumentParser(description=__doc__, version=__version__)
    parser.add_argument(
        '-V', '--verbose', default=0, action='count',
        help='increment verbosity')
    parser.add_argument(
        '-r', '--remote',
        help=("the remote target.  Use Git's SSH URL, e.g. "
              'ssh://user@host:port/~/path/to/base'))
    parser.add_argument(
        '-n', '--name',
        help=('override the name of the new remote repository (defaults to '
              'the local dirname + .git)'))
    parser.add_argument(
        'repo', default='.', nargs='?',
        help='local Git repository to publish (default: %(default)s)')
    args = parser.parse_args()

    if args.verbose >= 2:
        _LOG.setLevel(_logging.DEBUG)
    elif args.verbose >= 1:
        _LOG.setLevel(_logging.INFO)

    if args.remote is None:
        _LOG.error('--remote argument is required.')
        sys.exit(1)

    user,host,port,basedir = parse_remote(remote=args.remote)
    publish(
        repo=args.repo, user=user, host=host, port=port, basedir=basedir,
        name=args.name)
