#!/usr/bin/env python3
# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Creates a Git hook that calls a script with certain arguments."""

import argparse
import logging
import os
from pathlib import Path
import re
import shlex
import subprocess
from typing import Optional, Sequence, Union

_LOG: logging.Logger = logging.getLogger(__name__)


def git_repo_root(path: Union[Path, str]) -> Path:
    return Path(
        subprocess.run(
            ['git', '-C', path, 'rev-parse', '--show-toplevel'],
            check=True,
            stdout=subprocess.PIPE,
        )
        .stdout.strip()
        .decode()
    )


def _stdin_args_for_hook(hook) -> Sequence[str]:
    """Gives stdin arguments for each hook.

    See https://git-scm.com/docs/githooks for more information.
    """
    if hook == 'pre-push':
        return (
            'local_ref',
            'local_object_name',
            'remote_ref',
            'remote_object_name',
        )
    if hook in ('pre-receive', 'post-receive', 'reference-transaction'):
        return ('old_value', 'new_value', 'ref_name')
    if hook == 'post-rewrite':
        return ('old_object_name', 'new_object_name')
    return ()


def _replace_arg_in_hook(arg: str, unquoted_args: Sequence[str]) -> str:
    if arg in unquoted_args:
        return arg
    return shlex.quote(arg)


def install_git_hook(
    hook: str,
    command: Sequence[Union[Path, str]],
    repository: Union[Path, str] = '.',
) -> None:
    """Installs a simple Git hook that executes the provided command.

    Args:
      hook: Git hook to install, e.g. 'pre-push'.
      command: Command to execute as the hook. The command is executed from the
          root of the repo. Arguments are sanitised with `shlex.quote`, except
          for any arguments are equal to f'${stdin_arg}' for some `stdin_arg`
          that matches a standard-input argument to the git hook.
      repository: Repository to install the hook in.
    """
    if not command:
        raise ValueError('The command cannot be empty!')

    root = git_repo_root(repository).resolve()

    if root.joinpath('.git').is_dir():
        hook_path = root.joinpath('.git', 'hooks', hook)
    else:  # This repo is probably a submodule with a .git file instead
        match = re.match('^gitdir: (.*)$', root.joinpath('.git').read_text())
        if not match:
            raise ValueError('Unexpected format for .git file')

        hook_path = root.joinpath(match.group(1), 'hooks', hook).resolve()

    hook_path.parent.mkdir(exist_ok=True)

    hook_stdin_args = _stdin_args_for_hook(hook)
    read_stdin_command = 'read ' + ' '.join(hook_stdin_args)

    unquoted_args = [f'${arg}' for arg in hook_stdin_args]
    args = (_replace_arg_in_hook(str(a), unquoted_args) for a in command[1:])

    command_str = ' '.join([shlex.quote(str(command[0])), *args])

    with hook_path.open('w') as file:
        line = lambda *args: print(*args, file=file)

        line('#!/bin/sh')
        line(f'# {hook} hook generated by {__file__}')
        line()
        line('# Unset Git environment variables, which are set when this is ')
        line('# run as a Git hook. These environment variables cause issues ')
        line('# when trying to run Git commands on other repos from a ')
        line('# submodule hook.')
        line('unset $(git rev-parse --local-env-vars)')
        line()
        line('# Read the stdin args for the hook, made available by git.')
        line(read_stdin_command)
        line()
        line(command_str)

    hook_path.chmod(0o755)
    logging.info(
        'Installed %s hook for `%s` at %s', hook, command_str, hook_path
    )


def argument_parser(
    parser: Optional[argparse.ArgumentParser] = None,
) -> argparse.ArgumentParser:
    if parser is None:
        parser = argparse.ArgumentParser(description=__doc__)

    def path(arg: str) -> Path:
        if not os.path.exists(arg):
            raise argparse.ArgumentTypeError(f'"{arg}" is not a valid path')

        return Path(arg)

    parser.add_argument(
        '-r',
        '--repository',
        default='.',
        type=path,
        help='Path to the repository in which to install the hook',
    )
    parser.add_argument(
        '--hook', required=True, help='Which type of Git hook to create'
    )
    parser.add_argument(
        'command', nargs='*', help='Command to run in the commit hook'
    )

    return parser


if __name__ == '__main__':
    logging.basicConfig(format='%(message)s', level=logging.INFO)
    install_git_hook(**vars(argument_parser().parse_args()))
