#! /usr/bin/env python

import errno
import os
import subprocess as sp
import sys
import time
from dateutil import tz
from datetime import datetime

try:
    from shlex import quote
except ImportError:
    from pipes import quote

__all__ = ['ghp_import']
__version__ = "2.1.0"


class GhpError(Exception):
    def __init__(self, message):
        self.message = message


if sys.version_info[0] == 3:
    def enc(text):
        if isinstance(text, bytes):
            return text
        return text.encode()

    def dec(text):
        if isinstance(text, bytes):
            return text.decode('utf-8')
        return text

    def write(pipe, data):
        try:
            pipe.stdin.write(data)
        except IOError as e:
            if e.errno != errno.EPIPE:
                raise
else:
    def enc(text):
        if isinstance(text, unicode):  # noqa F821
            return text.encode('utf-8')
        return text

    def dec(text):
        if isinstance(text, unicode):  # noqa F821
            return text
        return text.decode('utf-8')

    def write(pipe, data):
        pipe.stdin.write(data)


class Git(object):
    def __init__(self, use_shell=False):
        self.use_shell = use_shell

        self.cmd = None
        self.pipe = None
        self.stderr = None
        self.stdout = None

    def check_repo(self):
        if self.call('rev-parse') != 0:
            error = self.stderr
            if not error:
                error = "Unknown Git error"
            error = dec(error)
            if error.startswith("fatal: "):
                error = error[len("fatal: "):]
            raise GhpError(error)

    def try_rebase(self, remote, branch, no_history=False):
        rc = self.call('rev-list', '--max-count=1', '%s/%s' % (remote, branch))
        if rc != 0:
            return True
        rev = dec(self.stdout.strip())
        if no_history:
            rc = self.call('update-ref', '-d', 'refs/heads/%s' % branch)
        else:
            rc = self.call('update-ref', 'refs/heads/%s' % branch, rev)
        if rc != 0:
            return False
        return True

    def get_config(self, key):
        self.call('config', key)
        return self.stdout.strip()

    def get_prev_commit(self, branch):
        rc = self.call('rev-list', '--max-count=1', branch, '--')
        if rc != 0:
            return None
        return dec(self.stdout).strip()

    def open(self, *args, **kwargs):
        if self.use_shell:
            self.cmd = 'git ' + ' '.join(map(quote, args))
        else:
            self.cmd = ['git'] + list(args)
        if sys.version_info >= (3, 2, 0):
            kwargs['universal_newlines'] = False
        for k in 'stdin stdout stderr'.split():
            kwargs.setdefault(k, sp.PIPE)
        kwargs['shell'] = self.use_shell
        self.pipe = sp.Popen(self.cmd, **kwargs)
        return self.pipe

    def call(self, *args, **kwargs):
        self.open(*args, **kwargs)
        (self.stdout, self.stderr) = self.pipe.communicate()
        return self.pipe.wait()

    def check_call(self, *args, **kwargs):
        kwargs["shell"] = self.use_shell
        sp.check_call(['git'] + list(args), **kwargs)


def mk_when(timestamp=None):
    if timestamp is None:
        timestamp = int(time.time())
    currtz = datetime.now(tz.tzlocal()).strftime('%z')
    return "%s %s" % (timestamp, currtz)


def start_commit(pipe, git, branch, message, prefix=None):
    uname = os.getenv('GIT_COMMITTER_NAME', dec(git.get_config('user.name')))
    email = os.getenv('GIT_COMMITTER_EMAIL', dec(git.get_config('user.email')))
    when = os.getenv('GIT_COMMITTER_DATE', mk_when())
    write(pipe, enc('commit refs/heads/%s\n' % branch))
    write(pipe, enc('committer %s <%s> %s\n' % (uname, email, when)))
    write(pipe, enc('data %d\n%s\n' % (len(enc(message)), message)))
    head = git.get_prev_commit(branch)
    if head:
        write(pipe, enc('from %s\n' % head))
    if prefix:
        write(pipe, enc('D %s\n' % prefix))
    else:
        write(pipe, enc('deleteall\n'))


def add_file(pipe, srcpath, tgtpath):
    with open(srcpath, "rb") as handle:
        if os.access(srcpath, os.X_OK):
            write(pipe, enc('M 100755 inline %s\n' % tgtpath))
        else:
            write(pipe, enc('M 100644 inline %s\n' % tgtpath))
        data = handle.read()
        write(pipe, enc('data %d\n' % len(data)))
        write(pipe, enc(data))
        write(pipe, enc('\n'))


