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