diff options
Diffstat (limited to 'hooks.py')
-rw-r--r-- | hooks.py | 145 |
1 files changed, 117 insertions, 28 deletions
@@ -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 | ||
17 | import errno | ||
17 | import json | 18 | import json |
18 | import os | 19 | import os |
19 | import re | 20 | import re |
21 | import subprocess | ||
20 | import sys | 22 | import sys |
21 | import traceback | 23 | import 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 | |||
36 | class RepoHook(object): | 39 | class 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) | ||