diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst index c99f4964b5..55199658e4 100644 --- a/pw_presubmit/docs.rst +++ b/pw_presubmit/docs.rst @@ -191,7 +191,7 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example. from pathlib import Path import re import sys - from typing import List, Pattern + from typing import List, Optional, Pattern try: import pw_cli.log @@ -283,16 +283,34 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example. PROGRAMS = pw_presubmit.Programs(other=OTHER, quick=QUICK, full=FULL) - def run(install: bool, **presubmit_args) -> int: + # + # Allowlist of remote refs for presubmit. If the remote ref being pushed to + # matches any of these values (with regex matching), then the presubmits + # checks will be run before pushing. + # + PRE_PUSH_REMOTE_REF_ALLOWLIST = ( + 'refs/for/main', + ) + + + def run(install: bool, remote_ref: Optional[str], **presubmit_args) -> int: """Process the --install argument then invoke pw_presubmit.""" # Install the presubmit Git pre-push hook, if requested. if install: - install_hook(__file__, 'pre-push', ['--base', 'HEAD~'], + # '$remote_ref' will be replaced by the actual value of the remote ref + # at runtime. + install_hook(__file__, 'pre-push', + ['--base', 'HEAD~', '--remote-ref', '$remote_ref'], git_repo.root()) return 0 - return cli.run(root=PROJECT_ROOT, **presubmit_args) + # Run the checks if either no remote_ref was passed, or if the remote ref + # matches anything in the allowlist. + if remote_ref is None or any( + re.search(pattern, remote_ref) + for pattern in PRE_PUSH_REMOTE_REF_ALLOWLIST): + return cli.run(root=PROJECT_ROOT, **presubmit_args) def main() -> int: @@ -306,6 +324,16 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example. action='store_true', help='Install the presubmit as a Git pre-push hook and exit.') + # Define an optional flag to pass the remote ref into this script, if it + # is run as a pre-push hook. The destination variable in the parsed args + # will be `remote_ref`, as dashes are replaced with underscores to make + # valid variable names. + parser.add_argument( + '--remote-ref', + default=None, + nargs='?', # Make optional. + help='Remote ref of the push command, for use by the pre-push hook.') + return run(**vars(parser.parse_args())) if __name__ == '__main__': diff --git a/pw_presubmit/py/pw_presubmit/install_hook.py b/pw_presubmit/py/pw_presubmit/install_hook.py index 62363f952c..2a0ee346c8 100755 --- a/pw_presubmit/py/pw_presubmit/install_hook.py +++ b/pw_presubmit/py/pw_presubmit/install_hook.py @@ -33,11 +33,42 @@ def git_repo_root(path: Union[Path, str]) -> Path: 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_hook(script, hook: str, args: Sequence[str] = (), repository: Union[Path, str] = '.') -> None: - """Installs a simple Git hook that calls a script with arguments.""" + """Installs a simple Git hook that calls a script with arguments. + + Args: + script: Path to the script to run in the hook. + hook: Git hook to install, e.g. 'pre-push'. + args: Arguments to pass to `script` when it is run in the hook. These will + be sanitised with `shlex.quote`, except for any arguments are equal to + f'${stdin_arg}' for some `stdin_arg` which matches a standard-input + argument to the git hook. + repository: Repository to install the hook in. + """ root = git_repo_root(repository).resolve() script = os.path.relpath(script, root) @@ -52,7 +83,12 @@ def install_hook(script, hook_path.parent.mkdir(exist_ok=True) - command = ' '.join(shlex.quote(arg) for arg in (script, *args)) + 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] + script_command = ' '.join( + _replace_arg_in_hook(arg, unquoted_args) for arg in (script, *args)) with hook_path.open('w') as file: line = lambda *args: print(*args, file=file) @@ -66,7 +102,10 @@ def install_hook(script, line('# submodule hook.') line('unset $(git rev-parse --local-env-vars)') line() - line(command) + line('# Read the stdin args for the hook, made available by git.') + line(read_stdin_command) + line() + line(script_command) hook_path.chmod(0o755) logging.info('Created %s hook for %s at %s', hook, script, hook_path)