blob: fc86fcfa102a13940194824ecc05270d4cf55125 [file] [log] [blame]
#!/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 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: 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()))