• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2020 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""Creates a Git hook that calls a script with certain arguments."""
16
17import argparse
18import logging
19import os
20from pathlib import Path
21import re
22import shlex
23import subprocess
24from typing import Sequence, Union
25
26_LOG: logging.Logger = logging.getLogger(__name__)
27
28
29def git_repo_root(path: Union[Path, str]) -> Path:
30    return Path(
31        subprocess.run(['git', '-C', path, 'rev-parse', '--show-toplevel'],
32                       check=True,
33                       stdout=subprocess.PIPE).stdout.strip().decode())
34
35
36def install_hook(script,
37                 hook: str,
38                 args: Sequence[str] = (),
39                 repository: Union[Path, str] = '.') -> None:
40    """Installs a simple Git hook that calls a script with arguments."""
41    root = git_repo_root(repository).resolve()
42    script = os.path.relpath(script, root)
43
44    if root.joinpath('.git').is_dir():
45        hook_path = root.joinpath('.git', 'hooks', hook)
46    else:  # This repo is probably a submodule with a .git file instead
47        match = re.match('^gitdir: (.*)$', root.joinpath('.git').read_text())
48        if not match:
49            raise ValueError('Unexpected format for .git file')
50
51        hook_path = root.joinpath(match.group(1), 'hooks', hook).resolve()
52
53    hook_path.parent.mkdir(exist_ok=True)
54
55    command = ' '.join(shlex.quote(arg) for arg in (script, *args))
56
57    with hook_path.open('w') as file:
58        line = lambda *args: print(*args, file=file)
59
60        line('#!/bin/sh')
61        line(f'# {hook} hook generated by {__file__}')
62        line()
63        line('# Unset Git environment variables, which are set when this is ')
64        line('# run as a Git hook. These environment variables cause issues ')
65        line('# when trying to run Git commands on other repos from a ')
66        line('# submodule hook.')
67        line('unset $(git rev-parse --local-env-vars)')
68        line()
69        line(command)
70
71    hook_path.chmod(0o755)
72    logging.info('Created %s hook for %s at %s', hook, script, hook_path)
73
74
75def argument_parser(parser=None) -> argparse.ArgumentParser:
76    if parser is None:
77        parser = argparse.ArgumentParser(description=__doc__)
78
79    def path(arg: str) -> Path:
80        if not os.path.exists(arg):
81            raise argparse.ArgumentTypeError(f'"{arg}" is not a valid path')
82
83        return Path(arg)
84
85    parser.add_argument(
86        '-r',
87        '--repository',
88        default='.',
89        type=path,
90        help='Path to the repository in which to install the hook')
91    parser.add_argument('--hook',
92                        required=True,
93                        help='Which type of Git hook to create')
94    parser.add_argument('-s',
95                        '--script',
96                        required=True,
97                        type=path,
98                        help='Path to the script to execute in the hook')
99    parser.add_argument('args',
100                        nargs='*',
101                        help='Arguments to provide to the commit hook')
102
103    return parser
104
105
106if __name__ == '__main__':
107    logging.basicConfig(format='%(message)s', level=logging.INFO)
108    install_hook(**vars(argument_parser().parse_args()))
109