221 lines · python
1#!/usr/bin/env python32#3# ======- pre-push - LLVM Git Help Integration ---------*- python -*--========#4#5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.6# See https://llvm.org/LICENSE.txt for license information.7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception8#9# ==------------------------------------------------------------------------==#10 11"""12pre-push git hook integration13=============================14 15This script is intended to be setup as a pre-push hook, from the root of the16repo run:17 18 ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push19 20From the git doc:21 22 The pre-push hook runs during git push, after the remote refs have been23 updated but before any objects have been transferred. It receives the name24 and location of the remote as parameters, and a list of to-be-updated refs25 through stdin. You can use it to validate a set of ref updates before a push26 occurs (a non-zero exit code will abort the push).27"""28 29import argparse30import shutil31import subprocess32import sys33import time34from shlex import quote35 36VERBOSE = False37QUIET = False38dev_null_fd = None39z40 = "0000000000000000000000000000000000000000"40 41 42def eprint(*args, **kwargs):43 print(*args, file=sys.stderr, **kwargs)44 45 46def log(*args, **kwargs):47 if QUIET:48 return49 print(*args, **kwargs)50 51 52def log_verbose(*args, **kwargs):53 if not VERBOSE:54 return55 print(*args, **kwargs)56 57 58def die(msg):59 eprint(msg)60 sys.exit(1)61 62 63def ask_confirm(prompt):64 while True:65 query = input("%s (y/N): " % (prompt))66 if query.lower() not in ["y", "n", ""]:67 print("Expect y or n!")68 continue69 return query.lower() == "y"70 71 72def shell(73 cmd,74 strip=True,75 cwd=None,76 stdin=None,77 die_on_failure=True,78 ignore_errors=False,79 text=True,80 print_raw_stderr=False,81):82 # Escape args when logging for easy repro.83 quoted_cmd = [quote(arg) for arg in cmd]84 cwd_msg = ""85 if cwd:86 cwd_msg = " in %s" % cwd87 log_verbose("Running%s: %s" % (cwd_msg, " ".join(quoted_cmd)))88 89 # Silence errors if requested.90 err_pipe = subprocess.DEVNULL if ignore_errors else subprocess.PIPE91 92 start = time.time()93 p = subprocess.Popen(94 cmd,95 cwd=cwd,96 stdout=subprocess.PIPE,97 stderr=err_pipe,98 stdin=subprocess.PIPE,99 universal_newlines=text,100 )101 stdout, stderr = p.communicate(input=stdin)102 elapsed = time.time() - start103 104 log_verbose("Command took %0.1fs" % elapsed)105 106 if p.returncode == 0 or ignore_errors:107 if stderr and not ignore_errors:108 if not print_raw_stderr:109 eprint("`%s` printed to stderr:" % " ".join(quoted_cmd))110 eprint(stderr.rstrip())111 if strip:112 if text:113 stdout = stdout.rstrip("\r\n")114 else:115 stdout = stdout.rstrip(b"\r\n")116 if VERBOSE:117 for l in stdout.splitlines():118 log_verbose("STDOUT: %s" % l)119 return stdout120 err_msg = "`%s` returned %s" % (" ".join(quoted_cmd), p.returncode)121 eprint(err_msg)122 if stderr:123 eprint(stderr.rstrip())124 if die_on_failure:125 sys.exit(2)126 raise RuntimeError(err_msg)127 128 129def git(*cmd, **kwargs):130 return shell(["git"] + list(cmd), **kwargs)131 132 133def get_revs_to_push(range):134 commits = git("rev-list", range).splitlines()135 # Reverse the order so we print the oldest commit first136 commits.reverse()137 return commits138 139 140def handle_push(args, local_ref, local_sha, remote_ref, remote_sha):141 """Check a single push request (which can include multiple revisions)"""142 log_verbose(143 "Handle push, reproduce with "144 "`echo %s %s %s %s | pre-push.py %s %s"145 % (local_ref, local_sha, remote_ref, remote_sha, args.remote, args.url)146 )147 # Handle request to delete148 if local_sha == z40:149 if not ask_confirm(150 'Are you sure you want to delete "%s" on remote "%s"?'151 % (remote_ref, args.url)152 ):153 die("Aborting")154 return155 156 # Push a new branch157 if remote_sha == z40:158 if not ask_confirm(159 'Are you sure you want to push a new branch/tag "%s" on remote "%s"?'160 % (remote_ref, args.url)161 ):162 die("Aborting")163 range = local_sha164 return165 else:166 # Update to existing branch, examine new commits167 range = "%s..%s" % (remote_sha, local_sha)168 # Check that the remote commit exists, otherwise let git proceed169 if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True):170 return171 172 revs = get_revs_to_push(range)173 if not revs:174 # This can happen if someone is force pushing an older revision to a branch175 return176 177 # Print the revision about to be pushed commits178 print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url))179 for sha in revs[:10]:180 print(" - " + git("show", "--oneline", "--quiet", sha))181 if len(revs) > 10:182 print("and %d more!" % (len(revs) - 5))183 184 if len(revs) > 1:185 if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)):186 die("Aborting")187 188 return189 190 191if __name__ == "__main__":192 if not shutil.which("git"):193 die("error: cannot find git command")194 195 argv = sys.argv[1:]196 p = argparse.ArgumentParser(197 prog="pre-push",198 formatter_class=argparse.RawDescriptionHelpFormatter,199 description=__doc__,200 )201 verbosity_group = p.add_mutually_exclusive_group()202 verbosity_group.add_argument(203 "-q", "--quiet", action="store_true", help="print less information"204 )205 verbosity_group.add_argument(206 "-v", "--verbose", action="store_true", help="print more information"207 )208 209 p.add_argument("remote", type=str, help="Name of the remote")210 p.add_argument("url", type=str, help="URL for the remote")211 212 args = p.parse_args(argv)213 VERBOSE = args.verbose214 QUIET = args.quiet215 216 lines = sys.stdin.readlines()217 sys.stdin = open("/dev/tty", "r")218 for line in lines:219 local_ref, local_sha, remote_ref, remote_sha = line.split()220 handle_push(args, local_ref, local_sha, remote_ref, remote_sha)221