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 os 31import shutil 32import subprocess 33import sys 34import time 35from shlex import quote 36 37VERBOSE = False 38QUIET = False 39dev_null_fd = None 40z40 = '0000000000000000000000000000000000000000' 41 42 43def eprint(*args, **kwargs): 44 print(*args, file=sys.stderr, **kwargs) 45 46 47def log(*args, **kwargs): 48 if QUIET: 49 return 50 print(*args, **kwargs) 51 52 53def log_verbose(*args, **kwargs): 54 if not VERBOSE: 55 return 56 print(*args, **kwargs) 57 58 59def die(msg): 60 eprint(msg) 61 sys.exit(1) 62 63 64def ask_confirm(prompt): 65 while True: 66 query = input('%s (y/N): ' % (prompt)) 67 if query.lower() not in ['y', 'n', '']: 68 print('Expect y or n!') 69 continue 70 return query.lower() == 'y' 71 72 73def get_dev_null(): 74 """Lazily create a /dev/null fd for use in shell()""" 75 global dev_null_fd 76 if dev_null_fd is None: 77 dev_null_fd = open(os.devnull, 'w') 78 return dev_null_fd 79 80 81def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True, 82 ignore_errors=False, text=True, print_raw_stderr=False): 83 # Escape args when logging for easy repro. 84 quoted_cmd = [quote(arg) for arg in cmd] 85 cwd_msg = '' 86 if cwd: 87 cwd_msg = ' in %s' % cwd 88 log_verbose('Running%s: %s' % (cwd_msg, ' '.join(quoted_cmd))) 89 90 err_pipe = subprocess.PIPE 91 if ignore_errors: 92 # Silence errors if requested. 93 err_pipe = get_dev_null() 94 95 start = time.time() 96 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe, 97 stdin=subprocess.PIPE, 98 universal_newlines=text) 99 stdout, stderr = p.communicate(input=stdin) 100 elapsed = time.time() - start 101 102 log_verbose('Command took %0.1fs' % elapsed) 103 104 if p.returncode == 0 or ignore_errors: 105 if stderr and not ignore_errors: 106 if not print_raw_stderr: 107 eprint('`%s` printed to stderr:' % ' '.join(quoted_cmd)) 108 eprint(stderr.rstrip()) 109 if strip: 110 if text: 111 stdout = stdout.rstrip('\r\n') 112 else: 113 stdout = stdout.rstrip(b'\r\n') 114 if VERBOSE: 115 for l in stdout.splitlines(): 116 log_verbose('STDOUT: %s' % l) 117 return stdout 118 err_msg = '`%s` returned %s' % (' '.join(quoted_cmd), p.returncode) 119 eprint(err_msg) 120 if stderr: 121 eprint(stderr.rstrip()) 122 if die_on_failure: 123 sys.exit(2) 124 raise RuntimeError(err_msg) 125 126 127def git(*cmd, **kwargs): 128 return shell(['git'] + list(cmd), **kwargs) 129 130 131def get_revs_to_push(range): 132 commits = git('rev-list', range).splitlines() 133 # Reverse the order so we print the oldest commit first 134 commits.reverse() 135 return commits 136 137 138def handle_push(args, local_ref, local_sha, remote_ref, remote_sha): 139 '''Check a single push request (which can include multiple revisions)''' 140 log_verbose('Handle push, reproduce with ' 141 '`echo %s %s %s %s | pre-push.py %s %s' 142 % (local_ref, local_sha, remote_ref, remote_sha, args.remote, 143 args.url)) 144 # Handle request to delete 145 if local_sha == z40: 146 if not ask_confirm('Are you sure you want to delete "%s" on remote "%s"?' % (remote_ref, args.url)): 147 die("Aborting") 148 return 149 150 # Push a new branch 151 if remote_sha == z40: 152 if not ask_confirm('Are you sure you want to push a new branch/tag "%s" on remote "%s"?' % (remote_ref, args.url)): 153 die("Aborting") 154 range=local_sha 155 return 156 else: 157 # Update to existing branch, examine new commits 158 range='%s..%s' % (remote_sha, local_sha) 159 # Check that the remote commit exists, otherwise let git proceed 160 if "commit" not in git('cat-file','-t', remote_sha, ignore_errors=True): 161 return 162 163 revs = get_revs_to_push(range) 164 if not revs: 165 # This can happen if someone is force pushing an older revision to a branch 166 return 167 168 # Print the revision about to be pushed commits 169 print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url)) 170 for sha in revs: 171 print(' - ' + git('show', '--oneline', '--quiet', sha)) 172 173 if len(revs) > 1: 174 if not ask_confirm('Are you sure you want to push %d commits?' % len(revs)): 175 die('Aborting') 176 177 178 for sha in revs: 179 msg = git('log', '--format=%B', '-n1', sha) 180 if 'Differential Revision' not in msg: 181 continue 182 for line in msg.splitlines(): 183 for tag in ['Summary', 'Reviewers', 'Subscribers', 'Tags']: 184 if line.startswith(tag + ':'): 185 eprint('Please remove arcanist tags from the commit message (found "%s" tag in %s)' % (tag, sha[:12])) 186 if len(revs) == 1: 187 eprint('Try running: llvm/utils/git/arcfilter.sh') 188 die('Aborting (force push by adding "--no-verify")') 189 190 return 191 192 193if __name__ == '__main__': 194 if not shutil.which('git'): 195 die('error: cannot find git command') 196 197 argv = sys.argv[1:] 198 p = argparse.ArgumentParser( 199 prog='pre-push', formatter_class=argparse.RawDescriptionHelpFormatter, 200 description=__doc__) 201 verbosity_group = p.add_mutually_exclusive_group() 202 verbosity_group.add_argument('-q', '--quiet', action='store_true', 203 help='print less information') 204 verbosity_group.add_argument('-v', '--verbose', action='store_true', 205 help='print more information') 206 207 p.add_argument('remote', type=str, help='Name of the remote') 208 p.add_argument('url', type=str, help='URL for the remote') 209 210 args = p.parse_args(argv) 211 VERBOSE = args.verbose 212 QUIET = args.quiet 213 214 lines = sys.stdin.readlines() 215 sys.stdin = open('/dev/tty', 'r') 216 for line in lines: 217 local_ref, local_sha, remote_ref, remote_sha = line.split() 218 handle_push(args, local_ref, local_sha, remote_ref, remote_sha) 219