blob: d6e1e3bbcd078de85c34f51c650240472e0930a7 [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 Frysinger044e52e2025-06-05 16:53:40 -040025# The API we've documented to hook authors. Keep in sync with repo-hooks.md.
26_API_ARGS = {
27 "pre-upload": {"project_list", "worktree_list"},
Kenny Cheng82d500e2025-06-02 21:55:04 +080028 "post-sync": {"repo_topdir"},
Mike Frysinger044e52e2025-06-05 16:53:40 -040029}
30
31
Mike Frysingerd4aee652023-10-19 05:13:32 -040032class RepoHook:
Gavin Makea2e3302023-03-11 06:46:20 +000033 """A RepoHook contains information about a script to run as a hook.
Remy Bohmer16c13282020-09-10 10:38:04 +020034
Gavin Makea2e3302023-03-11 06:46:20 +000035 Hooks are used to run a python script before running an upload (for
36 instance, to run presubmit checks). Eventually, we may have hooks for other
37 actions.
Remy Bohmer16c13282020-09-10 10:38:04 +020038
Gavin Makea2e3302023-03-11 06:46:20 +000039 This shouldn't be confused with files in the 'repo/hooks' directory. Those
40 files are copied into each '.git/hooks' folder for each project. Repo-level
41 hooks are associated instead with repo actions.
Remy Bohmer16c13282020-09-10 10:38:04 +020042
Gavin Makea2e3302023-03-11 06:46:20 +000043 Hooks are always python. When a hook is run, we will load the hook into the
44 interpreter and execute its main() function.
Remy Bohmer7f7acfe2020-08-01 18:36:44 +020045
Gavin Makea2e3302023-03-11 06:46:20 +000046 Combinations of hook option flags:
47 - no-verify=False, verify=False (DEFAULT):
48 If stdout is a tty, can prompt about running hooks if needed.
49 If user denies running hooks, the action is cancelled. If stdout is
50 not a tty and we would need to prompt about hooks, action is
51 cancelled.
52 - no-verify=False, verify=True:
53 Always run hooks with no prompt.
54 - no-verify=True, verify=False:
55 Never run hooks, but run action anyway (AKA bypass hooks).
56 - no-verify=True, verify=True:
57 Invalid
Remy Bohmer16c13282020-09-10 10:38:04 +020058 """
Remy Bohmer16c13282020-09-10 10:38:04 +020059
Gavin Makea2e3302023-03-11 06:46:20 +000060 def __init__(
61 self,
62 hook_type,
63 hooks_project,
64 repo_topdir,
65 manifest_url,
Mike Frysinger044e52e2025-06-05 16:53:40 -040066 bug_url=None,
Gavin Makea2e3302023-03-11 06:46:20 +000067 bypass_hooks=False,
68 allow_all_hooks=False,
69 ignore_hooks=False,
70 abort_if_user_denies=False,
71 ):
72 """RepoHook constructor.
Remy Bohmer16c13282020-09-10 10:38:04 +020073
Gavin Makea2e3302023-03-11 06:46:20 +000074 Params:
75 hook_type: A string representing the type of hook. This is also used
76 to figure out the name of the file containing the hook. For
77 example: 'pre-upload'.
78 hooks_project: The project containing the repo hooks.
79 If you have a manifest, this is manifest.repo_hooks_project.
80 OK if this is None, which will make the hook a no-op.
81 repo_topdir: The top directory of the repo client checkout.
82 This is the one containing the .repo directory. Scripts will
83 run with CWD as this directory.
84 If you have a manifest, this is manifest.topdir.
85 manifest_url: The URL to the manifest git repo.
Mike Frysinger044e52e2025-06-05 16:53:40 -040086 bug_url: The URL to report issues.
Gavin Makea2e3302023-03-11 06:46:20 +000087 bypass_hooks: If True, then 'Do not run the hook'.
88 allow_all_hooks: If True, then 'Run the hook without prompting'.
89 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
90 abort_if_user_denies: If True, we'll abort running the hook if the
91 user doesn't allow us to run the hook.
92 """
93 self._hook_type = hook_type
94 self._hooks_project = hooks_project
95 self._repo_topdir = repo_topdir
96 self._manifest_url = manifest_url
Mike Frysinger044e52e2025-06-05 16:53:40 -040097 self._bug_url = bug_url
Gavin Makea2e3302023-03-11 06:46:20 +000098 self._bypass_hooks = bypass_hooks
99 self._allow_all_hooks = allow_all_hooks
100 self._ignore_hooks = ignore_hooks
101 self._abort_if_user_denies = abort_if_user_denies
Remy Bohmer16c13282020-09-10 10:38:04 +0200102
Gavin Makea2e3302023-03-11 06:46:20 +0000103 # Store the full path to the script for convenience.
Gavin Mak239fad72025-07-25 23:20:06 +0000104 self._script_fullpath = None
105 if self._hooks_project and self._hooks_project.worktree:
Gavin Makea2e3302023-03-11 06:46:20 +0000106 self._script_fullpath = os.path.join(
107 self._hooks_project.worktree, self._hook_type + ".py"
108 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200109
Gavin Makea2e3302023-03-11 06:46:20 +0000110 def _GetHash(self):
111 """Return a hash of the contents of the hooks directory.
Remy Bohmer16c13282020-09-10 10:38:04 +0200112
Gavin Makea2e3302023-03-11 06:46:20 +0000113 We'll just use git to do this. This hash has the property that if
114 anything changes in the directory we will return a different has.
Remy Bohmer16c13282020-09-10 10:38:04 +0200115
Gavin Makea2e3302023-03-11 06:46:20 +0000116 SECURITY CONSIDERATION:
117 This hash only represents the contents of files in the hook
118 directory, not any other files imported or called by hooks. Changes
119 to imported files can change the script behavior without affecting
120 the hash.
Remy Bohmer16c13282020-09-10 10:38:04 +0200121
Gavin Makea2e3302023-03-11 06:46:20 +0000122 Returns:
123 A string representing the hash. This will always be ASCII so that
124 it can be printed to the user easily.
125 """
126 assert self._hooks_project, "Must have hooks to calculate their hash."
Remy Bohmer16c13282020-09-10 10:38:04 +0200127
Gavin Makea2e3302023-03-11 06:46:20 +0000128 # We will use the work_git object rather than just calling
129 # GetRevisionId(). That gives us a hash of the latest checked in version
130 # of the files that the user will actually be executing. Specifically,
131 # GetRevisionId() doesn't appear to change even if a user checks out a
132 # different version of the hooks repo (via git checkout) nor if a user
133 # commits their own revs.
134 #
135 # NOTE: Local (non-committed) changes will not be factored into this
136 # hash. I think this is OK, since we're really only worried about
137 # warning the user about upstream changes.
138 return self._hooks_project.work_git.rev_parse(HEAD)
Remy Bohmer16c13282020-09-10 10:38:04 +0200139
Gavin Makea2e3302023-03-11 06:46:20 +0000140 def _GetMustVerb(self):
141 """Return 'must' if the hook is required; 'should' if not."""
142 if self._abort_if_user_denies:
143 return "must"
144 else:
145 return "should"
Remy Bohmer16c13282020-09-10 10:38:04 +0200146
Gavin Makea2e3302023-03-11 06:46:20 +0000147 def _CheckForHookApproval(self):
148 """Check to see whether this hook has been approved.
Remy Bohmer16c13282020-09-10 10:38:04 +0200149
Gavin Makea2e3302023-03-11 06:46:20 +0000150 We'll accept approval of manifest URLs if they're using secure
151 transports. This way the user can say they trust the manifest hoster.
152 For insecure hosts, we fall back to checking the hash of the hooks repo.
Remy Bohmer16c13282020-09-10 10:38:04 +0200153
Gavin Makea2e3302023-03-11 06:46:20 +0000154 Note that we ask permission for each individual hook even though we use
155 the hash of all hooks when detecting changes. We'd like the user to be
156 able to approve / deny each hook individually. We only use the hash of
157 all hooks because there is no other easy way to detect changes to local
158 imports.
Remy Bohmer16c13282020-09-10 10:38:04 +0200159
Gavin Makea2e3302023-03-11 06:46:20 +0000160 Returns:
161 True if this hook is approved to run; False otherwise.
Remy Bohmer16c13282020-09-10 10:38:04 +0200162
Gavin Makea2e3302023-03-11 06:46:20 +0000163 Raises:
164 HookError: Raised if the user doesn't approve and
165 abort_if_user_denies was passed to the consturctor.
166 """
167 if self._ManifestUrlHasSecureScheme():
168 return self._CheckForHookApprovalManifest()
169 else:
170 return self._CheckForHookApprovalHash()
Remy Bohmer16c13282020-09-10 10:38:04 +0200171
Gavin Makea2e3302023-03-11 06:46:20 +0000172 def _CheckForHookApprovalHelper(
173 self, subkey, new_val, main_prompt, changed_prompt
174 ):
175 """Check for approval for a particular attribute and hook.
Remy Bohmer16c13282020-09-10 10:38:04 +0200176
Gavin Makea2e3302023-03-11 06:46:20 +0000177 Args:
178 subkey: The git config key under [repo.hooks.<hook_type>] to store
179 the last approved string.
180 new_val: The new value to compare against the last approved one.
181 main_prompt: Message to display to the user to ask for approval.
182 changed_prompt: Message explaining why we're re-asking for approval.
Remy Bohmer16c13282020-09-10 10:38:04 +0200183
Gavin Makea2e3302023-03-11 06:46:20 +0000184 Returns:
185 True if this hook is approved to run; False otherwise.
Remy Bohmer16c13282020-09-10 10:38:04 +0200186
Gavin Makea2e3302023-03-11 06:46:20 +0000187 Raises:
188 HookError: Raised if the user doesn't approve and
189 abort_if_user_denies was passed to the consturctor.
190 """
191 hooks_config = self._hooks_project.config
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400192 git_approval_key = f"repo.hooks.{self._hook_type}.{subkey}"
Remy Bohmer16c13282020-09-10 10:38:04 +0200193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 # Get the last value that the user approved for this hook; may be None.
195 old_val = hooks_config.GetString(git_approval_key)
Remy Bohmer16c13282020-09-10 10:38:04 +0200196
Gavin Makea2e3302023-03-11 06:46:20 +0000197 if old_val is not None:
198 # User previously approved hook and asked not to be prompted again.
199 if new_val == old_val:
200 # Approval matched. We're done.
201 return True
202 else:
203 # Give the user a reason why we're prompting, since they last
204 # told us to "never ask again".
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400205 prompt = f"WARNING: {changed_prompt}\n\n"
Gavin Makea2e3302023-03-11 06:46:20 +0000206 else:
207 prompt = ""
Remy Bohmer16c13282020-09-10 10:38:04 +0200208
Gavin Makea2e3302023-03-11 06:46:20 +0000209 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
210 if sys.stdout.isatty():
211 prompt += main_prompt + " (yes/always/NO)? "
212 response = input(prompt).lower()
213 print()
Remy Bohmer16c13282020-09-10 10:38:04 +0200214
Gavin Makea2e3302023-03-11 06:46:20 +0000215 # User is doing a one-time approval.
216 if response in ("y", "yes"):
217 return True
218 elif response == "always":
219 hooks_config.SetString(git_approval_key, new_val)
220 return True
Remy Bohmer16c13282020-09-10 10:38:04 +0200221
Gavin Makea2e3302023-03-11 06:46:20 +0000222 # For anything else, we'll assume no approval.
223 if self._abort_if_user_denies:
224 raise HookError(
225 "You must allow the %s hook or use --no-verify."
226 % self._hook_type
227 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200228
Gavin Makea2e3302023-03-11 06:46:20 +0000229 return False
Remy Bohmer16c13282020-09-10 10:38:04 +0200230
Gavin Makea2e3302023-03-11 06:46:20 +0000231 def _ManifestUrlHasSecureScheme(self):
232 """Check if the URI for the manifest is a secure transport."""
233 secure_schemes = (
234 "file",
235 "https",
236 "ssh",
237 "persistent-https",
238 "sso",
239 "rpc",
240 )
241 parse_results = urllib.parse.urlparse(self._manifest_url)
242 return parse_results.scheme in secure_schemes
Remy Bohmer16c13282020-09-10 10:38:04 +0200243
Gavin Makea2e3302023-03-11 06:46:20 +0000244 def _CheckForHookApprovalManifest(self):
245 """Check whether the user has approved this manifest host.
Remy Bohmer16c13282020-09-10 10:38:04 +0200246
Gavin Makea2e3302023-03-11 06:46:20 +0000247 Returns:
248 True if this hook is approved to run; False otherwise.
249 """
250 return self._CheckForHookApprovalHelper(
251 "approvedmanifest",
252 self._manifest_url,
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400253 f"Run hook scripts from {self._manifest_url}",
254 f"Manifest URL has changed since {self._hook_type} was allowed.",
Gavin Makea2e3302023-03-11 06:46:20 +0000255 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200256
Gavin Makea2e3302023-03-11 06:46:20 +0000257 def _CheckForHookApprovalHash(self):
258 """Check whether the user has approved the hooks repo.
Remy Bohmer16c13282020-09-10 10:38:04 +0200259
Gavin Makea2e3302023-03-11 06:46:20 +0000260 Returns:
261 True if this hook is approved to run; False otherwise.
262 """
263 prompt = (
264 "Repo %s run the script:\n"
265 " %s\n"
266 "\n"
267 "Do you want to allow this script to run"
268 )
269 return self._CheckForHookApprovalHelper(
270 "approvedhash",
271 self._GetHash(),
272 prompt % (self._GetMustVerb(), self._script_fullpath),
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400273 f"Scripts have changed since {self._hook_type} was allowed.",
Gavin Makea2e3302023-03-11 06:46:20 +0000274 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200275
Gavin Makea2e3302023-03-11 06:46:20 +0000276 @staticmethod
277 def _ExtractInterpFromShebang(data):
278 """Extract the interpreter used in the shebang.
Remy Bohmer16c13282020-09-10 10:38:04 +0200279
Gavin Makea2e3302023-03-11 06:46:20 +0000280 Try to locate the interpreter the script is using (ignoring `env`).
Remy Bohmer16c13282020-09-10 10:38:04 +0200281
Gavin Makea2e3302023-03-11 06:46:20 +0000282 Args:
283 data: The file content of the script.
Remy Bohmer16c13282020-09-10 10:38:04 +0200284
Gavin Makea2e3302023-03-11 06:46:20 +0000285 Returns:
286 The basename of the main script interpreter, or None if a shebang is
287 not used or could not be parsed out.
288 """
289 firstline = data.splitlines()[:1]
290 if not firstline:
291 return None
Remy Bohmer16c13282020-09-10 10:38:04 +0200292
Gavin Makea2e3302023-03-11 06:46:20 +0000293 # The format here can be tricky.
294 shebang = firstline[0].strip()
295 m = re.match(r"^#!\s*([^\s]+)(?:\s+([^\s]+))?", shebang)
296 if not m:
297 return None
Remy Bohmer16c13282020-09-10 10:38:04 +0200298
Gavin Makea2e3302023-03-11 06:46:20 +0000299 # If the using `env`, find the target program.
300 interp = m.group(1)
301 if os.path.basename(interp) == "env":
302 interp = m.group(2)
Remy Bohmer16c13282020-09-10 10:38:04 +0200303
Gavin Makea2e3302023-03-11 06:46:20 +0000304 return interp
Remy Bohmer16c13282020-09-10 10:38:04 +0200305
Gavin Makea2e3302023-03-11 06:46:20 +0000306 def _ExecuteHookViaImport(self, data, context, **kwargs):
307 """Execute the hook code in |data| directly.
Remy Bohmer16c13282020-09-10 10:38:04 +0200308
Gavin Makea2e3302023-03-11 06:46:20 +0000309 Args:
310 data: The code of the hook to execute.
311 context: Basic Python context to execute the hook inside.
312 kwargs: Arbitrary arguments to pass to the hook script.
Remy Bohmer16c13282020-09-10 10:38:04 +0200313
Gavin Makea2e3302023-03-11 06:46:20 +0000314 Raises:
315 HookError: When the hooks failed for any reason.
316 """
317 # Exec, storing global context in the context dict. We catch exceptions
318 # and convert to a HookError w/ just the failing traceback.
Remy Bohmer16c13282020-09-10 10:38:04 +0200319 try:
Gavin Makea2e3302023-03-11 06:46:20 +0000320 exec(compile(data, self._script_fullpath, "exec"), context)
321 except Exception:
322 raise HookError(
323 "%s\nFailed to import %s hook; see traceback above."
324 % (traceback.format_exc(), self._hook_type)
325 )
326
327 # Running the script should have defined a main() function.
328 if "main" not in context:
329 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
330
331 # Call the main function in the hook. If the hook should cause the
332 # build to fail, it will raise an Exception. We'll catch that convert
333 # to a HookError w/ just the failing traceback.
334 try:
335 context["main"](**kwargs)
336 except Exception:
337 raise HookError(
338 "%s\nFailed to run main() for %s hook; see traceback "
339 "above." % (traceback.format_exc(), self._hook_type)
340 )
341
342 def _ExecuteHook(self, **kwargs):
343 """Actually execute the given hook.
344
345 This will run the hook's 'main' function in our python interpreter.
346
347 Args:
348 kwargs: Keyword arguments to pass to the hook. These are often
349 specific to the hook type. For instance, pre-upload hooks will
350 contain a project_list.
351 """
352 # Keep sys.path and CWD stashed away so that we can always restore them
353 # upon function exit.
354 orig_path = os.getcwd()
355 orig_syspath = sys.path
356
357 try:
358 # Always run hooks with CWD as topdir.
359 os.chdir(self._repo_topdir)
360
361 # Put the hook dir as the first item of sys.path so hooks can do
362 # relative imports. We want to replace the repo dir as [0] so
363 # hooks can't import repo files.
364 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
365
366 # Initial global context for the hook to run within.
367 context = {"__file__": self._script_fullpath}
368
369 # Add 'hook_should_take_kwargs' to the arguments to be passed to
370 # main. We don't actually want hooks to define their main with this
371 # argument--it's there to remind them that their hook should always
372 # take **kwargs.
373 # For instance, a pre-upload hook should be defined like:
374 # def main(project_list, **kwargs):
375 #
376 # This allows us to later expand the API without breaking old hooks.
377 kwargs = kwargs.copy()
378 kwargs["hook_should_take_kwargs"] = True
379
380 # See what version of python the hook has been written against.
381 data = open(self._script_fullpath).read()
382 interp = self._ExtractInterpFromShebang(data)
Gavin Makea2e3302023-03-11 06:46:20 +0000383 if interp:
384 prog = os.path.basename(interp)
Mike Frysinger3b8f9532023-10-14 01:25:50 +0545385 if prog.startswith("python2"):
386 raise HookError("Python 2 is not supported")
Remy Bohmer16c13282020-09-10 10:38:04 +0200387
Gavin Makea2e3302023-03-11 06:46:20 +0000388 # Run the hook by importing directly.
Mike Frysinger3b8f9532023-10-14 01:25:50 +0545389 self._ExecuteHookViaImport(data, context, **kwargs)
Gavin Makea2e3302023-03-11 06:46:20 +0000390 finally:
391 # Restore sys.path and CWD.
392 sys.path = orig_syspath
393 os.chdir(orig_path)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200394
Gavin Makea2e3302023-03-11 06:46:20 +0000395 def _CheckHook(self):
396 # Bail with a nice error if we can't find the hook.
397 if not os.path.isfile(self._script_fullpath):
398 raise HookError(
399 "Couldn't find repo hook: %s" % self._script_fullpath
400 )
Remy Bohmer16c13282020-09-10 10:38:04 +0200401
Gavin Makea2e3302023-03-11 06:46:20 +0000402 def Run(self, **kwargs):
403 """Run the hook.
Remy Bohmer16c13282020-09-10 10:38:04 +0200404
Gavin Makea2e3302023-03-11 06:46:20 +0000405 If the hook doesn't exist (because there is no hooks project or because
406 this particular hook is not enabled), this is a no-op.
Remy Bohmer16c13282020-09-10 10:38:04 +0200407
Gavin Makea2e3302023-03-11 06:46:20 +0000408 Args:
409 user_allows_all_hooks: If True, we will never prompt about running
410 the hook--we'll just assume it's OK to run it.
411 kwargs: Keyword arguments to pass to the hook. These are often
412 specific to the hook type. For instance, pre-upload hooks will
413 contain a project_list.
Remy Bohmer16c13282020-09-10 10:38:04 +0200414
Gavin Makea2e3302023-03-11 06:46:20 +0000415 Returns:
416 True: On success or ignore hooks by user-request
417 False: The hook failed. The caller should respond with aborting the
418 action. Some examples in which False is returned:
419 * Finding the hook failed while it was enabled, or
420 * the user declined to run a required hook (from
421 _CheckForHookApproval)
422 In all these cases the user did not pass the proper arguments to
423 ignore the result through the option combinations as listed in
424 AddHookOptionGroup().
425 """
Mike Frysinger044e52e2025-06-05 16:53:40 -0400426 # Make sure our own callers use the documented API.
427 exp_kwargs = _API_ARGS.get(self._hook_type, set())
428 got_kwargs = set(kwargs.keys())
429 if exp_kwargs != got_kwargs:
430 print(
431 "repo internal error: "
432 f"hook '{self._hook_type}' called incorrectly\n"
433 f" got: {sorted(got_kwargs)}\n"
434 f" expected: {sorted(exp_kwargs)}\n"
435 f"Please file a bug: {self._bug_url}",
436 file=sys.stderr,
437 )
438 return False
439
Gavin Makea2e3302023-03-11 06:46:20 +0000440 # Do not do anything in case bypass_hooks is set, or
441 # no-op if there is no hooks project or if hook is disabled.
442 if (
443 self._bypass_hooks
444 or not self._hooks_project
Gavin Mak239fad72025-07-25 23:20:06 +0000445 or not self._script_fullpath
Gavin Makea2e3302023-03-11 06:46:20 +0000446 or self._hook_type not in self._hooks_project.enabled_repo_hooks
447 ):
448 return True
Remy Bohmer16c13282020-09-10 10:38:04 +0200449
Gavin Makea2e3302023-03-11 06:46:20 +0000450 passed = True
451 try:
452 self._CheckHook()
Remy Bohmer16c13282020-09-10 10:38:04 +0200453
Gavin Makea2e3302023-03-11 06:46:20 +0000454 # Make sure the user is OK with running the hook.
455 if self._allow_all_hooks or self._CheckForHookApproval():
456 # Run the hook with the same version of python we're using.
457 self._ExecuteHook(**kwargs)
458 except SystemExit as e:
459 passed = False
460 print(
461 "ERROR: %s hooks exited with exit code: %s"
462 % (self._hook_type, str(e)),
463 file=sys.stderr,
464 )
465 except HookError as e:
466 passed = False
467 print("ERROR: %s" % str(e), file=sys.stderr)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200468
Gavin Makea2e3302023-03-11 06:46:20 +0000469 if not passed and self._ignore_hooks:
470 print(
471 "\nWARNING: %s hooks failed, but continuing anyways."
472 % self._hook_type,
473 file=sys.stderr,
474 )
475 passed = True
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200476
Gavin Makea2e3302023-03-11 06:46:20 +0000477 return passed
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200478
Gavin Makea2e3302023-03-11 06:46:20 +0000479 @classmethod
480 def FromSubcmd(cls, manifest, opt, *args, **kwargs):
481 """Method to construct the repo hook class
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200482
Gavin Makea2e3302023-03-11 06:46:20 +0000483 Args:
484 manifest: The current active manifest for this command from which we
485 extract a couple of fields.
486 opt: Contains the commandline options for the action of this hook.
487 It should contain the options added by AddHookOptionGroup() in
488 which we are interested in RepoHook execution.
489 """
490 for key in ("bypass_hooks", "allow_all_hooks", "ignore_hooks"):
491 kwargs.setdefault(key, getattr(opt, key))
492 kwargs.update(
493 {
494 "hooks_project": manifest.repo_hooks_project,
495 "repo_topdir": manifest.topdir,
496 "manifest_url": manifest.manifestProject.GetRemote(
497 "origin"
498 ).url,
Mike Frysinger044e52e2025-06-05 16:53:40 -0400499 "bug_url": manifest.contactinfo.bugurl,
Gavin Makea2e3302023-03-11 06:46:20 +0000500 }
501 )
502 return cls(*args, **kwargs)
Remy Bohmer7f7acfe2020-08-01 18:36:44 +0200503
Gavin Makea2e3302023-03-11 06:46:20 +0000504 @staticmethod
505 def AddOptionGroup(parser, name):
506 """Help options relating to the various hooks."""
507
508 # Note that verify and no-verify are NOT opposites of each other, which
509 # is why they store to different locations. We are using them to match
510 # 'git commit' syntax.
511 group = parser.add_option_group(name + " hooks")
512 group.add_option(
513 "--no-verify",
514 dest="bypass_hooks",
515 action="store_true",
516 help="Do not run the %s hook." % name,
517 )
518 group.add_option(
519 "--verify",
520 dest="allow_all_hooks",
521 action="store_true",
522 help="Run the %s hook without prompting." % name,
523 )
524 group.add_option(
525 "--ignore-hooks",
526 action="store_true",
527 help="Do not abort if %s hooks fail." % name,
528 )