#! /usr/bin/env python3

import configparser
import hashlib
import os
import subprocess
import sys

URL = os.environ['PERU_MODULE_URL']
REV = os.environ['PERU_MODULE_REV'] or 'master'
REUP = os.environ['PERU_MODULE_REUP'] or 'master'

# Because peru gives each plugin a unique cache dir based on its cacheable
# fields (in this case, url) we could clone directly into cache_root. However,
# because the git plugin needs to handle git submodules as well, it still has
# to separate things out by repo url.
CACHE_ROOT = os.environ['PERU_PLUGIN_CACHE']


def git(*args, git_dir=None, capture_output=False):
    # Avoid forgetting this arg.
    assert git_dir is None or os.path.isdir(git_dir)

    command = ['git']
    if git_dir:
        command.append('--git-dir={0}'.format(git_dir))
    command.extend(args)

    stdout = subprocess.PIPE if capture_output else None
    # Always let stderr print to the caller.
    process = subprocess.Popen(
        command,
        stdin=subprocess.DEVNULL,
        stdout=stdout,
        universal_newlines=True)
    output, _ = process.communicate()
    if process.returncode != 0:
        sys.exit(1)

    return output


def has_clone(url):
    return os.path.exists(repo_cache_path(url))


def clone_if_needed(url):
    repo_path = repo_cache_path(url)
    if not has_clone(url):
        git('clone', '--mirror', '--progress', url, repo_path)
    return repo_path


def repo_cache_path(url):
    # If we just concatenate the escaped repo URL into the path, we start to
    # run up against the 260-character path limit on Windows.
    url_hash = hashlib.sha1(url.encode()).hexdigest()
    return os.path.join(CACHE_ROOT, url_hash)


def clone_and_maybe_print(url):
    if not has_clone(url):
        print('git clone ' + url)
    return clone_if_needed(url)


def git_fetch(url, repo_path):
    print('git fetch ' + url)
    git('fetch', '--prune', git_dir=repo_path)


def already_has_rev(repo, rev):
    try:
        # Make sure the rev exists.
        git('cat-file', '-e', rev, git_dir=repo)
        # Get the hash for the rev.
        output = git('rev-parse', rev, git_dir=repo, capture_output=True)
    except:
        return False

    # Only return True for revs that are absolute hashes.
    # We could consider treating tags the way, but...
    # 1) Tags actually can change.
    # 2) It's not clear at a glance if something is a branch or a tag.
    # Keep it simple.
    return output.strip() == rev


def checkout_tree(url, rev, dest):
    repo_path = clone_and_maybe_print(url)
    if not already_has_rev(repo_path, rev):
        git_fetch(url, repo_path)
    # If we just use `git checkout rev -- .` here, we get an error when rev is
    # an empty commit.
    git('--work-tree=' + dest, 'read-tree', rev, git_dir=repo_path)
    git('--work-tree=' + dest, 'checkout-index', '--all', git_dir=repo_path)
    checkout_submodules(repo_path, rev, dest)


def checkout_submodules(repo_path, rev, work_tree):
    if os.environ['PERU_MODULE_SUBMODULES'] == 'false':
        return

    gitmodules = os.path.join(work_tree, '.gitmodules')
    if not os.path.exists(gitmodules):
        return

    parser = configparser.ConfigParser()
    parser.read(gitmodules)
    for section in parser.sections():
        sub_relative_path = parser[section]['path']
        sub_full_path = os.path.join(work_tree, sub_relative_path)
        sub_url = parser[section]['url']
        ls_tree = git('ls-tree', rev, sub_relative_path,
                      git_dir=repo_path, capture_output=True)
        # Normally when you run `git submodule add ...`, git puts two things in
        # your repo: an entry in .gitmodules, and a commit object at the
        # appropriate path inside your repo. However, it's possible for those
        # two to get out of sync, especially if you use mv/rm on a directory
        # followed by `git add`, instead of the smarter `git mv`/`git rm`. If
        # we run into one of these missing submodules, just skip it.
        if len(ls_tree.strip()) == 0:
            print('WARNING: sudmodule ' + sub_relative_path +
                  ' is configured in .gitmodules, but missing in the repo')
            continue
        sub_rev = ls_tree.split()[2]
        checkout_tree(sub_url, sub_rev, sub_full_path)


def plugin_sync():
    checkout_tree(URL, REV, os.environ['PERU_SYNC_DEST'])


def plugin_reup():
    reup_output = os.environ['PERU_REUP_OUTPUT']
    repo_path = clone_if_needed(URL)
    git_fetch(URL, repo_path)
    output = git('rev-parse', REUP, git_dir=repo_path, capture_output=True)
    with open(reup_output, 'w') as out_file:
        print('rev:', output.strip(), file=out_file)

command = os.environ['PERU_PLUGIN_COMMAND']
if command == 'sync':
    plugin_sync()
elif command == 'reup':
    plugin_reup()
else:
    raise RuntimeError('Unknown command: ' + repr(command))