def add_nojekyll(pipe, prefix=None):
    if prefix:
        fpath = os.path.join(prefix, '.nojekyll')
    else:
        fpath = '.nojekyll'
    write(pipe, enc('M 100644 inline %s\n' % fpath))
    write(pipe, enc('data 0\n'))
    write(pipe, enc('\n'))


def add_cname(pipe, cname):
    write(pipe, enc('M 100644 inline CNAME\n'))
    write(pipe, enc('data %d\n%s\n' % (len(enc(cname)), cname)))


def gitpath(fname):
    norm = os.path.normpath(fname)
    return "/".join(norm.split(os.path.sep))


def run_import(git, srcdir, **opts):
    srcdir = dec(srcdir)
    pipe = git.open('fast-import', '--date-format=rfc2822', '--quiet',
                    stdin=sp.PIPE, stdout=None, stderr=None)
    start_commit(pipe, git, opts['branch'], opts['mesg'], opts['prefix'])
    for path, _, fnames in os.walk(srcdir, followlinks=opts['followlinks']):
        for fn in fnames:
            fpath = os.path.join(path, fn)
            gpath = gitpath(os.path.relpath(fpath, start=srcdir))
            if opts['prefix']:
                gpath = os.path.join(opts['prefix'], gpath)
            add_file(pipe, fpath, gpath)
    if opts['nojekyll']:
        add_nojekyll(pipe, opts['prefix'])
    if opts['cname'] is not None:
        add_cname(pipe, opts['cname'])
    write(pipe, enc('\n'))
    pipe.stdin.close()
    if pipe.wait() != 0:
        sys.stdout.write(enc("Failed to process commit.\n"))


def options():
    return [
        (('-n', '--no-jekyll'), dict(
            dest='nojekyll',
            default=False,
            action="store_true",
            help='Include a .nojekyll file in the branch.',
        )),
        (('-c', '--cname'), dict(
            dest='cname',
            default=None,
            help='Write a CNAME file with the given CNAME.',
        )),
        (('-m', '--message'), dict(
            dest='mesg',
            default='Update documentation',
            help='The commit message to use on the target branch.',
        )),
        (('-p', '--push'), dict(
            dest='push',
            default=False,
            action='store_true',
            help='Push the branch to origin/{branch} after committing.',
        )),
        (('-x', '--prefix'), dict(
            dest='prefix',
            default=None,
            help='The prefix to add to each file that gets pushed to the '
                 'remote. Only files below this prefix will be cleared '
                 'out. [%(default)s]',
        )),
        (('-f', '--force'), dict(
            dest='force',
            default=False, action='store_true',
            help='Force the push to the repository.',
        )),
        (('-o', '--no-history'), dict(
            dest='no_history',
            default=False,
            action='store_true',
            help='Force new commit without parent history.',
        )),
        (('-r', '--remote'), dict(
            dest='remote',
            default='origin',
            help='The name of the remote to push to. [%(default)s]',
        )),
        (('-b', '--branch'), dict(
            dest='branch',
            default='gh-pages',
            help='Name of the branch to write to. [%(default)s]',
        )),
        (('-s', '--shell'), dict(
            dest='use_shell',
            default=False,
            action='store_true',
            help='Use the shell when invoking Git. [%(default)s]',
        )),
        (('-l', '--follow-links'), dict(
            dest='followlinks',
            default=False,
            action='store_true',
            help='Follow symlinks when adding files. [%(default)s]',
        ))
    ]


def ghp_import(srcdir, **kwargs):
    if not os.path.isdir(srcdir):
        raise GhpError("Not a directory: %s" % srcdir)

    opts = {kwargs["dest"]: kwargs["default"] for _, kwargs in options()}
    opts.update(kwargs)

    git = Git(use_shell=opts['use_shell'])
    git.check_repo()

    if not git.try_rebase(opts['remote'], opts['branch'], opts['no_history']):
        raise GhpError("Failed to rebase %s branch." % opts['branch'])

    run_import(git, srcdir, **opts)

    if opts['push']:
        if opts['force'] or opts['no_history']:
            git.check_call('push', opts['remote'], opts['branch'], '--force')
        else:
            git.check_call('push', opts['remote'], opts['branch'])


def main():
    from argparse import ArgumentParser

    parser = ArgumentParser()
    parser.add_argument("--version", action="version", version=__version__)
    parser.add_argument("directory")
    for args, kwargs in options():
        parser.add_argument(*args, **kwargs)

    args = parser.parse_args().__dict__

    try:
        ghp_import(args.pop("directory"), **args)
    except GhpError as e:
        parser.error(e.message)


if __name__ == '__main__':
    main()
