| #!/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 os |
| 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 get_dev_null(): |
| """Lazily create a /dev/null fd for use in shell()""" |
| global dev_null_fd |
| if dev_null_fd is None: |
| dev_null_fd = open(os.devnull, "w") |
| return dev_null_fd |
| |
| |
| 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))) |
| |
| err_pipe = subprocess.PIPE |
| if ignore_errors: |
| # Silence errors if requested. |
| err_pipe = get_dev_null() |
| |
| 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: |
| print(" - " + git("show", "--oneline", "--quiet", sha)) |
| |
| if len(revs) > 1: |
| if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)): |
| die("Aborting") |
| |
| for sha in revs: |
| msg = git("log", "--format=%B", "-n1", sha) |
| if "Differential Revision" not in msg: |
| continue |
| for line in msg.splitlines(): |
| for tag in ["Summary", "Reviewers", "Subscribers", "Tags"]: |
| if line.startswith(tag + ":"): |
| eprint( |
| 'Please remove arcanist tags from the commit message (found "%s" tag in %s)' |
| % (tag, sha[:12]) |
| ) |
| if len(revs) == 1: |
| eprint("Try running: llvm/utils/git/arcfilter.sh") |
| die('Aborting (force push by adding "--no-verify")') |
| |
| 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) |