--- /dev/null
+#!/usr/bin/env python3
+#
+# ======- pre-push - LLVM Git Help Integration ---------*- python -*--========#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==------------------------------------------------------------------------==#
+
+"""
+pre-push git hook integration
+=============================
+
+This script is intended to be setup as a pre-push hook, from the root of the
+repo run:
+
+ ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push
+
+From the git doc:
+
+ The pre-push hook runs during git push, after the remote refs have been
+ updated but before any objects have been transferred. It receives the name
+ and location of the remote as parameters, and a list of to-be-updated refs
+ through stdin. You can use it to validate a set of ref updates before a push
+ occurs (a non-zero exit code will abort the push).
+"""
+
+import argparse
+import collections
+import os
+import re
+import shutil
+import subprocess
+import sys
+import time
+import getpass
+from shlex import quote
+
+VERBOSE = False
+QUIET = False
+dev_null_fd = None
+z40 = '0000000000000000000000000000000000000000'
+
+
+def eprint(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+
+def log(*args, **kwargs):
+ if QUIET:
+ return
+ print(*args, **kwargs)
+
+
+def log_verbose(*args, **kwargs):
+ if not VERBOSE:
+ return
+ print(*args, **kwargs)
+
+
+def die(msg):
+ eprint(msg)
+ sys.exit(1)
+
+
+def ask_confirm(prompt):
+ while True:
+ query = input('%s (y/N): ' % (prompt))
+ if query.lower() not in ['y', 'n', '']:
+ print('Expect y or n!')
+ continue
+ return query.lower() == 'y'
+
+
+def get_dev_null():
+ """Lazily create a /dev/null fd for use in shell()"""
+ global dev_null_fd
+ if dev_null_fd is None:
+ dev_null_fd = open(os.devnull, 'w')
+ return dev_null_fd
+
+
+def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True,
+ ignore_errors=False, text=True, print_raw_stderr=False):
+ # Escape args when logging for easy repro.
+ quoted_cmd = [quote(arg) for arg in cmd]
+ cwd_msg = ''
+ if cwd:
+ cwd_msg = ' in %s' % cwd
+ log_verbose('Running%s: %s' % (cwd_msg, ' '.join(quoted_cmd)))
+
+ err_pipe = subprocess.PIPE
+ if ignore_errors:
+ # Silence errors if requested.
+ err_pipe = get_dev_null()
+
+ start = time.time()
+ p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe,
+ stdin=subprocess.PIPE,
+ universal_newlines=text)
+ stdout, stderr = p.communicate(input=stdin)
+ elapsed = time.time() - start
+
+ log_verbose('Command took %0.1fs' % elapsed)
+
+ if p.returncode == 0 or ignore_errors:
+ if stderr and not ignore_errors:
+ if not print_raw_stderr:
+ eprint('`%s` printed to stderr:' % ' '.join(quoted_cmd))
+ eprint(stderr.rstrip())
+ if strip:
+ if text:
+ stdout = stdout.rstrip('\r\n')
+ else:
+ stdout = stdout.rstrip(b'\r\n')
+ if VERBOSE:
+ for l in stdout.splitlines():
+ log_verbose('STDOUT: %s' % l)
+ return stdout
+ err_msg = '`%s` returned %s' % (' '.join(quoted_cmd), p.returncode)
+ eprint(err_msg)
+ if stderr:
+ eprint(stderr.rstrip())
+ if die_on_failure:
+ sys.exit(2)
+ raise RuntimeError(err_msg)
+
+
+def git(*cmd, **kwargs):
+ return shell(['git'] + list(cmd), **kwargs)
+
+
+def get_revs_to_push(range):
+ commits = git('rev-list', range).splitlines()
+ # Reverse the order so we print the oldest commit first
+ commits.reverse()
+ return commits
+
+
+def handle_push(args, local_ref, local_sha, remote_ref, remote_sha):
+ '''Check a single push request (which can include multiple revisions)'''
+ log_verbose('Handle push, reproduce with '
+ '`echo %s %s %s %s | pre-push.py %s %s'
+ % (local_ref, local_sha, remote_ref, remote_sha, args.remote,
+ args.url))
+ # Handle request to delete
+ if local_sha == z40:
+ if not ask_confirm('Are you sure you want to delete "%s" on remote "%s"?' % (remote_ref, args.url)):
+ die("Aborting")
+ return
+
+ # Push a new branch
+ if remote_sha == z40:
+ if not ask_confirm('Are you sure you want to push a new branch/tag "%s" on remote "%s"?' % (remote_ref, args.url)):
+ die("Aborting")
+ range=local_sha
+ return
+ else:
+ # Update to existing branch, examine new commits
+ range='%s..%s' % (remote_sha, local_sha)
+ # Check that the remote commit exists, otherwise let git proceed
+ if "commit" not in git('cat-file','-t', remote_sha, ignore_errors=True):
+ return
+
+ revs = get_revs_to_push(range)
+ if not revs:
+ # This can happen if someone is force pushing an older revision to a branch
+ return
+
+ # Print the revision about to be pushed commits
+ print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url))
+ for sha in revs:
+ print(' - ' + git('show', '--oneline', '--quiet', sha))
+
+ if len(revs) > 1:
+ if not ask_confirm('Are you sure you want to push %d commits?' % len(revs)):
+ die('Aborting')
+
+
+ for sha in revs:
+ msg = git('log', '--format=%B', '-n1', sha)
+ if 'Differential Revision' not in msg:
+ continue
+ for line in msg.splitlines():
+ for tag in ['Summary', 'Reviewers', 'Subscribers', 'Tags']:
+ if line.startswith(tag + ':'):
+ eprint('Please remove arcanist tags from the commit message (found "%s" tag in %s)' % (tag, sha[:12]))
+ if len(revs) == 1:
+ eprint('Try running: llvm/utils/git/arcfilter.sh')
+ die('Aborting (force push by adding "--no-verify")')
+
+ return
+
+
+if __name__ == '__main__':
+ if not shutil.which('git'):
+ die('error: cannot find git command')
+
+ argv = sys.argv[1:]
+ p = argparse.ArgumentParser(
+ prog='pre-push', formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=__doc__)
+ verbosity_group = p.add_mutually_exclusive_group()
+ verbosity_group.add_argument('-q', '--quiet', action='store_true',
+ help='print less information')
+ verbosity_group.add_argument('-v', '--verbose', action='store_true',
+ help='print more information')
+
+ p.add_argument('remote', type=str, help='Name of the remote')
+ p.add_argument('url', type=str, help='URL for the remote')
+
+ args = p.parse_args(argv)
+ VERBOSE = args.verbose
+ QUIET = args.quiet
+
+ lines = sys.stdin.readlines()
+ sys.stdin = open('/dev/tty', 'r')
+ for line in lines:
+ local_ref, local_sha, remote_ref, remote_sha = line.split()
+ handle_push(args, local_ref, local_sha, remote_ref, remote_sha)