diff options
| -rw-r--r-- | hooks.py | 145 | ||||
| -rw-r--r-- | subcmds/upload.py | 64 |
2 files changed, 128 insertions, 81 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) | ||
diff --git a/subcmds/upload.py b/subcmds/upload.py index f441aae4..6196fe4c 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
| @@ -21,7 +21,7 @@ import sys | |||
| 21 | 21 | ||
| 22 | from command import InteractiveCommand | 22 | from command import InteractiveCommand |
| 23 | from editor import Editor | 23 | from editor import Editor |
| 24 | from error import HookError, UploadError | 24 | from error import UploadError |
| 25 | from git_command import GitCommand | 25 | from git_command import GitCommand |
| 26 | from git_refs import R_HEADS | 26 | from git_refs import R_HEADS |
| 27 | from hooks import RepoHook | 27 | from hooks import RepoHook |
| @@ -205,33 +205,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
| 205 | p.add_option('--no-cert-checks', | 205 | p.add_option('--no-cert-checks', |
| 206 | dest='validate_certs', action='store_false', default=True, | 206 | dest='validate_certs', action='store_false', default=True, |
| 207 | help='Disable verifying ssl certs (unsafe).') | 207 | help='Disable verifying ssl certs (unsafe).') |
| 208 | 208 | RepoHook.AddOptionGroup(p, 'pre-upload') | |
| 209 | # Options relating to upload hook. Note that verify and no-verify are NOT | ||
| 210 | # opposites of each other, which is why they store to different locations. | ||
| 211 | # We are using them to match 'git commit' syntax. | ||
| 212 | # | ||
| 213 | # Combinations: | ||
| 214 | # - no-verify=False, verify=False (DEFAULT): | ||
| 215 | # If stdout is a tty, can prompt about running upload hooks if needed. | ||
| 216 | # If user denies running hooks, the upload is cancelled. If stdout is | ||
| 217 | # not a tty and we would need to prompt about upload hooks, upload is | ||
| 218 | # cancelled. | ||
| 219 | # - no-verify=False, verify=True: | ||
| 220 | # Always run upload hooks with no prompt. | ||
| 221 | # - no-verify=True, verify=False: | ||
| 222 | # Never run upload hooks, but upload anyway (AKA bypass hooks). | ||
| 223 | # - no-verify=True, verify=True: | ||
| 224 | # Invalid | ||
| 225 | g = p.add_option_group('Upload hooks') | ||
| 226 | g.add_option('--no-verify', | ||
| 227 | dest='bypass_hooks', action='store_true', | ||
| 228 | help='Do not run the upload hook.') | ||
| 229 | g.add_option('--verify', | ||
| 230 | dest='allow_all_hooks', action='store_true', | ||
| 231 | help='Run the upload hook without prompting.') | ||
| 232 | g.add_option('--ignore-hooks', | ||
| 233 | dest='ignore_hooks', action='store_true', | ||
| 234 | help='Do not abort uploading if upload hooks fail.') | ||
| 235 | 209 | ||
| 236 | def _SingleBranch(self, opt, branch, people): | 210 | def _SingleBranch(self, opt, branch, people): |
| 237 | project = branch.project | 211 | project = branch.project |
| @@ -572,31 +546,15 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
| 572 | (branch,), file=sys.stderr) | 546 | (branch,), file=sys.stderr) |
| 573 | return 1 | 547 | return 1 |
| 574 | 548 | ||
| 575 | if not opt.bypass_hooks: | 549 | pending_proj_names = [project.name for (project, available) in pending] |
| 576 | hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, | 550 | pending_worktrees = [project.worktree for (project, available) in pending] |
| 577 | self.manifest.topdir, | 551 | hook = RepoHook.FromSubcmd( |
| 578 | self.manifest.manifestProject.GetRemote('origin').url, | 552 | hook_type='pre-upload', manifest=self.manifest, |
| 579 | abort_if_user_denies=True) | 553 | opt=opt, abort_if_user_denies=True) |
| 580 | pending_proj_names = [project.name for (project, available) in pending] | 554 | if not hook.Run( |
| 581 | pending_worktrees = [project.worktree for (project, available) in pending] | 555 | project_list=pending_proj_names, |
| 582 | passed = True | 556 | worktree_list=pending_worktrees): |
| 583 | try: | 557 | return 1 |
| 584 | hook.Run(opt.allow_all_hooks, project_list=pending_proj_names, | ||
| 585 | worktree_list=pending_worktrees) | ||
| 586 | except SystemExit: | ||
| 587 | passed = False | ||
| 588 | if not opt.ignore_hooks: | ||
| 589 | raise | ||
| 590 | except HookError as e: | ||
| 591 | passed = False | ||
| 592 | print("ERROR: %s" % str(e), file=sys.stderr) | ||
| 593 | |||
| 594 | if not passed: | ||
| 595 | if opt.ignore_hooks: | ||
| 596 | print('\nWARNING: pre-upload hooks failed, but uploading anyways.', | ||
| 597 | file=sys.stderr) | ||
| 598 | else: | ||
| 599 | return 1 | ||
| 600 | 558 | ||
| 601 | if opt.reviewers: | 559 | if opt.reviewers: |
| 602 | reviewers = _SplitEmails(opt.reviewers) | 560 | reviewers = _SplitEmails(opt.reviewers) |
