#!/usr/bin/env python2

import errno
import os.path
import posixpath
import re
import subprocess
import urlparse

git_cache_dir = os.path.expanduser('~/.cache/git')

def git(*args, **kwargs):
    argv = ('git',) + args
    print '>> ' + ' '.join(argv)
    subprocess.check_call(argv, **kwargs)

def git_output(git_dir, *args):
    argv = ('git', '--git-dir', git_dir) + args
    print '>> ' + ' '.join(argv)
    return subprocess.check_output(argv, stderr=open(os.devnull, 'w'))

def iter_nul(expr, data):
    start = 0
    while start < len(data):
        end = data.index('\0', start)
        line = data[start:end]
        match = re.match(expr, line)
        if match:
            yield match.groups()
        start = end + 1

def git_output_list(expr, git_dir, *args):
    return iter_nul(expr, git_output(git_dir, *args))

def git_output_line(git_dir, *args):
    return git_output(git_dir, *args).strip()

def get_gitlinks(mirror_dir, for_rev = 'HEAD'):
    paths = {}

    try:
        submodules = {}

        submodule_expr = re.compile(r'submodule\.(.*)\.([^.]*)\n(.*)$', re.M)
        for name, key, value in git_output_list(submodule_expr, mirror_dir,
                                                'config', '-lz', '--blob', for_rev + ':.gitmodules'):
            if name not in submodules:
                submodules[name] = {}

            submodules[name][key] = value

        for name, mod in submodules.iteritems():
            if 'path' in mod and 'url' in mod:
                paths[mod['path']] = { 'submodule-name': name, 'submodule-url': mod['url'] }

    except subprocess.CalledProcessError:
        # no .gitmodules?  no problem.
        pass

    gitlinks = {}
    missing = []

    gitlink_expr = re.compile(r'^160000 commit (\w*)\t(.*)$')
    rev_tree = git_output_line(mirror_dir, 'show', '--quiet', '--format=%T', for_rev)
    for sha, path in git_output_list(gitlink_expr, mirror_dir, 'ls-tree', '-rzd', rev_tree):
        if path in paths:
            gitlinks[path] = paths[path]
            gitlinks[path]['commit'] = sha
        else:
            missing.append(path)

    return gitlinks, missing

def mkdir_with_parents(filename):
    try:
        os.makedirs(filename)
    except EnvironmentError as e:
        if e.errno != errno.EEXIST or not os.path.isdir(filename):
            raise

def get_mirror_dir(giturl):
    assert '://' in giturl

    while giturl.endswith('/'):
        giturl = giturl[:-1]

    if not giturl.endswith('.git'):
        canonical = giturl + '.git'
    elif giturl.endswith('/.git'):
        canonical = giturl[:-5] + '.git'
    else:
        canonical = giturl

    canonical = canonical.replace('://', '/')

    return os.path.join(git_cache_dir, canonical)

def git_rev_needs_update(mirror_dir, rev):
    try:
        real_rev = git_output_line(mirror_dir, 'rev-parse', rev)
        if real_rev != rev:
            return True

    except subprocess.CalledProcessError:
        return True

    try:
        if git_output_line(mirror_dir, 'cat-file', '-t', rev) == 'commit':
            return False

    except subprocess.CalledProcessError:
        return True

    return True

def fetch_or_clone(mirror_dir, giturl, for_rev):
    if os.path.exists(os.path.join(mirror_dir, 'HEAD')):
        if git_rev_needs_update(mirror_dir, for_rev):
            git('--git-dir', mirror_dir, 'fetch')

    else:
        mkdir_with_parents(os.path.dirname(mirror_dir))

        git('clone', '--mirror', giturl, mirror_dir)

def relative_url(base, url):
    prot, host, path, _, _ = urlparse.urlsplit(url)

    if not prot:
        baseprot, basehost, basepath, _, _ = urlparse.urlsplit(base + '/')
        path = posixpath.normpath(posixpath.join(basepath, path))
        url = urlparse.urlunsplit((baseprot, basehost, path, None, None))

    return url

def update(giturl, for_rev = 'HEAD'):
    mirror_dir = get_mirror_dir(giturl)

    fetch_or_clone(mirror_dir, giturl, for_rev)
    gitlinks, missing = get_gitlinks(mirror_dir, for_rev)

    for gitlink in gitlinks.itervalues():
        submodule_url = relative_url(giturl, gitlink['submodule-url'])

        update(submodule_url, gitlink['commit'])

def checkout_submodules(giturl, mirror_dir, workdir, rev):
    gitlinks, missing = get_gitlinks(mirror_dir, rev)

    if missing:
        raise Exception('Can not checkout {url} due to missing missing submodules for paths: {missing}.'.format(
                          url = giturl, missing = ' '.join(missing)))

    for path, gitlink in gitlinks.iteritems():
        submodule_url = relative_url(giturl, gitlink['submodule-url'])
        submodule_mirror = get_mirror_dir(submodule_url)
        submodule_name = gitlink['submodule-name']
        commit = gitlink['commit']

        git('submodule', 'init', '--', path, cwd=workdir)
        git('config', 'submodule.{}.url'.format(submodule_name), submodule_mirror, cwd=workdir)
        git('submodule', 'update', '--', path, cwd=workdir)

        checkout_submodules(submodule_url, submodule_mirror, os.path.join(workdir, path), commit)

def checkout(giturl, target, branch = 'HEAD'):
    mirror_dir = get_mirror_dir(giturl)

    if branch != 'HEAD':
        git('clone', '--branch', branch, mirror_dir, target)
    else:
        git('clone', mirror_dir, target)

    checkout_submodules(giturl, mirror_dir, target, branch)

import sys
if sys.argv[1] == 'update':
    print update(*sys.argv[2:])
elif sys.argv[1] == 'checkout':
    print checkout(*sys.argv[2:])
