Format codebase with black and check formatting in CQ
Apply rules set by https://gerrit-review.googlesource.com/c/git-repo/+/362954/ across the codebase and fix any lingering errors caught
by flake8. Also check black formatting in run_tests (and CQ).
Bug: b/267675342
Change-Id: I972d77649dac351150dcfeb1cd1ad0ea2efc1956
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/363474
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
diff --git a/hooks.py b/hooks.py
index 67c21a2..decf069 100644
--- a/hooks.py
+++ b/hooks.py
@@ -26,271 +26,293 @@
class RepoHook(object):
- """A RepoHook contains information about a script to run as a hook.
+ """A RepoHook contains information about a script to run as a hook.
- Hooks are used to run a python script before running an upload (for instance,
- to run presubmit checks). Eventually, we may have hooks for other actions.
+ Hooks are used to run a python script before running an upload (for
+ instance, to run presubmit checks). Eventually, we may have hooks for other
+ actions.
- This shouldn't be confused with files in the 'repo/hooks' directory. Those
- files are copied into each '.git/hooks' folder for each project. Repo-level
- hooks are associated instead with repo actions.
+ This shouldn't be confused with files in the 'repo/hooks' directory. Those
+ files are copied into each '.git/hooks' folder for each project. Repo-level
+ hooks are associated instead with repo actions.
- Hooks are always python. When a hook is run, we will load the hook into the
- interpreter and execute its main() function.
+ Hooks are always python. When a hook is run, we will load the hook into the
+ interpreter and execute its main() function.
- Combinations of hook option flags:
- - no-verify=False, verify=False (DEFAULT):
- If stdout is a tty, can prompt about running hooks if needed.
- If user denies running hooks, the action is cancelled. If stdout is
- not a tty and we would need to prompt about hooks, action is
- cancelled.
- - no-verify=False, verify=True:
- Always run hooks with no prompt.
- - no-verify=True, verify=False:
- Never run hooks, but run action anyway (AKA bypass hooks).
- - no-verify=True, verify=True:
- Invalid
- """
-
- def __init__(self,
- hook_type,
- hooks_project,
- repo_topdir,
- manifest_url,
- bypass_hooks=False,
- allow_all_hooks=False,
- ignore_hooks=False,
- abort_if_user_denies=False):
- """RepoHook constructor.
-
- Params:
- hook_type: A string representing the type of hook. This is also used
- to figure out the name of the file containing the hook. For
- example: 'pre-upload'.
- hooks_project: The project containing the repo hooks.
- If you have a manifest, this is manifest.repo_hooks_project.
- OK if this is None, which will make the hook a no-op.
- repo_topdir: The top directory of the repo client checkout.
- This is the one containing the .repo directory. Scripts will
- run with CWD as this directory.
- If you have a manifest, this is manifest.topdir.
- manifest_url: The URL to the manifest git repo.
- bypass_hooks: If True, then 'Do not run the hook'.
- allow_all_hooks: If True, then 'Run the hook without prompting'.
- ignore_hooks: If True, then 'Do not abort action if hooks fail'.
- abort_if_user_denies: If True, we'll abort running the hook if the user
- doesn't allow us to run the hook.
+ Combinations of hook option flags:
+ - no-verify=False, verify=False (DEFAULT):
+ If stdout is a tty, can prompt about running hooks if needed.
+ If user denies running hooks, the action is cancelled. If stdout is
+ not a tty and we would need to prompt about hooks, action is
+ cancelled.
+ - no-verify=False, verify=True:
+ Always run hooks with no prompt.
+ - no-verify=True, verify=False:
+ Never run hooks, but run action anyway (AKA bypass hooks).
+ - no-verify=True, verify=True:
+ Invalid
"""
- self._hook_type = hook_type
- self._hooks_project = hooks_project
- self._repo_topdir = repo_topdir
- self._manifest_url = manifest_url
- self._bypass_hooks = bypass_hooks
- self._allow_all_hooks = allow_all_hooks
- self._ignore_hooks = ignore_hooks
- self._abort_if_user_denies = abort_if_user_denies
- # Store the full path to the script for convenience.
- if self._hooks_project:
- self._script_fullpath = os.path.join(self._hooks_project.worktree,
- self._hook_type + '.py')
- else:
- self._script_fullpath = None
+ def __init__(
+ self,
+ hook_type,
+ hooks_project,
+ repo_topdir,
+ manifest_url,
+ bypass_hooks=False,
+ allow_all_hooks=False,
+ ignore_hooks=False,
+ abort_if_user_denies=False,
+ ):
+ """RepoHook constructor.
- def _GetHash(self):
- """Return a hash of the contents of the hooks directory.
+ Params:
+ hook_type: A string representing the type of hook. This is also used
+ to figure out the name of the file containing the hook. For
+ example: 'pre-upload'.
+ hooks_project: The project containing the repo hooks.
+ If you have a manifest, this is manifest.repo_hooks_project.
+ OK if this is None, which will make the hook a no-op.
+ repo_topdir: The top directory of the repo client checkout.
+ This is the one containing the .repo directory. Scripts will
+ run with CWD as this directory.
+ If you have a manifest, this is manifest.topdir.
+ manifest_url: The URL to the manifest git repo.
+ bypass_hooks: If True, then 'Do not run the hook'.
+ allow_all_hooks: If True, then 'Run the hook without prompting'.
+ ignore_hooks: If True, then 'Do not abort action if hooks fail'.
+ abort_if_user_denies: If True, we'll abort running the hook if the
+ user doesn't allow us to run the hook.
+ """
+ self._hook_type = hook_type
+ self._hooks_project = hooks_project
+ self._repo_topdir = repo_topdir
+ self._manifest_url = manifest_url
+ self._bypass_hooks = bypass_hooks
+ self._allow_all_hooks = allow_all_hooks
+ self._ignore_hooks = ignore_hooks
+ self._abort_if_user_denies = abort_if_user_denies
- We'll just use git to do this. This hash has the property that if anything
- changes in the directory we will return a different has.
+ # Store the full path to the script for convenience.
+ if self._hooks_project:
+ self._script_fullpath = os.path.join(
+ self._hooks_project.worktree, self._hook_type + ".py"
+ )
+ else:
+ self._script_fullpath = None
- SECURITY CONSIDERATION:
- This hash only represents the contents of files in the hook directory, not
- any other files imported or called by hooks. Changes to imported files
- can change the script behavior without affecting the hash.
+ def _GetHash(self):
+ """Return a hash of the contents of the hooks directory.
- Returns:
- A string representing the hash. This will always be ASCII so that it can
- be printed to the user easily.
- """
- assert self._hooks_project, "Must have hooks to calculate their hash."
+ We'll just use git to do this. This hash has the property that if
+ anything changes in the directory we will return a different has.
- # We will use the work_git object rather than just calling GetRevisionId().
- # That gives us a hash of the latest checked in version of the files that
- # the user will actually be executing. Specifically, GetRevisionId()
- # doesn't appear to change even if a user checks out a different version
- # of the hooks repo (via git checkout) nor if a user commits their own revs.
- #
- # NOTE: Local (non-committed) changes will not be factored into this hash.
- # I think this is OK, since we're really only worried about warning the user
- # about upstream changes.
- return self._hooks_project.work_git.rev_parse(HEAD)
+ SECURITY CONSIDERATION:
+ This hash only represents the contents of files in the hook
+ directory, not any other files imported or called by hooks. Changes
+ to imported files can change the script behavior without affecting
+ the hash.
- def _GetMustVerb(self):
- """Return 'must' if the hook is required; 'should' if not."""
- if self._abort_if_user_denies:
- return 'must'
- else:
- return 'should'
+ Returns:
+ A string representing the hash. This will always be ASCII so that
+ it can be printed to the user easily.
+ """
+ assert self._hooks_project, "Must have hooks to calculate their hash."
- def _CheckForHookApproval(self):
- """Check to see whether this hook has been approved.
+ # We will use the work_git object rather than just calling
+ # GetRevisionId(). That gives us a hash of the latest checked in version
+ # of the files that the user will actually be executing. Specifically,
+ # GetRevisionId() doesn't appear to change even if a user checks out a
+ # different version of the hooks repo (via git checkout) nor if a user
+ # commits their own revs.
+ #
+ # NOTE: Local (non-committed) changes will not be factored into this
+ # hash. I think this is OK, since we're really only worried about
+ # warning the user about upstream changes.
+ return self._hooks_project.work_git.rev_parse(HEAD)
- We'll accept approval of manifest URLs if they're using secure transports.
- This way the user can say they trust the manifest hoster. For insecure
- hosts, we fall back to checking the hash of the hooks repo.
+ def _GetMustVerb(self):
+ """Return 'must' if the hook is required; 'should' if not."""
+ if self._abort_if_user_denies:
+ return "must"
+ else:
+ return "should"
- Note that we ask permission for each individual hook even though we use
- the hash of all hooks when detecting changes. We'd like the user to be
- able to approve / deny each hook individually. We only use the hash of all
- hooks because there is no other easy way to detect changes to local imports.
+ def _CheckForHookApproval(self):
+ """Check to see whether this hook has been approved.
- Returns:
- True if this hook is approved to run; False otherwise.
+ We'll accept approval of manifest URLs if they're using secure
+ transports. This way the user can say they trust the manifest hoster.
+ For insecure hosts, we fall back to checking the hash of the hooks repo.
- Raises:
- HookError: Raised if the user doesn't approve and abort_if_user_denies
- was passed to the consturctor.
- """
- if self._ManifestUrlHasSecureScheme():
- return self._CheckForHookApprovalManifest()
- else:
- return self._CheckForHookApprovalHash()
+ Note that we ask permission for each individual hook even though we use
+ the hash of all hooks when detecting changes. We'd like the user to be
+ able to approve / deny each hook individually. We only use the hash of
+ all hooks because there is no other easy way to detect changes to local
+ imports.
- def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
- changed_prompt):
- """Check for approval for a particular attribute and hook.
+ Returns:
+ True if this hook is approved to run; False otherwise.
- Args:
- subkey: The git config key under [repo.hooks.<hook_type>] to store the
- last approved string.
- new_val: The new value to compare against the last approved one.
- main_prompt: Message to display to the user to ask for approval.
- changed_prompt: Message explaining why we're re-asking for approval.
+ Raises:
+ HookError: Raised if the user doesn't approve and
+ abort_if_user_denies was passed to the consturctor.
+ """
+ if self._ManifestUrlHasSecureScheme():
+ return self._CheckForHookApprovalManifest()
+ else:
+ return self._CheckForHookApprovalHash()
- Returns:
- True if this hook is approved to run; False otherwise.
+ def _CheckForHookApprovalHelper(
+ self, subkey, new_val, main_prompt, changed_prompt
+ ):
+ """Check for approval for a particular attribute and hook.
- Raises:
- HookError: Raised if the user doesn't approve and abort_if_user_denies
- was passed to the consturctor.
- """
- hooks_config = self._hooks_project.config
- git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
+ Args:
+ subkey: The git config key under [repo.hooks.<hook_type>] to store
+ the last approved string.
+ new_val: The new value to compare against the last approved one.
+ main_prompt: Message to display to the user to ask for approval.
+ changed_prompt: Message explaining why we're re-asking for approval.
- # Get the last value that the user approved for this hook; may be None.
- old_val = hooks_config.GetString(git_approval_key)
+ Returns:
+ True if this hook is approved to run; False otherwise.
- if old_val is not None:
- # User previously approved hook and asked not to be prompted again.
- if new_val == old_val:
- # Approval matched. We're done.
- return True
- else:
- # Give the user a reason why we're prompting, since they last told
- # us to "never ask again".
- prompt = 'WARNING: %s\n\n' % (changed_prompt,)
- else:
- prompt = ''
+ Raises:
+ HookError: Raised if the user doesn't approve and
+ abort_if_user_denies was passed to the consturctor.
+ """
+ hooks_config = self._hooks_project.config
+ git_approval_key = "repo.hooks.%s.%s" % (self._hook_type, subkey)
- # Prompt the user if we're not on a tty; on a tty we'll assume "no".
- if sys.stdout.isatty():
- prompt += main_prompt + ' (yes/always/NO)? '
- response = input(prompt).lower()
- print()
+ # Get the last value that the user approved for this hook; may be None.
+ old_val = hooks_config.GetString(git_approval_key)
- # User is doing a one-time approval.
- if response in ('y', 'yes'):
- return True
- elif response == 'always':
- hooks_config.SetString(git_approval_key, new_val)
- return True
+ if old_val is not None:
+ # User previously approved hook and asked not to be prompted again.
+ if new_val == old_val:
+ # Approval matched. We're done.
+ return True
+ else:
+ # Give the user a reason why we're prompting, since they last
+ # told us to "never ask again".
+ prompt = "WARNING: %s\n\n" % (changed_prompt,)
+ else:
+ prompt = ""
- # For anything else, we'll assume no approval.
- if self._abort_if_user_denies:
- raise HookError('You must allow the %s hook or use --no-verify.' %
- self._hook_type)
+ # Prompt the user if we're not on a tty; on a tty we'll assume "no".
+ if sys.stdout.isatty():
+ prompt += main_prompt + " (yes/always/NO)? "
+ response = input(prompt).lower()
+ print()
- return False
+ # User is doing a one-time approval.
+ if response in ("y", "yes"):
+ return True
+ elif response == "always":
+ hooks_config.SetString(git_approval_key, new_val)
+ return True
- def _ManifestUrlHasSecureScheme(self):
- """Check if the URI for the manifest is a secure transport."""
- secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
- parse_results = urllib.parse.urlparse(self._manifest_url)
- return parse_results.scheme in secure_schemes
+ # For anything else, we'll assume no approval.
+ if self._abort_if_user_denies:
+ raise HookError(
+ "You must allow the %s hook or use --no-verify."
+ % self._hook_type
+ )
- def _CheckForHookApprovalManifest(self):
- """Check whether the user has approved this manifest host.
+ return False
- Returns:
- True if this hook is approved to run; False otherwise.
- """
- return self._CheckForHookApprovalHelper(
- 'approvedmanifest',
- self._manifest_url,
- 'Run hook scripts from %s' % (self._manifest_url,),
- 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
+ def _ManifestUrlHasSecureScheme(self):
+ """Check if the URI for the manifest is a secure transport."""
+ secure_schemes = (
+ "file",
+ "https",
+ "ssh",
+ "persistent-https",
+ "sso",
+ "rpc",
+ )
+ parse_results = urllib.parse.urlparse(self._manifest_url)
+ return parse_results.scheme in secure_schemes
- def _CheckForHookApprovalHash(self):
- """Check whether the user has approved the hooks repo.
+ def _CheckForHookApprovalManifest(self):
+ """Check whether the user has approved this manifest host.
- Returns:
- True if this hook is approved to run; False otherwise.
- """
- prompt = ('Repo %s run the script:\n'
- ' %s\n'
- '\n'
- 'Do you want to allow this script to run')
- return self._CheckForHookApprovalHelper(
- 'approvedhash',
- self._GetHash(),
- prompt % (self._GetMustVerb(), self._script_fullpath),
- 'Scripts have changed since %s was allowed.' % (self._hook_type,))
+ Returns:
+ True if this hook is approved to run; False otherwise.
+ """
+ return self._CheckForHookApprovalHelper(
+ "approvedmanifest",
+ self._manifest_url,
+ "Run hook scripts from %s" % (self._manifest_url,),
+ "Manifest URL has changed since %s was allowed."
+ % (self._hook_type,),
+ )
- @staticmethod
- def _ExtractInterpFromShebang(data):
- """Extract the interpreter used in the shebang.
+ def _CheckForHookApprovalHash(self):
+ """Check whether the user has approved the hooks repo.
- Try to locate the interpreter the script is using (ignoring `env`).
+ Returns:
+ True if this hook is approved to run; False otherwise.
+ """
+ prompt = (
+ "Repo %s run the script:\n"
+ " %s\n"
+ "\n"
+ "Do you want to allow this script to run"
+ )
+ return self._CheckForHookApprovalHelper(
+ "approvedhash",
+ self._GetHash(),
+ prompt % (self._GetMustVerb(), self._script_fullpath),
+ "Scripts have changed since %s was allowed." % (self._hook_type,),
+ )
- Args:
- data: The file content of the script.
+ @staticmethod
+ def _ExtractInterpFromShebang(data):
+ """Extract the interpreter used in the shebang.
- Returns:
- The basename of the main script interpreter, or None if a shebang is not
- used or could not be parsed out.
- """
- firstline = data.splitlines()[:1]
- if not firstline:
- return None
+ Try to locate the interpreter the script is using (ignoring `env`).
- # The format here can be tricky.
- shebang = firstline[0].strip()
- m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
- if not m:
- return None
+ Args:
+ data: The file content of the script.
- # If the using `env`, find the target program.
- interp = m.group(1)
- if os.path.basename(interp) == 'env':
- interp = m.group(2)
+ Returns:
+ The basename of the main script interpreter, or None if a shebang is
+ not used or could not be parsed out.
+ """
+ firstline = data.splitlines()[:1]
+ if not firstline:
+ return None
- return interp
+ # The format here can be tricky.
+ shebang = firstline[0].strip()
+ m = re.match(r"^#!\s*([^\s]+)(?:\s+([^\s]+))?", shebang)
+ if not m:
+ return None
- def _ExecuteHookViaReexec(self, interp, context, **kwargs):
- """Execute the hook script through |interp|.
+ # If the using `env`, find the target program.
+ interp = m.group(1)
+ if os.path.basename(interp) == "env":
+ interp = m.group(2)
- Note: Support for this feature should be dropped ~Jun 2021.
+ return interp
- Args:
- interp: The Python program to run.
- context: Basic Python context to execute the hook inside.
- kwargs: Arbitrary arguments to pass to the hook script.
+ def _ExecuteHookViaReexec(self, interp, context, **kwargs):
+ """Execute the hook script through |interp|.
- Raises:
- HookError: When the hooks failed for any reason.
- """
- # This logic needs to be kept in sync with _ExecuteHookViaImport below.
- script = """
+ Note: Support for this feature should be dropped ~Jun 2021.
+
+ Args:
+ interp: The Python program to run.
+ context: Basic Python context to execute the hook inside.
+ kwargs: Arbitrary arguments to pass to the hook script.
+
+ Raises:
+ HookError: When the hooks failed for any reason.
+ """
+ # This logic needs to be kept in sync with _ExecuteHookViaImport below.
+ script = """
import json, os, sys
path = '''%(path)s'''
kwargs = json.loads('''%(kwargs)s''')
@@ -300,210 +322,240 @@
exec(compile(data, path, 'exec'), context)
context['main'](**kwargs)
""" % {
- 'path': self._script_fullpath,
- 'kwargs': json.dumps(kwargs),
- 'context': json.dumps(context),
- }
+ "path": self._script_fullpath,
+ "kwargs": json.dumps(kwargs),
+ "context": json.dumps(context),
+ }
- # We pass the script via stdin to avoid OS argv limits. It also makes
- # unhandled exception tracebacks less verbose/confusing for users.
- cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
- proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
- proc.communicate(input=script.encode('utf-8'))
- if proc.returncode:
- raise HookError('Failed to run %s hook.' % (self._hook_type,))
+ # We pass the script via stdin to avoid OS argv limits. It also makes
+ # unhandled exception tracebacks less verbose/confusing for users.
+ cmd = [interp, "-c", "import sys; exec(sys.stdin.read())"]
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
+ proc.communicate(input=script.encode("utf-8"))
+ if proc.returncode:
+ raise HookError("Failed to run %s hook." % (self._hook_type,))
- def _ExecuteHookViaImport(self, data, context, **kwargs):
- """Execute the hook code in |data| directly.
+ def _ExecuteHookViaImport(self, data, context, **kwargs):
+ """Execute the hook code in |data| directly.
- Args:
- data: The code of the hook to execute.
- context: Basic Python context to execute the hook inside.
- kwargs: Arbitrary arguments to pass to the hook script.
+ Args:
+ data: The code of the hook to execute.
+ context: Basic Python context to execute the hook inside.
+ kwargs: Arbitrary arguments to pass to the hook script.
- Raises:
- HookError: When the hooks failed for any reason.
- """
- # Exec, storing global context in the context dict. We catch exceptions
- # and convert to a HookError w/ just the failing traceback.
- try:
- exec(compile(data, self._script_fullpath, 'exec'), context)
- except Exception:
- raise HookError('%s\nFailed to import %s hook; see traceback above.' %
- (traceback.format_exc(), self._hook_type))
-
- # Running the script should have defined a main() function.
- if 'main' not in context:
- raise HookError('Missing main() in: "%s"' % self._script_fullpath)
-
- # Call the main function in the hook. If the hook should cause the
- # build to fail, it will raise an Exception. We'll catch that convert
- # to a HookError w/ just the failing traceback.
- try:
- context['main'](**kwargs)
- except Exception:
- raise HookError('%s\nFailed to run main() for %s hook; see traceback '
- 'above.' % (traceback.format_exc(), self._hook_type))
-
- def _ExecuteHook(self, **kwargs):
- """Actually execute the given hook.
-
- This will run the hook's 'main' function in our python interpreter.
-
- Args:
- kwargs: Keyword arguments to pass to the hook. These are often specific
- to the hook type. For instance, pre-upload hooks will contain
- a project_list.
- """
- # Keep sys.path and CWD stashed away so that we can always restore them
- # upon function exit.
- orig_path = os.getcwd()
- orig_syspath = sys.path
-
- try:
- # Always run hooks with CWD as topdir.
- os.chdir(self._repo_topdir)
-
- # Put the hook dir as the first item of sys.path so hooks can do
- # relative imports. We want to replace the repo dir as [0] so
- # hooks can't import repo files.
- sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
-
- # Initial global context for the hook to run within.
- context = {'__file__': self._script_fullpath}
-
- # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
- # We don't actually want hooks to define their main with this argument--
- # it's there to remind them that their hook should always take **kwargs.
- # For instance, a pre-upload hook should be defined like:
- # def main(project_list, **kwargs):
- #
- # This allows us to later expand the API without breaking old hooks.
- kwargs = kwargs.copy()
- kwargs['hook_should_take_kwargs'] = True
-
- # See what version of python the hook has been written against.
- data = open(self._script_fullpath).read()
- interp = self._ExtractInterpFromShebang(data)
- reexec = False
- if interp:
- prog = os.path.basename(interp)
- if prog.startswith('python2') and sys.version_info.major != 2:
- reexec = True
- elif prog.startswith('python3') and sys.version_info.major == 2:
- reexec = True
-
- # Attempt to execute the hooks through the requested version of Python.
- if reexec:
+ Raises:
+ HookError: When the hooks failed for any reason.
+ """
+ # Exec, storing global context in the context dict. We catch exceptions
+ # and convert to a HookError w/ just the failing traceback.
try:
- self._ExecuteHookViaReexec(interp, context, **kwargs)
- except OSError as e:
- if e.errno == errno.ENOENT:
- # We couldn't find the interpreter, so fallback to importing.
+ exec(compile(data, self._script_fullpath, "exec"), context)
+ except Exception:
+ raise HookError(
+ "%s\nFailed to import %s hook; see traceback above."
+ % (traceback.format_exc(), self._hook_type)
+ )
+
+ # Running the script should have defined a main() function.
+ if "main" not in context:
+ raise HookError('Missing main() in: "%s"' % self._script_fullpath)
+
+ # Call the main function in the hook. If the hook should cause the
+ # build to fail, it will raise an Exception. We'll catch that convert
+ # to a HookError w/ just the failing traceback.
+ try:
+ context["main"](**kwargs)
+ except Exception:
+ raise HookError(
+ "%s\nFailed to run main() for %s hook; see traceback "
+ "above." % (traceback.format_exc(), self._hook_type)
+ )
+
+ def _ExecuteHook(self, **kwargs):
+ """Actually execute the given hook.
+
+ This will run the hook's 'main' function in our python interpreter.
+
+ Args:
+ kwargs: Keyword arguments to pass to the hook. These are often
+ specific to the hook type. For instance, pre-upload hooks will
+ contain a project_list.
+ """
+ # Keep sys.path and CWD stashed away so that we can always restore them
+ # upon function exit.
+ orig_path = os.getcwd()
+ orig_syspath = sys.path
+
+ try:
+ # Always run hooks with CWD as topdir.
+ os.chdir(self._repo_topdir)
+
+ # Put the hook dir as the first item of sys.path so hooks can do
+ # relative imports. We want to replace the repo dir as [0] so
+ # hooks can't import repo files.
+ sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
+
+ # Initial global context for the hook to run within.
+ context = {"__file__": self._script_fullpath}
+
+ # Add 'hook_should_take_kwargs' to the arguments to be passed to
+ # main. We don't actually want hooks to define their main with this
+ # argument--it's there to remind them that their hook should always
+ # take **kwargs.
+ # For instance, a pre-upload hook should be defined like:
+ # def main(project_list, **kwargs):
+ #
+ # This allows us to later expand the API without breaking old hooks.
+ kwargs = kwargs.copy()
+ kwargs["hook_should_take_kwargs"] = True
+
+ # See what version of python the hook has been written against.
+ data = open(self._script_fullpath).read()
+ interp = self._ExtractInterpFromShebang(data)
reexec = False
- else:
- raise
+ if interp:
+ prog = os.path.basename(interp)
+ if prog.startswith("python2") and sys.version_info.major != 2:
+ reexec = True
+ elif prog.startswith("python3") and sys.version_info.major == 2:
+ reexec = True
- # Run the hook by importing directly.
- if not reexec:
- self._ExecuteHookViaImport(data, context, **kwargs)
- finally:
- # Restore sys.path and CWD.
- sys.path = orig_syspath
- os.chdir(orig_path)
+ # Attempt to execute the hooks through the requested version of
+ # Python.
+ if reexec:
+ try:
+ self._ExecuteHookViaReexec(interp, context, **kwargs)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ # We couldn't find the interpreter, so fallback to
+ # importing.
+ reexec = False
+ else:
+ raise
- def _CheckHook(self):
- # Bail with a nice error if we can't find the hook.
- if not os.path.isfile(self._script_fullpath):
- raise HookError('Couldn\'t find repo hook: %s' % self._script_fullpath)
+ # Run the hook by importing directly.
+ if not reexec:
+ self._ExecuteHookViaImport(data, context, **kwargs)
+ finally:
+ # Restore sys.path and CWD.
+ sys.path = orig_syspath
+ os.chdir(orig_path)
- def Run(self, **kwargs):
- """Run the hook.
+ def _CheckHook(self):
+ # Bail with a nice error if we can't find the hook.
+ if not os.path.isfile(self._script_fullpath):
+ raise HookError(
+ "Couldn't find repo hook: %s" % self._script_fullpath
+ )
- If the hook doesn't exist (because there is no hooks project or because
- this particular hook is not enabled), this is a no-op.
+ def Run(self, **kwargs):
+ """Run the hook.
- Args:
- user_allows_all_hooks: If True, we will never prompt about running the
- hook--we'll just assume it's OK to run it.
- kwargs: Keyword arguments to pass to the hook. These are often specific
- to the hook type. For instance, pre-upload hooks will contain
- a project_list.
+ If the hook doesn't exist (because there is no hooks project or because
+ this particular hook is not enabled), this is a no-op.
- Returns:
- True: On success or ignore hooks by user-request
- False: The hook failed. The caller should respond with aborting the action.
- Some examples in which False is returned:
- * Finding the hook failed while it was enabled, or
- * the user declined to run a required hook (from _CheckForHookApproval)
- In all these cases the user did not pass the proper arguments to
- ignore the result through the option combinations as listed in
- AddHookOptionGroup().
- """
- # Do not do anything in case bypass_hooks is set, or
- # no-op if there is no hooks project or if hook is disabled.
- if (self._bypass_hooks or
- not self._hooks_project or
- self._hook_type not in self._hooks_project.enabled_repo_hooks):
- return True
+ Args:
+ user_allows_all_hooks: If True, we will never prompt about running
+ the hook--we'll just assume it's OK to run it.
+ kwargs: Keyword arguments to pass to the hook. These are often
+ specific to the hook type. For instance, pre-upload hooks will
+ contain a project_list.
- passed = True
- try:
- self._CheckHook()
+ Returns:
+ True: On success or ignore hooks by user-request
+ False: The hook failed. The caller should respond with aborting the
+ action. Some examples in which False is returned:
+ * Finding the hook failed while it was enabled, or
+ * the user declined to run a required hook (from
+ _CheckForHookApproval)
+ In all these cases the user did not pass the proper arguments to
+ ignore the result through the option combinations as listed in
+ AddHookOptionGroup().
+ """
+ # Do not do anything in case bypass_hooks is set, or
+ # no-op if there is no hooks project or if hook is disabled.
+ if (
+ self._bypass_hooks
+ or not self._hooks_project
+ or self._hook_type not in self._hooks_project.enabled_repo_hooks
+ ):
+ return True
- # Make sure the user is OK with running the hook.
- if self._allow_all_hooks or self._CheckForHookApproval():
- # Run the hook with the same version of python we're using.
- self._ExecuteHook(**kwargs)
- except SystemExit as e:
- passed = False
- print('ERROR: %s hooks exited with exit code: %s' % (self._hook_type, str(e)),
- file=sys.stderr)
- except HookError as e:
- passed = False
- print('ERROR: %s' % str(e), file=sys.stderr)
+ passed = True
+ try:
+ self._CheckHook()
- if not passed and self._ignore_hooks:
- print('\nWARNING: %s hooks failed, but continuing anyways.' % self._hook_type,
- file=sys.stderr)
- passed = True
+ # Make sure the user is OK with running the hook.
+ if self._allow_all_hooks or self._CheckForHookApproval():
+ # Run the hook with the same version of python we're using.
+ self._ExecuteHook(**kwargs)
+ except SystemExit as e:
+ passed = False
+ print(
+ "ERROR: %s hooks exited with exit code: %s"
+ % (self._hook_type, str(e)),
+ file=sys.stderr,
+ )
+ except HookError as e:
+ passed = False
+ print("ERROR: %s" % str(e), file=sys.stderr)
- return passed
+ if not passed and self._ignore_hooks:
+ print(
+ "\nWARNING: %s hooks failed, but continuing anyways."
+ % self._hook_type,
+ file=sys.stderr,
+ )
+ passed = True
- @classmethod
- def FromSubcmd(cls, manifest, opt, *args, **kwargs):
- """Method to construct the repo hook class
+ return passed
- Args:
- manifest: The current active manifest for this command from which we
- extract a couple of fields.
- opt: Contains the commandline options for the action of this hook.
- It should contain the options added by AddHookOptionGroup() in which
- we are interested in RepoHook execution.
- """
- for key in ('bypass_hooks', 'allow_all_hooks', 'ignore_hooks'):
- kwargs.setdefault(key, getattr(opt, key))
- kwargs.update({
- 'hooks_project': manifest.repo_hooks_project,
- 'repo_topdir': manifest.topdir,
- 'manifest_url': manifest.manifestProject.GetRemote('origin').url,
- })
- return cls(*args, **kwargs)
+ @classmethod
+ def FromSubcmd(cls, manifest, opt, *args, **kwargs):
+ """Method to construct the repo hook class
- @staticmethod
- def AddOptionGroup(parser, name):
- """Help options relating to the various hooks."""
+ Args:
+ manifest: The current active manifest for this command from which we
+ extract a couple of fields.
+ opt: Contains the commandline options for the action of this hook.
+ It should contain the options added by AddHookOptionGroup() in
+ which we are interested in RepoHook execution.
+ """
+ for key in ("bypass_hooks", "allow_all_hooks", "ignore_hooks"):
+ kwargs.setdefault(key, getattr(opt, key))
+ kwargs.update(
+ {
+ "hooks_project": manifest.repo_hooks_project,
+ "repo_topdir": manifest.topdir,
+ "manifest_url": manifest.manifestProject.GetRemote(
+ "origin"
+ ).url,
+ }
+ )
+ return cls(*args, **kwargs)
- # Note that verify and no-verify are NOT opposites of each other, which
- # is why they store to different locations. We are using them to match
- # 'git commit' syntax.
- group = parser.add_option_group(name + ' hooks')
- group.add_option('--no-verify',
- dest='bypass_hooks', action='store_true',
- help='Do not run the %s hook.' % name)
- group.add_option('--verify',
- dest='allow_all_hooks', action='store_true',
- help='Run the %s hook without prompting.' % name)
- group.add_option('--ignore-hooks',
- action='store_true',
- help='Do not abort if %s hooks fail.' % name)
+ @staticmethod
+ def AddOptionGroup(parser, name):
+ """Help options relating to the various hooks."""
+
+ # Note that verify and no-verify are NOT opposites of each other, which
+ # is why they store to different locations. We are using them to match
+ # 'git commit' syntax.
+ group = parser.add_option_group(name + " hooks")
+ group.add_option(
+ "--no-verify",
+ dest="bypass_hooks",
+ action="store_true",
+ help="Do not run the %s hook." % name,
+ )
+ group.add_option(
+ "--verify",
+ dest="allow_all_hooks",
+ action="store_true",
+ help="Run the %s hook without prompting." % name,
+ )
+ group.add_option(
+ "--ignore-hooks",
+ action="store_true",
+ help="Do not abort if %s hooks fail." % name,
+ )