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).
35 from shlex
import quote
40 z40
= '0000000000000000000000000000000000000000'
43 def eprint(*args
, **kwargs
):
44 print(*args
, file=sys
.stderr
, **kwargs
)
47 def log(*args
, **kwargs
):
50 print(*args
, **kwargs
)
53 def log_verbose(*args
, **kwargs
):
56 print(*args
, **kwargs
)
64 def ask_confirm(prompt
):
66 query
= input('%s (y/N): ' % (prompt
))
67 if query
.lower() not in ['y', 'n', '']:
68 print('Expect y or n!')
70 return query
.lower() == 'y'
74 """Lazily create a /dev/null fd for use in shell()"""
76 if dev_null_fd
is None:
77 dev_null_fd
= open(os
.devnull
, 'w')
81 def 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
]
87 cwd_msg
= ' in %s' % cwd
88 log_verbose('Running%s: %s' % (cwd_msg
, ' '.join(quoted_cmd
)))
90 err_pipe
= subprocess
.PIPE
92 # Silence errors if requested.
93 err_pipe
= get_dev_null()
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
102 log_verbose('Command took %0.1fs' % elapsed
)
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())
111 stdout
= stdout
.rstrip('\r\n')
113 stdout
= stdout
.rstrip(b
'\r\n')
115 for l
in stdout
.splitlines():
116 log_verbose('STDOUT: %s' % l
)
118 err_msg
= '`%s` returned %s' % (' '.join(quoted_cmd
), p
.returncode
)
121 eprint(stderr
.rstrip())
124 raise RuntimeError(err_msg
)
127 def git(*cmd
, **kwargs
):
128 return shell(['git'] + list(cmd
), **kwargs
)
131 def get_revs_to_push(range):
132 commits
= git('rev-list', range).splitlines()
133 # Reverse the order so we print the oldest commit first
138 def 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
,
144 # Handle request to delete
146 if not ask_confirm('Are you sure you want to delete "%s" on remote "%s"?' % (remote_ref
, args
.url
)):
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
)):
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):
163 revs
= get_revs_to_push(range)
165 # This can happen if someone is force pushing an older revision to a branch
168 # Print the revision about to be pushed commits
169 print('Pushing to "%s" on remote "%s"' % (remote_ref
, args
.url
))
171 print(' - ' + git('show', '--oneline', '--quiet', sha
))
174 if not ask_confirm('Are you sure you want to push %d commits?' % len(revs
)):
179 msg
= git('log', '--format=%B', '-n1', sha
)
180 if 'Differential Revision' not in msg
:
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]))
187 eprint('Try running: llvm/utils/git/arcfilter.sh')
188 die('Aborting (force push by adding "--no-verify")')
193 if __name__
== '__main__':
194 if not shutil
.which('git'):
195 die('error: cannot find git command')
198 p
= argparse
.ArgumentParser(
199 prog
='pre-push', formatter_class
=argparse
.RawDescriptionHelpFormatter
,
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')
207 p
.add_argument('remote', type=str, help='Name of the remote')
208 p
.add_argument('url', type=str, help='URL for the remote')
210 args
= p
.parse_args(argv
)
211 VERBOSE
= args
.verbose
214 lines
= sys
.stdin
.readlines()
215 sys
.stdin
= open('/dev/tty', 'r')
217 local_ref
, local_sha
, remote_ref
, remote_sha
= line
.split()
218 handle_push(args
, local_ref
, local_sha
, remote_ref
, remote_sha
)