| #!/usr/bin/env python3 | |
| # | |
| # ======- pre-push - LLVM Git Help Integration ---------*- python -*--========# | |
| # | |
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| # See https://llvm.org/LICENSE.txt for license information. | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| # | |
| # ==------------------------------------------------------------------------==# | |
| """ | |
| pre-push git hook integration | |
| ============================= | |
| This script is intended to be setup as a pre-push hook, from the root of the | |
| repo run: | |
| ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push | |
| From the git doc: | |
| The pre-push hook runs during git push, after the remote refs have been | |
| updated but before any objects have been transferred. It receives the name | |
| and location of the remote as parameters, and a list of to-be-updated refs | |
| through stdin. You can use it to validate a set of ref updates before a push | |
| occurs (a non-zero exit code will abort the push). | |
| """ | |
| import argparse | |
| import shutil | |
| import subprocess | |
| import sys | |
| import time | |
| from shlex import quote | |
| VERBOSE = False | |
| QUIET = False | |
| dev_null_fd = None | |
| z40 = "0000000000000000000000000000000000000000" | |
| def eprint(*args, **kwargs): | |
| print(*args, file=sys.stderr, **kwargs) | |
| def log(*args, **kwargs): | |
| if QUIET: | |
| return | |
| print(*args, **kwargs) | |
| def log_verbose(*args, **kwargs): | |
| if not VERBOSE: | |
| return | |
| print(*args, **kwargs) | |
| def die(msg): | |
| eprint(msg) | |
| sys.exit(1) | |
| def ask_confirm(prompt): | |
| while True: | |
| query = input("%s (y/N): " % (prompt)) | |
| if query.lower() not in ["y", "n", ""]: | |
| print("Expect y or n!") | |
| continue | |
| return query.lower() == "y" | |
| def shell( | |
| cmd, | |
| strip=True, | |
| cwd=None, | |
| stdin=None, | |
| die_on_failure=True, | |
| ignore_errors=False, | |
| text=True, | |
| print_raw_stderr=False, | |
| ): | |
| # Escape args when logging for easy repro. | |
| quoted_cmd = [quote(arg) for arg in cmd] | |
| cwd_msg = "" | |
| if cwd: | |
| cwd_msg = " in %s" % cwd | |
| log_verbose("Running%s: %s" % (cwd_msg, " ".join(quoted_cmd))) | |
| # Silence errors if requested. | |
| err_pipe = subprocess.DEVNULL if ignore_errors else subprocess.PIPE | |
| start = time.time() | |
| p = subprocess.Popen( | |
| cmd, | |
| cwd=cwd, | |
| stdout=subprocess.PIPE, | |
| stderr=err_pipe, | |
| stdin=subprocess.PIPE, | |
| universal_newlines=text, | |
| ) | |
| stdout, stderr = p.communicate(input=stdin) | |
| elapsed = time.time() - start | |
| log_verbose("Command took %0.1fs" % elapsed) | |
| if p.returncode == 0 or ignore_errors: | |
| if stderr and not ignore_errors: | |
| if not print_raw_stderr: | |
| eprint("`%s` printed to stderr:" % " ".join(quoted_cmd)) | |
| eprint(stderr.rstrip()) | |
| if strip: | |
| if text: | |
| stdout = stdout.rstrip("\r\n") | |
| else: | |
| stdout = stdout.rstrip(b"\r\n") | |
| if VERBOSE: | |
| for l in stdout.splitlines(): | |
| log_verbose("STDOUT: %s" % l) | |
| return stdout | |
| err_msg = "`%s` returned %s" % (" ".join(quoted_cmd), p.returncode) | |
| eprint(err_msg) | |
| if stderr: | |
| eprint(stderr.rstrip()) | |
| if die_on_failure: | |
| sys.exit(2) | |
| raise RuntimeError(err_msg) | |
| def git(*cmd, **kwargs): | |
| return shell(["git"] + list(cmd), **kwargs) | |
| def get_revs_to_push(range): | |
| commits = git("rev-list", range).splitlines() | |
| # Reverse the order so we print the oldest commit first | |
| commits.reverse() | |
| return commits | |
| def handle_push(args, local_ref, local_sha, remote_ref, remote_sha): | |
| """Check a single push request (which can include multiple revisions)""" | |
| log_verbose( | |
| "Handle push, reproduce with " | |
| "`echo %s %s %s %s | pre-push.py %s %s" | |
| % (local_ref, local_sha, remote_ref, remote_sha, args.remote, args.url) | |
| ) | |
| # Handle request to delete | |
| if local_sha == z40: | |
| if not ask_confirm( | |
| 'Are you sure you want to delete "%s" on remote "%s"?' | |
| % (remote_ref, args.url) | |
| ): | |
| die("Aborting") | |
| return | |
| # Push a new branch | |
| if remote_sha == z40: | |
| if not ask_confirm( | |
| 'Are you sure you want to push a new branch/tag "%s" on remote "%s"?' | |
| % (remote_ref, args.url) | |
| ): | |
| die("Aborting") | |
| range = local_sha | |
| return | |
| else: | |
| # Update to existing branch, examine new commits | |
| range = "%s..%s" % (remote_sha, local_sha) | |
| # Check that the remote commit exists, otherwise let git proceed | |
| if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True): | |
| return | |
| revs = get_revs_to_push(range) | |
| if not revs: | |
| # This can happen if someone is force pushing an older revision to a branch | |
| return | |
| # Print the revision about to be pushed commits | |
| print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url)) | |
| for sha in revs[:10]: | |
| print(" - " + git("show", "--oneline", "--quiet", sha)) | |
| if len(revs) > 10: | |
| print("and %d more!" % (len(revs) - 5)) | |
| if len(revs) > 1: | |
| if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)): | |
| die("Aborting") | |
| return | |
| if __name__ == "__main__": | |
| if not shutil.which("git"): | |
| die("error: cannot find git command") | |
| argv = sys.argv[1:] | |
| p = argparse.ArgumentParser( | |
| prog="pre-push", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| description=__doc__, | |
| ) | |
| verbosity_group = p.add_mutually_exclusive_group() | |
| verbosity_group.add_argument( | |
| "-q", "--quiet", action="store_true", help="print less information" | |
| ) | |
| verbosity_group.add_argument( | |
| "-v", "--verbose", action="store_true", help="print more information" | |
| ) | |
| p.add_argument("remote", type=str, help="Name of the remote") | |
| p.add_argument("url", type=str, help="URL for the remote") | |
| args = p.parse_args(argv) | |
| VERBOSE = args.verbose | |
| QUIET = args.quiet | |
| lines = sys.stdin.readlines() | |
| sys.stdin = open("/dev/tty", "r") | |
| for line in lines: | |
| local_ref, local_sha, remote_ref, remote_sha = line.split() | |
| handle_push(args, local_ref, local_sha, remote_ref, remote_sha) |