3 # ======- pre-push - LLVM Git Help Integration ---------*- python -*--========#
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
9 # ==------------------------------------------------------------------------==#
12 pre-push git hook integration
13 =============================
15 This script is intended to be setup as a pre-push hook, from the root of the
18 ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push
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).
38 from shlex
import quote
43 z40
= '0000000000000000000000000000000000000000'
46 def eprint(*args
, **kwargs
):
47 print(*args
, file=sys
.stderr
, **kwargs
)
50 def log(*args
, **kwargs
):
53 print(*args
, **kwargs
)
56 def log_verbose(*args
, **kwargs
):
59 print(*args
, **kwargs
)
67 def ask_confirm(prompt
):
69 query
= input('%s (y/N): ' % (prompt
))
70 if query
.lower() not in ['y', 'n', '']:
71 print('Expect y or n!')
73 return query
.lower() == 'y'
77 """Lazily create a /dev/null fd for use in shell()"""
79 if dev_null_fd
is None:
80 dev_null_fd
= open(os
.devnull
, 'w')
84 def 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
]
90 cwd_msg
= ' in %s' % cwd
91 log_verbose('Running%s: %s' % (cwd_msg
, ' '.join(quoted_cmd
)))
93 err_pipe
= subprocess
.PIPE
95 # Silence errors if requested.
96 err_pipe
= get_dev_null()
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
105 log_verbose('Command took %0.1fs' % elapsed
)
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())
114 stdout
= stdout
.rstrip('\r\n')
116 stdout
= stdout
.rstrip(b
'\r\n')
118 for l
in stdout
.splitlines():
119 log_verbose('STDOUT: %s' % l
)
121 err_msg
= '`%s` returned %s' % (' '.join(quoted_cmd
), p
.returncode
)
124 eprint(stderr
.rstrip())
127 raise RuntimeError(err_msg
)
130 def git(*cmd
, **kwargs
):
131 return shell(['git'] + list(cmd
), **kwargs
)
134 def get_revs_to_push(range):
135 commits
= git('rev-list', range).splitlines()
136 # Reverse the order so we print the oldest commit first
141 def 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
,
147 # Handle request to delete
149 if not ask_confirm('Are you sure you want to delete "%s" on remote "%s"?' % (remote_ref
, args
.url
)):
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
)):
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):
166 revs
= get_revs_to_push(range)
168 # This can happen if someone is force pushing an older revision to a branch
171 # Print the revision about to be pushed commits
172 print('Pushing to "%s" on remote "%s"' % (remote_ref
, args
.url
))
174 print(' - ' + git('show', '--oneline', '--quiet', sha
))
177 if not ask_confirm('Are you sure you want to push %d commits?' % len(revs
)):
182 msg
= git('log', '--format=%B', '-n1', sha
)
183 if 'Differential Revision' not in msg
:
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]))
190 eprint('Try running: llvm/utils/git/arcfilter.sh')
191 die('Aborting (force push by adding "--no-verify")')
196 if __name__
== '__main__':
197 if not shutil
.which('git'):
198 die('error: cannot find git command')
201 p
= argparse
.ArgumentParser(
202 prog
='pre-push', formatter_class
=argparse
.RawDescriptionHelpFormatter
,
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')
210 p
.add_argument('remote', type=str, help='Name of the remote')
211 p
.add_argument('url', type=str, help='URL for the remote')
213 args
= p
.parse_args(argv
)
214 VERBOSE
= args
.verbose
217 lines
= sys
.stdin
.readlines()
218 sys
.stdin
= open('/dev/tty', 'r')
220 local_ref
, local_sha
, remote_ref
, remote_sha
= line
.split()
221 handle_push(args
, local_ref
, local_sha
, remote_ref
, remote_sha
)