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