1a61c73dbSMehdi Amini#!/usr/bin/env python3 2a61c73dbSMehdi Amini# 3a61c73dbSMehdi Amini# ======- pre-push - LLVM Git Help Integration ---------*- python -*--========# 4a61c73dbSMehdi Amini# 5a61c73dbSMehdi Amini# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6a61c73dbSMehdi Amini# See https://llvm.org/LICENSE.txt for license information. 7a61c73dbSMehdi Amini# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8a61c73dbSMehdi Amini# 9a61c73dbSMehdi Amini# ==------------------------------------------------------------------------==# 10a61c73dbSMehdi Amini 11a61c73dbSMehdi Amini""" 12a61c73dbSMehdi Aminipre-push git hook integration 13a61c73dbSMehdi Amini============================= 14a61c73dbSMehdi Amini 15a61c73dbSMehdi AminiThis script is intended to be setup as a pre-push hook, from the root of the 16a61c73dbSMehdi Aminirepo run: 17a61c73dbSMehdi Amini 18a61c73dbSMehdi Amini ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push 19a61c73dbSMehdi Amini 20a61c73dbSMehdi AminiFrom the git doc: 21a61c73dbSMehdi Amini 22a61c73dbSMehdi Amini The pre-push hook runs during git push, after the remote refs have been 23a61c73dbSMehdi Amini updated but before any objects have been transferred. It receives the name 24a61c73dbSMehdi Amini and location of the remote as parameters, and a list of to-be-updated refs 25a61c73dbSMehdi Amini through stdin. You can use it to validate a set of ref updates before a push 26a61c73dbSMehdi Amini occurs (a non-zero exit code will abort the push). 27a61c73dbSMehdi Amini""" 28a61c73dbSMehdi Amini 29a61c73dbSMehdi Aminiimport argparse 30a61c73dbSMehdi Aminiimport shutil 31a61c73dbSMehdi Aminiimport subprocess 32a61c73dbSMehdi Aminiimport sys 33a61c73dbSMehdi Aminiimport time 34a61c73dbSMehdi Aminifrom shlex import quote 35a61c73dbSMehdi Amini 36a61c73dbSMehdi AminiVERBOSE = False 37a61c73dbSMehdi AminiQUIET = False 38a61c73dbSMehdi Aminidev_null_fd = None 39b71edfaaSTobias Hietaz40 = "0000000000000000000000000000000000000000" 40a61c73dbSMehdi Amini 41a61c73dbSMehdi Amini 42a61c73dbSMehdi Aminidef eprint(*args, **kwargs): 43a61c73dbSMehdi Amini print(*args, file=sys.stderr, **kwargs) 44a61c73dbSMehdi Amini 45a61c73dbSMehdi Amini 46a61c73dbSMehdi Aminidef log(*args, **kwargs): 47a61c73dbSMehdi Amini if QUIET: 48a61c73dbSMehdi Amini return 49a61c73dbSMehdi Amini print(*args, **kwargs) 50a61c73dbSMehdi Amini 51a61c73dbSMehdi Amini 52a61c73dbSMehdi Aminidef log_verbose(*args, **kwargs): 53a61c73dbSMehdi Amini if not VERBOSE: 54a61c73dbSMehdi Amini return 55a61c73dbSMehdi Amini print(*args, **kwargs) 56a61c73dbSMehdi Amini 57a61c73dbSMehdi Amini 58a61c73dbSMehdi Aminidef die(msg): 59a61c73dbSMehdi Amini eprint(msg) 60a61c73dbSMehdi Amini sys.exit(1) 61a61c73dbSMehdi Amini 62a61c73dbSMehdi Amini 63a61c73dbSMehdi Aminidef ask_confirm(prompt): 64a61c73dbSMehdi Amini while True: 65b71edfaaSTobias Hieta query = input("%s (y/N): " % (prompt)) 66b71edfaaSTobias Hieta if query.lower() not in ["y", "n", ""]: 67b71edfaaSTobias Hieta print("Expect y or n!") 68a61c73dbSMehdi Amini continue 69b71edfaaSTobias Hieta return query.lower() == "y" 70a61c73dbSMehdi Amini 71a61c73dbSMehdi Amini 72b71edfaaSTobias Hietadef shell( 73b71edfaaSTobias Hieta cmd, 74b71edfaaSTobias Hieta strip=True, 75b71edfaaSTobias Hieta cwd=None, 76b71edfaaSTobias Hieta stdin=None, 77b71edfaaSTobias Hieta die_on_failure=True, 78b71edfaaSTobias Hieta ignore_errors=False, 79b71edfaaSTobias Hieta text=True, 80b71edfaaSTobias Hieta print_raw_stderr=False, 81b71edfaaSTobias Hieta): 82a61c73dbSMehdi Amini # Escape args when logging for easy repro. 83a61c73dbSMehdi Amini quoted_cmd = [quote(arg) for arg in cmd] 84b71edfaaSTobias Hieta cwd_msg = "" 85a61c73dbSMehdi Amini if cwd: 86b71edfaaSTobias Hieta cwd_msg = " in %s" % cwd 87b71edfaaSTobias Hieta log_verbose("Running%s: %s" % (cwd_msg, " ".join(quoted_cmd))) 88a61c73dbSMehdi Amini 89a61c73dbSMehdi Amini # Silence errors if requested. 90*c49770c6SNicolas van Kempen err_pipe = subprocess.DEVNULL if ignore_errors else subprocess.PIPE 91a61c73dbSMehdi Amini 92a61c73dbSMehdi Amini start = time.time() 93b71edfaaSTobias Hieta p = subprocess.Popen( 94b71edfaaSTobias Hieta cmd, 95b71edfaaSTobias Hieta cwd=cwd, 96b71edfaaSTobias Hieta stdout=subprocess.PIPE, 97b71edfaaSTobias Hieta stderr=err_pipe, 98a61c73dbSMehdi Amini stdin=subprocess.PIPE, 99b71edfaaSTobias Hieta universal_newlines=text, 100b71edfaaSTobias Hieta ) 101a61c73dbSMehdi Amini stdout, stderr = p.communicate(input=stdin) 102a61c73dbSMehdi Amini elapsed = time.time() - start 103a61c73dbSMehdi Amini 104b71edfaaSTobias Hieta log_verbose("Command took %0.1fs" % elapsed) 105a61c73dbSMehdi Amini 106a61c73dbSMehdi Amini if p.returncode == 0 or ignore_errors: 107a61c73dbSMehdi Amini if stderr and not ignore_errors: 108a61c73dbSMehdi Amini if not print_raw_stderr: 109b71edfaaSTobias Hieta eprint("`%s` printed to stderr:" % " ".join(quoted_cmd)) 110a61c73dbSMehdi Amini eprint(stderr.rstrip()) 111a61c73dbSMehdi Amini if strip: 112a61c73dbSMehdi Amini if text: 113b71edfaaSTobias Hieta stdout = stdout.rstrip("\r\n") 114a61c73dbSMehdi Amini else: 115b71edfaaSTobias Hieta stdout = stdout.rstrip(b"\r\n") 116a61c73dbSMehdi Amini if VERBOSE: 117a61c73dbSMehdi Amini for l in stdout.splitlines(): 118b71edfaaSTobias Hieta log_verbose("STDOUT: %s" % l) 119a61c73dbSMehdi Amini return stdout 120b71edfaaSTobias Hieta err_msg = "`%s` returned %s" % (" ".join(quoted_cmd), p.returncode) 121a61c73dbSMehdi Amini eprint(err_msg) 122a61c73dbSMehdi Amini if stderr: 123a61c73dbSMehdi Amini eprint(stderr.rstrip()) 124a61c73dbSMehdi Amini if die_on_failure: 125a61c73dbSMehdi Amini sys.exit(2) 126a61c73dbSMehdi Amini raise RuntimeError(err_msg) 127a61c73dbSMehdi Amini 128a61c73dbSMehdi Amini 129a61c73dbSMehdi Aminidef git(*cmd, **kwargs): 130b71edfaaSTobias Hieta return shell(["git"] + list(cmd), **kwargs) 131a61c73dbSMehdi Amini 132a61c73dbSMehdi Amini 133a61c73dbSMehdi Aminidef get_revs_to_push(range): 134b71edfaaSTobias Hieta commits = git("rev-list", range).splitlines() 135a61c73dbSMehdi Amini # Reverse the order so we print the oldest commit first 136a61c73dbSMehdi Amini commits.reverse() 137a61c73dbSMehdi Amini return commits 138a61c73dbSMehdi Amini 139a61c73dbSMehdi Amini 140a61c73dbSMehdi Aminidef handle_push(args, local_ref, local_sha, remote_ref, remote_sha): 141b71edfaaSTobias Hieta """Check a single push request (which can include multiple revisions)""" 142b71edfaaSTobias Hieta log_verbose( 143b71edfaaSTobias Hieta "Handle push, reproduce with " 144b71edfaaSTobias Hieta "`echo %s %s %s %s | pre-push.py %s %s" 145b71edfaaSTobias Hieta % (local_ref, local_sha, remote_ref, remote_sha, args.remote, args.url) 146b71edfaaSTobias Hieta ) 147a61c73dbSMehdi Amini # Handle request to delete 148a61c73dbSMehdi Amini if local_sha == z40: 149b71edfaaSTobias Hieta if not ask_confirm( 150b71edfaaSTobias Hieta 'Are you sure you want to delete "%s" on remote "%s"?' 151b71edfaaSTobias Hieta % (remote_ref, args.url) 152b71edfaaSTobias Hieta ): 153a61c73dbSMehdi Amini die("Aborting") 154a61c73dbSMehdi Amini return 155a61c73dbSMehdi Amini 156a61c73dbSMehdi Amini # Push a new branch 157a61c73dbSMehdi Amini if remote_sha == z40: 158b71edfaaSTobias Hieta if not ask_confirm( 159b71edfaaSTobias Hieta 'Are you sure you want to push a new branch/tag "%s" on remote "%s"?' 160b71edfaaSTobias Hieta % (remote_ref, args.url) 161b71edfaaSTobias Hieta ): 162a61c73dbSMehdi Amini die("Aborting") 163a61c73dbSMehdi Amini range = local_sha 164a61c73dbSMehdi Amini return 165a61c73dbSMehdi Amini else: 166a61c73dbSMehdi Amini # Update to existing branch, examine new commits 167b71edfaaSTobias Hieta range = "%s..%s" % (remote_sha, local_sha) 168a61c73dbSMehdi Amini # Check that the remote commit exists, otherwise let git proceed 169b71edfaaSTobias Hieta if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True): 170a61c73dbSMehdi Amini return 171a61c73dbSMehdi Amini 172a61c73dbSMehdi Amini revs = get_revs_to_push(range) 173a61c73dbSMehdi Amini if not revs: 174a61c73dbSMehdi Amini # This can happen if someone is force pushing an older revision to a branch 175a61c73dbSMehdi Amini return 176a61c73dbSMehdi Amini 177a61c73dbSMehdi Amini # Print the revision about to be pushed commits 178a61c73dbSMehdi Amini print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url)) 179a61c73dbSMehdi Amini for sha in revs: 180b71edfaaSTobias Hieta print(" - " + git("show", "--oneline", "--quiet", sha)) 181a61c73dbSMehdi Amini 182a61c73dbSMehdi Amini if len(revs) > 1: 183b71edfaaSTobias Hieta if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)): 184b71edfaaSTobias Hieta die("Aborting") 185a61c73dbSMehdi Amini 186a61c73dbSMehdi Amini for sha in revs: 187b71edfaaSTobias Hieta msg = git("log", "--format=%B", "-n1", sha) 188b71edfaaSTobias Hieta if "Differential Revision" not in msg: 189a61c73dbSMehdi Amini continue 190a61c73dbSMehdi Amini for line in msg.splitlines(): 191b71edfaaSTobias Hieta for tag in ["Summary", "Reviewers", "Subscribers", "Tags"]: 192b71edfaaSTobias Hieta if line.startswith(tag + ":"): 193b71edfaaSTobias Hieta eprint( 194b71edfaaSTobias Hieta 'Please remove arcanist tags from the commit message (found "%s" tag in %s)' 195b71edfaaSTobias Hieta % (tag, sha[:12]) 196b71edfaaSTobias Hieta ) 197a61c73dbSMehdi Amini if len(revs) == 1: 198b71edfaaSTobias Hieta eprint("Try running: llvm/utils/git/arcfilter.sh") 199a61c73dbSMehdi Amini die('Aborting (force push by adding "--no-verify")') 200a61c73dbSMehdi Amini 201a61c73dbSMehdi Amini return 202a61c73dbSMehdi Amini 203a61c73dbSMehdi Amini 204b71edfaaSTobias Hietaif __name__ == "__main__": 205b71edfaaSTobias Hieta if not shutil.which("git"): 206b71edfaaSTobias Hieta die("error: cannot find git command") 207a61c73dbSMehdi Amini 208a61c73dbSMehdi Amini argv = sys.argv[1:] 209a61c73dbSMehdi Amini p = argparse.ArgumentParser( 210b71edfaaSTobias Hieta prog="pre-push", 211b71edfaaSTobias Hieta formatter_class=argparse.RawDescriptionHelpFormatter, 212b71edfaaSTobias Hieta description=__doc__, 213b71edfaaSTobias Hieta ) 214a61c73dbSMehdi Amini verbosity_group = p.add_mutually_exclusive_group() 215b71edfaaSTobias Hieta verbosity_group.add_argument( 216b71edfaaSTobias Hieta "-q", "--quiet", action="store_true", help="print less information" 217b71edfaaSTobias Hieta ) 218b71edfaaSTobias Hieta verbosity_group.add_argument( 219b71edfaaSTobias Hieta "-v", "--verbose", action="store_true", help="print more information" 220b71edfaaSTobias Hieta ) 221a61c73dbSMehdi Amini 222b71edfaaSTobias Hieta p.add_argument("remote", type=str, help="Name of the remote") 223b71edfaaSTobias Hieta p.add_argument("url", type=str, help="URL for the remote") 224a61c73dbSMehdi Amini 225a61c73dbSMehdi Amini args = p.parse_args(argv) 226a61c73dbSMehdi Amini VERBOSE = args.verbose 227a61c73dbSMehdi Amini QUIET = args.quiet 228a61c73dbSMehdi Amini 229a61c73dbSMehdi Amini lines = sys.stdin.readlines() 230b71edfaaSTobias Hieta sys.stdin = open("/dev/tty", "r") 231a61c73dbSMehdi Amini for line in lines: 232a61c73dbSMehdi Amini local_ref, local_sha, remote_ref, remote_sha = line.split() 233a61c73dbSMehdi Amini handle_push(args, local_ref, local_sha, remote_ref, remote_sha) 234