blob: 82bf7e3687a566c4b0b8835a7e3826819d11ad3a [file] [log] [blame]
Remy Bohmer16c13282020-09-10 10:38:04 +02001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Remy Bohmer16c13282020-09-10 10:38:04 +020015import os
16import re
17import sys
18import traceback
Mike Frysingeracf63b22019-06-13 02:24:21 -040019import urllib.parse
Remy Bohmer16c13282020-09-10 10:38:04 +020020
21from error import HookError
22from git_refs import HEAD
23
Remy Bohmer7f7acfe2020-08-01 18:36:44 +020024
Mike Frysingerd4aee652023-10-19 05:13:32 -040025class RepoHook:
Gavin Makea2e3302023-03-11 06:46:20 +000026 """A RepoHook contains information about a script to run as a hook.
Remy Bohmer16c13282020-09-10 10:38:04 +020027
Gavin Makea2e3302023-03-11 06:46:20 +000028 Hooks are used to run a python script before running an upload (for
29 instance, to run presubmit checks). Eventually, we may have hooks for other
30 actions.
Remy Bohmer16c13282020-09-10 10:38:04 +020031
Gavin Makea2e3302023-03-11 06:46:20 +000032 This shouldn't be confused with files in the 'repo/hooks' directory. Those
33 files are copied into each '.git/hooks' folder for each project. Repo-level
34 hooks are associated instead with repo actions.
Remy Bohmer16c13282020-09-10 10:38:04 +020035
Gavin Makea2e3302023-03-11 06:46:20 +000036 Hooks are always python. When a hook is run, we will load the hook into the
37 interpreter and execute its main() function.
Remy Bohmer7f7acfe2020-08-01 18:36:44 +020038
Gavin Makea2e3302023-03-11 06:46:20 +000039 Combinations of hook option flags:
40 - no-verify=False, verify=False (DEFAULT):
41 If stdout is a tty, can prompt about running hooks if needed.
42 If user denies running hooks, the action is cancelled. If stdout is
43 not a tty and we would need to prompt about hooks, action is
44 cancelled.
45 - no-verify=False, verify=True:
46 Always run hooks with no prompt.
47 - no-verify=True, verify=False:
48 Never run hooks, but run action anyway (AKA bypass hooks).
49 - no-verify=True, verify=True:
50 Invalid
Remy Bohmer16c13282020-09-10 10:38:04 +020051 """
Remy Bohmer16c13282020-09-10 10:38:04 +020052
Gavin Makea2e3302023-03-11 06:46:20 +000053 def __init__(
54 self,
55 hook_type,
56 hooks_project,
57 repo_topdir,
58 manifest_url,
59 bypass_hooks=False,
60 allow_all_hooks=False,
61 ignore_hooks=False,
62 abort_if_user_denies=False,
63 ):
64 """RepoHook constructor.
Remy Bohmer16c13282020-09-10 10:38:04 +020065
Gavin Makea2e3302023-03-11 06:46:20 +000066 Params:
67 hook_type: A string representing the type of hook. This is also used
68 to figure out the name of the file containing the hook. For
69 example: 'pre-upload'.
70 hooks_project: The project containing the repo hooks.
71 If you have a manifest, this is manifest.repo_hooks_project.
72 OK if this is None, which will make the hook a no-op.
73 repo_topdir: The top directory of the repo client checkout.
74 This is the one containing the .repo directory. Scripts will
75 run with CWD as this directory.
76 If you have a manifest, this is manifest.topdir.
77 manifest_url: The URL to the manifest git repo.
78 bypass_hooks: If True, then 'Do not run the hook'.
79 allow_all_hooks: If True, then 'Run the hook without prompting'.
80 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
81 abort_if_user_denies: If True, we'll abort running the hook if the
82 user doesn't allow us to run the hook.
83 """
84 self._hook_type = hook_type
85 self._hooks_project = hooks_project
86 self._repo_topdir = repo_topdir
87 self._manifest_url = manifest_url
88 self._bypass_hooks = bypass_hooks
89 self._allow_all_hooks = allow_all_hooks
90 self._ignore_hooks = ignore_hooks
91 self._abort_if_user_denies = abort_if_user_denies
Remy Bohmer16c13282020-09-10 10:38:04 +020092
Gavin Makea2e3302023-03-11 06:46:20 +000093 # Store the full path to the script for convenience.
94 if self._hooks_project:
95 self._script_fullpath = os.path.join(
96 self._hooks_project.worktree, self._hook_type + ".py"
97 )
98 else:
99 self._script_fullpath = None
Remy Bohmer16c13282020-09-10 10:38:04 +0200100
Gavin Makea2e3302023-03-11 06:46:20 +0000101 def _GetHash(self):
102 """Return a hash of the contents of the hooks directory.
Remy Bohmer16c13282020-09-10 10:38:04 +0200103
Gavin Makea2e3302023-03-11 06:46:20 +0000104 We'll just use git to do this. This hash has the property that if
105 anything changes in the directory we will return a different has.
Remy Bohmer16c13282020-09-10 10:38:04 +0200106
Gavin Makea2e3302023-03-11 06:46:20 +0000107 SECURITY CONSIDERATION:
108 This hash only represents the contents of files in the hook
109 directory, not any other files imported or called by hooks. Changes
110 to imported files can change the script behavior without affecting
111 the hash.
Remy Bohmer16c13282020-09-10 10:38:04 +0200112
Gavin Makea2e3302023-03-11 06:46:20 +0000113 Returns:
114 A string representing the hash. This will always be ASCII so that
115 it can be printed to the user easily.
116 """
117 assert self._hooks_project, "Must have hooks to calculate their hash."
Remy Bohmer16c13282020-09-10 10:38:04 +0200118
Gavin Makea2e3302023-03-11 06:46:20 +0000119 # We will use the work_git object rather than just calling
120 # GetRevisionId(). That gives us a hash of the latest checked in version
121 # of the files that the user will actually be executing. Specifically,
122 # GetRevisionId() doesn't appear to change even if a user checks out a
123 # different version of the hooks repo (via git checkout) nor if a user
124 # commits their own revs.
125 #
126 # NOTE: Local (non-committed) changes will not be factored into this
127 # hash. I think this is OK, since we're really only worried about
128 # warning the user about upstream changes.
129 return self._hooks_project.work_git.rev_parse(HEAD)
Remy Bohmer16c13282020-09-10 10:38:04 +0200130
Gavin Makea2e3302023-03-11 06:46:20 +0000131 def _GetMustVerb(self):
132 """Return 'must' if the hook is required; 'should' if not."""
133 if self._abort_if_user_denies:
134 return "must"
135 else:
136 return "should"
Remy Bohmer16c13282020-09-10 10:38:04 +0200137
Gavin Makea2e3302023-03-11 06:46:20 +0000138 def _CheckForHookApproval(self):
139 """Check to see whether this hook has been approved.
Remy Bohmer16c13282020-09-10 10:38:04 +0200140
Gavin Makea2e3302023-03-11 06:46:20 +0000141 We'll accept approval of manifest URLs if they're using secure
142 transports. This way the user can say they trust the manifest hoster.
143 For insecure hosts, we fall back to checking the hash of the hooks repo.
Remy Bohmer16c13282020-09-10 10:38:04 +0200144
Gavin Makea2e3302023-03-11 06:46:20 +0000145 Note that we ask permission for each individual hook even though we use
146 the hash of all hooks when detecting changes. We'd like the user to be
147 able to approve / deny each hook individually. We only use the hash of
148 all hooks because there is no other easy way to detect changes to local
149 imports.
Remy Bohmer16c13282020-09-10 10:38:04 +0200150
Gavin Makea2e3302023-03-11 06:46:20 +0000151 Returns:
152 True if this hook is approved to run; False otherwise.
Remy Bohmer16c13282020-09-10 10:38:04 +0200153
Gavin Makea2e3302023-03-11 06:46:20 +0000154 Raises:
155 HookError: Raised if the user doesn't approve and
156 abort_if_user_denies was passed to the consturctor.
157 """
158 if self._ManifestUrlHasSecureScheme():
159 return self._CheckForHookApprovalManifest()
160 else:
161 return self._CheckForHookApprovalHash()
Remy Bohmer16c13282020-09-10 10:38:04 +0200162
Gavin Makea2e3302023-03-11 06:46:20 +0000163 def _CheckForHookApprovalHelper(
164 self, subkey, new_val, main_prompt, changed_prompt
165 ):
166 """Check for approval for a particular attribute and hook.
Remy Bohmer16c13282020-09-10 10:38:04 +0200167
Gavin Makea2e3302023-03-11 06:46:20 +0000168 Args:
169 subkey: The git config key under [repo.hooks.<hook_type>] to store
170 the last approved string.
171 new_val: The new value to compare against the last approved one.
172 main_prompt: Message to display to the user to ask for approval.
173 changed_prompt: Message explaining why we're re-asking for approval.
Remy Bohmer16c13282020-09-10 10:38:04 +0200174
Gavin Makea2e3302023-03-11 06:46:20 +0000175 Returns:
176 True if this hook is approved to run; False otherwise.
Remy Bohmer16c13282020-09-10 10:38:04 +0200177
Gavin Makea2e3302023-03-11 06:46:20 +0000178 Raises:
179 HookError: Raised if the user doesn't approve and
180 abort_if_user_denies was passed to the consturctor.
181 """
182 hooks_config = self._hooks_project.config
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400183 git_approval_key = f"repo.hooks.{self._hook_type}.{subkey}"
Remy Bohmer16c13282020-09-10 10:38:04 +0200184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 # Get the last value that the user approved for this hook; may be None.
186 old_val = hooks_config.GetString(git_approval_key)
Remy Bohmer16c13282020-09-10 10:38:04 +0200187
Gavin Makea2e3302023-03-11 06:46:20 +0000188 if old_val is not None:
189 # User previously approved hook and asked not to be prompted again.
190 if new_val == old_val:
191 # Approval matched. We're done.
192 return True
193 else:
194 # Give the user a reason why we're prompting, since they last
195 # told us to "never ask again".
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400196 prompt = f"WARNING: {changed_prompt}\n\n"
Gavin Makea2e3302023-03-11 06:46:20 +0000197 else:
198 prompt = ""
Remy Bohmer16c13282020-09-10 10:38:04 +0200199
Gavin Makea2e3302023-03-11 06:46:20 +0000200 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
201 if sys.stdout.isatty():
202 prompt += main_prompt + " (yes/always/NO)? "
203 response = input(prompt).lower()
204 print()
Remy Bohmer16c13282020-09-10 10:38:04 +0200205
Gavin Makea2e3302023-03-11 06:46:20 +0000206 # User is doing a one-time approval.
207 if response in ("y", "yes"):
208 return True
209 elif response == "always":
210 hooks_config.SetString(git_approval_key, new_val)
211 return True
Remy Bohmer16c13282020-09-10 10:38:04 +0200212
Gavin Makea2e3302023-03-11 06:46:20 +0000213 # For anything else, we'll assume no approval.
214 if self._abort_if_user_denies:
215 raise HookError(
216 "You must allow the %s hook or use --no-verify."
217 % self._hook_type
218 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 return False
Remy Bohmer16c13282020-09-10 10:38:04 +0200221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 def _ManifestUrlHasSecureScheme(self):
223 """Check if the URI for the manifest is a secure transport."""
224 secure_schemes = (
225 "file",
226 "https",
227 "ssh",
228 "persistent-https",
229 "sso",
230 "rpc",
231 )
232 parse_results = urllib.parse.urlparse(self._manifest_url)
233 return parse_results.scheme in secure_schemes
Remy Bohmer16c13282020-09-10 10:38:04 +0200234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 def _CheckForHookApprovalManifest(self):
236 """Check whether the user has approved this manifest host.
Remy Bohmer16c13282020-09-10 10:38:04 +0200237
Gavin Makea2e3302023-03-11 06:46:20 +0000238 Returns:
239 True if this hook is approved to run; False otherwise.
240 """
241 return self._CheckForHookApprovalHelper(
242 "approvedmanifest",
243 self._manifest_url,
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400244 f"Run hook scripts from {self._manifest_url}",
245 f"Manifest URL has changed since {self._hook_type} was allowed.",
Gavin Makea2e3302023-03-11 06:46:20 +0000246 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200247
Gavin Makea2e3302023-03-11 06:46:20 +0000248 def _CheckForHookApprovalHash(self):
249 """Check whether the user has approved the hooks repo.
Remy Bohmer16c13282020-09-10 10:38:04 +0200250
Gavin Makea2e3302023-03-11 06:46:20 +0000251 Returns:
252 True if this hook is approved to run; False otherwise.
253 """
254 prompt = (
255 "Repo %s run the script:\n"
256 " %s\n"
257 "\n"
258 "Do you want to allow this script to run"
259 )
260 return self._CheckForHookApprovalHelper(
261 "approvedhash",
262 self._GetHash(),
263 prompt % (self._GetMustVerb(), self._script_fullpath),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400264 f"Scripts have changed since {self._hook_type} was allowed.",
Gavin Makea2e3302023-03-11 06:46:20 +0000265 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200266
Gavin Makea2e3302023-03-11 06:46:20 +0000267 @staticmethod
268 def _ExtractInterpFromShebang(data):
269 """Extract the interpreter used in the shebang.
Remy Bohmer16c13282020-09-10 10:38:04 +0200270
Gavin Makea2e3302023-03-11 06:46:20 +0000271 Try to locate the interpreter the script is using (ignoring `env`).
Remy Bohmer16c13282020-09-10 10:38:04 +0200272
Gavin Makea2e3302023-03-11 06:46:20 +0000273 Args:
274 data: The file content of the script.
Remy Bohmer16c13282020-09-10 10:38:04 +0200275
Gavin Makea2e3302023-03-11 06:46:20 +0000276 Returns:
277 The basename of the main script interpreter, or None if a shebang is
278 not used or could not be parsed out.
279 """
280 firstline = data.splitlines()[:1]
281 if not firstline:
282 return None
Remy Bohmer16c13282020-09-10 10:38:04 +0200283
Gavin Makea2e3302023-03-11 06:46:20 +0000284 # The format here can be tricky.
285 shebang = firstline[0].strip()
286 m = re.match(r"^#!\s*([^\s]+)(?:\s+([^\s]+))?", shebang)
287 if not m:
288 return None
Remy Bohmer16c13282020-09-10 10:38:04 +0200289
Gavin Makea2e3302023-03-11 06:46:20 +0000290 # If the using `env`, find the target program.
291 interp = m.group(1)
292 if os.path.basename(interp) == "env":
293 interp = m.group(2)
Remy Bohmer16c13282020-09-10 10:38:04 +0200294
Gavin Makea2e3302023-03-11 06:46:20 +0000295 return interp
Remy Bohmer16c13282020-09-10 10:38:04 +0200296
Gavin Makea2e3302023-03-11 06:46:20 +0000297 def _ExecuteHookViaImport(self, data, context, **kwargs):
298 """Execute the hook code in |data| directly.
Remy Bohmer16c13282020-09-10 10:38:04 +0200299
Gavin Makea2e3302023-03-11 06:46:20 +0000300 Args:
301 data: The code of the hook to execute.
302 context: Basic Python context to execute the hook inside.
303 kwargs: Arbitrary arguments to pass to the hook script.
Remy Bohmer16c13282020-09-10 10:38:04 +0200304
Gavin Makea2e3302023-03-11 06:46:20 +0000305 Raises:
306 HookError: When the hooks failed for any reason.
307 """
308 # Exec, storing global context in the context dict. We catch exceptions
309 # and convert to a HookError w/ just the failing traceback.
Remy Bohmer16c13282020-09-10 10:38:04 +0200310 try:
Gavin Makea2e3302023-03-11 06:46:20 +0000311 exec(compile(data, self._script_fullpath, "exec"), context)
312 except Exception:
313 raise HookError(
314 "%s\nFailed to import %s hook; see traceback above."
315 % (traceback.format_exc(), self._hook_type)
316 )
317
318 # Running the script should have defined a main() function.
319 if "main" not in context:
320 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
321
322 # Call the main function in the hook. If the hook should cause the
323 # build to fail, it will raise an Exception. We'll catch that convert
324 # to a HookError w/ just the failing traceback.
325 try:
326 context["main"](**kwargs)
327 except Exception:
328 raise HookError(
329 "%s\nFailed to run main() for %s hook; see traceback "
330 "above." % (traceback.format_exc(), self._hook_type)
331 )
332
333 def _ExecuteHook(self, **kwargs):
334 """Actually execute the given hook.
335
336 This will run the hook's 'main' function in our python interpreter.
337
338 Args:
339 kwargs: Keyword arguments to pass to the hook. These are often
340 specific to the hook type. For instance, pre-upload hooks will
341 contain a project_list.
342 """
343 # Keep sys.path and CWD stashed away so that we can always restore them
344 # upon function exit.
345 orig_path = os.getcwd()
346 orig_syspath = sys.path
347
348 try:
349 # Always run hooks with CWD as topdir.
350 os.chdir(self._repo_topdir)
351
352 # Put the hook dir as the first item of sys.path so hooks can do
353 # relative imports. We want to replace the repo dir as [0] so
354 # hooks can't import repo files.
355 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
356
357 # Initial global context for the hook to run within.
358 context = {"__file__": self._script_fullpath}
359
360 # Add 'hook_should_take_kwargs' to the arguments to be passed to
361 # main. We don't actually want hooks to define their main with this
362 # argument--it's there to remind them that their hook should always
363 # take **kwargs.
364 # For instance, a pre-upload hook should be defined like:
365 # def main(project_list, **kwargs):
366 #
367 # This allows us to later expand the API without breaking old hooks.
368 kwargs = kwargs.copy()
369 kwargs["hook_should_take_kwargs"] = True
370
371 # See what version of python the hook has been written against.
372 data = open(self._script_fullpath).read()
373 interp = self._ExtractInterpFromShebang(data)
Gavin Makea2e3302023-03-11 06:46:20 +0000374 if interp:
375 prog = os.path.basename(interp)
Mike Frysinger3b8f9532023-10-14 01:25:50 +0545376 if prog.startswith("python2"):
377 raise HookError("Python 2 is not supported")
Remy Bohmer16c13282020-09-10 10:38:04 +0200378
Gavin Makea2e3302023-03-11 06:46:20 +0000379 # Run the hook by importing directly.
Mike Frysinger3b8f9532023-10-14 01:25:50 +0545380 self._ExecuteHookViaImport(data, context, **kwargs)
Gavin Makea2e3302023-03-11 06:46:20 +0000381 finally:
382 # Restore sys.path and CWD.
383 sys.path = orig_syspath
384 os.chdir(orig_path)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 def _CheckHook(self):
387 # Bail with a nice error if we can't find the hook.
388 if not os.path.isfile(self._script_fullpath):
389 raise HookError(
390 "Couldn't find repo hook: %s" % self._script_fullpath
391 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200392
Gavin Makea2e3302023-03-11 06:46:20 +0000393 def Run(self, **kwargs):
394 """Run the hook.
Remy Bohmer16c13282020-09-10 10:38:04 +0200395
Gavin Makea2e3302023-03-11 06:46:20 +0000396 If the hook doesn't exist (because there is no hooks project or because
397 this particular hook is not enabled), this is a no-op.
Remy Bohmer16c13282020-09-10 10:38:04 +0200398
Gavin Makea2e3302023-03-11 06:46:20 +0000399 Args:
400 user_allows_all_hooks: If True, we will never prompt about running
401 the hook--we'll just assume it's OK to run it.
402 kwargs: Keyword arguments to pass to the hook. These are often
403 specific to the hook type. For instance, pre-upload hooks will
404 contain a project_list.
Remy Bohmer16c13282020-09-10 10:38:04 +0200405
Gavin Makea2e3302023-03-11 06:46:20 +0000406 Returns:
407 True: On success or ignore hooks by user-request
408 False: The hook failed. The caller should respond with aborting the
409 action. Some examples in which False is returned:
410 * Finding the hook failed while it was enabled, or
411 * the user declined to run a required hook (from
412 _CheckForHookApproval)
413 In all these cases the user did not pass the proper arguments to
414 ignore the result through the option combinations as listed in
415 AddHookOptionGroup().
416 """
417 # Do not do anything in case bypass_hooks is set, or
418 # no-op if there is no hooks project or if hook is disabled.
419 if (
420 self._bypass_hooks
421 or not self._hooks_project
422 or self._hook_type not in self._hooks_project.enabled_repo_hooks
423 ):
424 return True
Remy Bohmer16c13282020-09-10 10:38:04 +0200425
Gavin Makea2e3302023-03-11 06:46:20 +0000426 passed = True
427 try:
428 self._CheckHook()
Remy Bohmer16c13282020-09-10 10:38:04 +0200429
Gavin Makea2e3302023-03-11 06:46:20 +0000430 # Make sure the user is OK with running the hook.
431 if self._allow_all_hooks or self._CheckForHookApproval():
432 # Run the hook with the same version of python we're using.
433 self._ExecuteHook(**kwargs)
434 except SystemExit as e:
435 passed = False
436 print(
437 "ERROR: %s hooks exited with exit code: %s"
438 % (self._hook_type, str(e)),
439 file=sys.stderr,
440 )
441 except HookError as e:
442 passed = False
443 print("ERROR: %s" % str(e), file=sys.stderr)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 if not passed and self._ignore_hooks:
446 print(
447 "\nWARNING: %s hooks failed, but continuing anyways."
448 % self._hook_type,
449 file=sys.stderr,
450 )
451 passed = True
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200452
Gavin Makea2e3302023-03-11 06:46:20 +0000453 return passed
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 @classmethod
456 def FromSubcmd(cls, manifest, opt, *args, **kwargs):
457 """Method to construct the repo hook class
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200458
Gavin Makea2e3302023-03-11 06:46:20 +0000459 Args:
460 manifest: The current active manifest for this command from which we
461 extract a couple of fields.
462 opt: Contains the commandline options for the action of this hook.
463 It should contain the options added by AddHookOptionGroup() in
464 which we are interested in RepoHook execution.
465 """
466 for key in ("bypass_hooks", "allow_all_hooks", "ignore_hooks"):
467 kwargs.setdefault(key, getattr(opt, key))
468 kwargs.update(
469 {
470 "hooks_project": manifest.repo_hooks_project,
471 "repo_topdir": manifest.topdir,
472 "manifest_url": manifest.manifestProject.GetRemote(
473 "origin"
474 ).url,
475 }
476 )
477 return cls(*args, **kwargs)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200478
Gavin Makea2e3302023-03-11 06:46:20 +0000479 @staticmethod
480 def AddOptionGroup(parser, name):
481 """Help options relating to the various hooks."""
482
483 # Note that verify and no-verify are NOT opposites of each other, which
484 # is why they store to different locations. We are using them to match
485 # 'git commit' syntax.
486 group = parser.add_option_group(name + " hooks")
487 group.add_option(
488 "--no-verify",
489 dest="bypass_hooks",
490 action="store_true",
491 help="Do not run the %s hook." % name,
492 )
493 group.add_option(
494 "--verify",
495 dest="allow_all_hooks",
496 action="store_true",
497 help="Run the %s hook without prompting." % name,
498 )
499 group.add_option(
500 "--ignore-hooks",
501 action="store_true",
502 help="Do not abort if %s hooks fail." % name,
503 )