summaryrefslogtreecommitdiffstats
path: root/hooks.py
diff options
context:
space:
mode:
authorRemy Bohmer <github@bohmer.net>2020-08-01 18:36:44 +0200
committerMike Frysinger <vapier@google.com>2020-11-23 09:59:16 +0000
commit7f7acfe9fd93cfd4a697f2bc851d1b8182f6336e (patch)
tree646e53f6ccbedd55105017797b6f5b883909a3dc /hooks.py
parent169b0218b384f04426d7509757a8684f957967bf (diff)
downloadgit-repo-7f7acfe9fd93cfd4a697f2bc851d1b8182f6336e.tar.gz
Concentrate the RepoHook knowledge in the RepoHook class
The knowledge about running hooks and all its exception handling is scattered over multiple files. This makes the code harder to read, but also it requires duplication of logic in case other RepoHooks are added to different commands. This refactoring also creates uniform behavior of the hooks across multiple commands and it guarantees the re-use of the same arguments on all of them. Signed-off-by: Remy Bohmer <github@bohmer.net> Change-Id: Ia4d90eab429e4af00943306e89faec8db35ba29d Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/277562 Tested-by: Remy Bohmer <oss@bohmer.net> Reviewed-by: Mike Frysinger <vapier@google.com>
Diffstat (limited to 'hooks.py')
-rw-r--r--hooks.py145
1 files changed, 117 insertions, 28 deletions
diff --git a/hooks.py b/hooks.py
index 177bc88b..1abba0c4 100644
--- a/hooks.py
+++ b/hooks.py
@@ -14,9 +14,11 @@
14# See the License for the specific language governing permissions and 14# See the License for the specific language governing permissions and
15# limitations under the License. 15# limitations under the License.
16 16
17import errno
17import json 18import json
18import os 19import os
19import re 20import re
21import subprocess
20import sys 22import sys
21import traceback 23import traceback
22 24
@@ -33,6 +35,7 @@ else:
33 urllib.parse = urlparse 35 urllib.parse = urlparse
34 input = raw_input # noqa: F821 36 input = raw_input # noqa: F821
35 37
38
36class RepoHook(object): 39class RepoHook(object):
37 """A RepoHook contains information about a script to run as a hook. 40 """A RepoHook contains information about a script to run as a hook.
38 41
@@ -45,13 +48,29 @@ class RepoHook(object):
45 48
46 Hooks are always python. When a hook is run, we will load the hook into the 49 Hooks are always python. When a hook is run, we will load the hook into the
47 interpreter and execute its main() function. 50 interpreter and execute its main() function.
51
52 Combinations of hook option flags:
53 - no-verify=False, verify=False (DEFAULT):
54 If stdout is a tty, can prompt about running hooks if needed.
55 If user denies running hooks, the action is cancelled. If stdout is
56 not a tty and we would need to prompt about hooks, action is
57 cancelled.
58 - no-verify=False, verify=True:
59 Always run hooks with no prompt.
60 - no-verify=True, verify=False:
61 Never run hooks, but run action anyway (AKA bypass hooks).
62 - no-verify=True, verify=True:
63 Invalid
48 """ 64 """
49 65
50 def __init__(self, 66 def __init__(self,
51 hook_type, 67 hook_type,
52 hooks_project, 68 hooks_project,
53 topdir, 69 repo_topdir,
54 manifest_url, 70 manifest_url,
71 bypass_hooks=False,
72 allow_all_hooks=False,
73 ignore_hooks=False,
55 abort_if_user_denies=False): 74 abort_if_user_denies=False):
56 """RepoHook constructor. 75 """RepoHook constructor.
57 76
@@ -59,20 +78,27 @@ class RepoHook(object):
59 hook_type: A string representing the type of hook. This is also used 78 hook_type: A string representing the type of hook. This is also used
60 to figure out the name of the file containing the hook. For 79 to figure out the name of the file containing the hook. For
61 example: 'pre-upload'. 80 example: 'pre-upload'.
62 hooks_project: The project containing the repo hooks. If you have a 81 hooks_project: The project containing the repo hooks.
63 manifest, this is manifest.repo_hooks_project. OK if this is None, 82 If you have a manifest, this is manifest.repo_hooks_project.
64 which will make the hook a no-op. 83 OK if this is None, which will make the hook a no-op.
65 topdir: Repo's top directory (the one containing the .repo directory). 84 repo_topdir: The top directory of the repo client checkout.
66 Scripts will run with CWD as this directory. If you have a manifest, 85 This is the one containing the .repo directory. Scripts will
67 this is manifest.topdir 86 run with CWD as this directory.
87 If you have a manifest, this is manifest.topdir.
68 manifest_url: The URL to the manifest git repo. 88 manifest_url: The URL to the manifest git repo.
69 abort_if_user_denies: If True, we'll throw a HookError() if the user 89 bypass_hooks: If True, then 'Do not run the hook'.
90 allow_all_hooks: If True, then 'Run the hook without prompting'.
91 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
92 abort_if_user_denies: If True, we'll abort running the hook if the user
70 doesn't allow us to run the hook. 93 doesn't allow us to run the hook.
71 """ 94 """
72 self._hook_type = hook_type 95 self._hook_type = hook_type
73 self._hooks_project = hooks_project 96 self._hooks_project = hooks_project
97 self._repo_topdir = repo_topdir
74 self._manifest_url = manifest_url 98 self._manifest_url = manifest_url
75 self._topdir = topdir 99 self._bypass_hooks = bypass_hooks
100 self._allow_all_hooks = allow_all_hooks
101 self._ignore_hooks = ignore_hooks
76 self._abort_if_user_denies = abort_if_user_denies 102 self._abort_if_user_denies = abort_if_user_denies
77 103
78 # Store the full path to the script for convenience. 104 # Store the full path to the script for convenience.
@@ -108,7 +134,7 @@ class RepoHook(object):
108 # NOTE: Local (non-committed) changes will not be factored into this hash. 134 # NOTE: Local (non-committed) changes will not be factored into this hash.
109 # I think this is OK, since we're really only worried about warning the user 135 # I think this is OK, since we're really only worried about warning the user
110 # about upstream changes. 136 # about upstream changes.
111 return self._hooks_project.work_git.rev_parse('HEAD') 137 return self._hooks_project.work_git.rev_parse(HEAD)
112 138
113 def _GetMustVerb(self): 139 def _GetMustVerb(self):
114 """Return 'must' if the hook is required; 'should' if not.""" 140 """Return 'must' if the hook is required; 'should' if not."""
@@ -347,7 +373,7 @@ context['main'](**kwargs)
347 373
348 try: 374 try:
349 # Always run hooks with CWD as topdir. 375 # Always run hooks with CWD as topdir.
350 os.chdir(self._topdir) 376 os.chdir(self._repo_topdir)
351 377
352 # Put the hook dir as the first item of sys.path so hooks can do 378 # 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 379 # relative imports. We want to replace the repo dir as [0] so
@@ -397,7 +423,12 @@ context['main'](**kwargs)
397 sys.path = orig_syspath 423 sys.path = orig_syspath
398 os.chdir(orig_path) 424 os.chdir(orig_path)
399 425
400 def Run(self, user_allows_all_hooks, **kwargs): 426 def _CheckHook(self):
427 # Bail with a nice error if we can't find the hook.
428 if not os.path.isfile(self._script_fullpath):
429 raise HookError('Couldn\'t find repo hook: %s' % self._script_fullpath)
430
431 def Run(self, **kwargs):
401 """Run the hook. 432 """Run the hook.
402 433
403 If the hook doesn't exist (because there is no hooks project or because 434 If the hook doesn't exist (because there is no hooks project or because
@@ -410,22 +441,80 @@ context['main'](**kwargs)
410 to the hook type. For instance, pre-upload hooks will contain 441 to the hook type. For instance, pre-upload hooks will contain
411 a project_list. 442 a project_list.
412 443
413 Raises: 444 Returns:
414 HookError: If there was a problem finding the hook or the user declined 445 True: On success or ignore hooks by user-request
415 to run a required hook (from _CheckForHookApproval). 446 False: The hook failed. The caller should respond with aborting the action.
447 Some examples in which False is returned:
448 * Finding the hook failed while it was enabled, or
449 * the user declined to run a required hook (from _CheckForHookApproval)
450 In all these cases the user did not pass the proper arguments to
451 ignore the result through the option combinations as listed in
452 AddHookOptionGroup().
416 """ 453 """
417 # No-op if there is no hooks project or if hook is disabled. 454 # Do not do anything in case bypass_hooks is set, or
418 if ((not self._hooks_project) or (self._hook_type not in 455 # no-op if there is no hooks project or if hook is disabled.
419 self._hooks_project.enabled_repo_hooks)): 456 if (self._bypass_hooks or
420 return 457 not self._hooks_project or
421 458 self._hook_type not in self._hooks_project.enabled_repo_hooks):
422 # Bail with a nice error if we can't find the hook. 459 return True
423 if not os.path.isfile(self._script_fullpath): 460
424 raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath) 461 passed = True
462 try:
463 self._CheckHook()
464
465 # Make sure the user is OK with running the hook.
466 if self._allow_all_hooks or self._CheckForHookApproval():
467 # Run the hook with the same version of python we're using.
468 self._ExecuteHook(**kwargs)
469 except SystemExit as e:
470 passed = False
471 print('ERROR: %s hooks exited with exit code: %s' % (self._hook_type, str(e)),
472 file=sys.stderr)
473 except HookError as e:
474 passed = False
475 print('ERROR: %s' % str(e), file=sys.stderr)
476
477 if not passed and self._ignore_hooks:
478 print('\nWARNING: %s hooks failed, but continuing anyways.' % self._hook_type,
479 file=sys.stderr)
480 passed = True
481
482 return passed
483
484 @classmethod
485 def FromSubcmd(cls, manifest, opt, *args, **kwargs):
486 """Method to construct the repo hook class
425 487
426 # Make sure the user is OK with running the hook. 488 Args:
427 if (not user_allows_all_hooks) and (not self._CheckForHookApproval()): 489 manifest: The current active manifest for this command from which we
428 return 490 extract a couple of fields.
491 opt: Contains the commandline options for the action of this hook.
492 It should contain the options added by AddHookOptionGroup() in which
493 we are interested in RepoHook execution.
494 """
495 for key in ('bypass_hooks', 'allow_all_hooks', 'ignore_hooks'):
496 kwargs.setdefault(key, getattr(opt, key))
497 kwargs.update({
498 'hooks_project': manifest.repo_hooks_project,
499 'repo_topdir': manifest.topdir,
500 'manifest_url': manifest.manifestProject.GetRemote('origin').url,
501 })
502 return cls(*args, **kwargs)
429 503
430 # Run the hook with the same version of python we're using. 504 @staticmethod
431 self._ExecuteHook(**kwargs) 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('--no-verify',
513 dest='bypass_hooks', action='store_true',
514 help='Do not run the %s hook.' % name)
515 group.add_option('--verify',
516 dest='allow_all_hooks', action='store_true',
517 help='Run the %s hook without prompting.' % name)
518 group.add_option('--ignore-hooks',
519 action='store_true',
520 help='Do not abort if %s hooks fail.' % name)