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) | ||
