1#!/usr/bin/env python3 2# 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-exception 8# 9# ==------------------------------------------------------------------------==# 10 11""" 12pre-push git hook integration 13============================= 14 15This script is intended to be setup as a pre-push hook, from the root of the 16repo run: 17 18 ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push 19 20From the git doc: 21 22 The pre-push hook runs during git push, after the remote refs have been 23 updated but before any objects have been transferred. It receives the name 24 and location of the remote as parameters, and a list of to-be-updated refs 25 through stdin. You can use it to validate a set of ref updates before a push 26 occurs (a non-zero exit code will abort the push). 27""" 28 29import argparse 30import shutil 31import subprocess 32import sys 33import time 34from shlex import quote 35 36VERBOSE = False 37QUIET = False 38dev_null_fd = None 39z40 = "0000000000000000000000000000000000000000" 40 41 42def eprint(*args, **kwargs): 43 print(*args, file=sys.stderr, **kwargs) 44 45 46def log(*args, **kwargs): 47 if QUIET: 48 return 49 print(*args, **kwargs) 50 51 52def log_verbose(*args, **kwargs): 53 if not VERBOSE: 54 return 55 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 continue 69 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" % cwd 87 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.PIPE 91 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() - start 103 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 stdout 120 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 first 136 commits.reverse() 137 return commits 138 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 delete 148 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 return 155 156 # Push a new branch 157 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_sha 164 return 165 else: 166 # Update to existing branch, examine new commits 167 range = "%s..%s" % (remote_sha, local_sha) 168 # Check that the remote commit exists, otherwise let git proceed 169 if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True): 170 return 171 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 branch 175 return 176 177 # Print the revision about to be pushed commits 178 print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url)) 179 for sha in revs: 180 print(" - " + git("show", "--oneline", "--quiet", sha)) 181 182 if len(revs) > 1: 183 if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)): 184 die("Aborting") 185 186 for sha in revs: 187 msg = git("log", "--format=%B", "-n1", sha) 188 if "Differential Revision" not in msg: 189 continue 190 for line in msg.splitlines(): 191 for tag in ["Summary", "Reviewers", "Subscribers", "Tags"]: 192 if line.startswith(tag + ":"): 193 eprint( 194 'Please remove arcanist tags from the commit message (found "%s" tag in %s)' 195 % (tag, sha[:12]) 196 ) 197 if len(revs) == 1: 198 eprint("Try running: llvm/utils/git/arcfilter.sh") 199 die('Aborting (force push by adding "--no-verify")') 200 201 return 202 203 204if __name__ == "__main__": 205 if not shutil.which("git"): 206 die("error: cannot find git command") 207 208 argv = sys.argv[1:] 209 p = argparse.ArgumentParser( 210 prog="pre-push", 211 formatter_class=argparse.RawDescriptionHelpFormatter, 212 description=__doc__, 213 ) 214 verbosity_group = p.add_mutually_exclusive_group() 215 verbosity_group.add_argument( 216 "-q", "--quiet", action="store_true", help="print less information" 217 ) 218 verbosity_group.add_argument( 219 "-v", "--verbose", action="store_true", help="print more information" 220 ) 221 222 p.add_argument("remote", type=str, help="Name of the remote") 223 p.add_argument("url", type=str, help="URL for the remote") 224 225 args = p.parse_args(argv) 226 VERBOSE = args.verbose 227 QUIET = args.quiet 228 229 lines = sys.stdin.readlines() 230 sys.stdin = open("/dev/tty", "r") 231 for line in lines: 232 local_ref, local_sha, remote_ref, remote_sha = line.split() 233 handle_push(args, local_ref, local_sha, remote_ref, remote_sha) 234