summaryrefslogtreecommitdiffstats
path: root/hooks.py
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2023-03-11 06:46:20 +0000
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-03-22 17:46:28 +0000
commitea2e330e43c182dc16b0111ebc69ee5a71ee4ce1 (patch)
treedc33ba0e56825b3e007d0589891756724725a465 /hooks.py
parent1604cf255f8c1786a23388db6d5277ac7949a24a (diff)
downloadgit-repo-ea2e330e43c182dc16b0111ebc69ee5a71ee4ce1.tar.gz
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>
Diffstat (limited to 'hooks.py')
-rw-r--r--hooks.py988
1 files changed, 520 insertions, 468 deletions
diff --git a/hooks.py b/hooks.py
index 67c21a25..decf0699 100644
--- a/hooks.py
+++ b/hooks.py
@@ -26,271 +26,293 @@ from git_refs import HEAD
26 26
27 27
28class RepoHook(object): 28class RepoHook(object):
29 """A RepoHook contains information about a script to run as a hook. 29 """A RepoHook contains information about a script to run as a hook.
30 30
31 Hooks are used to run a python script before running an upload (for instance, 31 Hooks are used to run a python script before running an upload (for
32 to run presubmit checks). Eventually, we may have hooks for other actions. 32 instance, to run presubmit checks). Eventually, we may have hooks for other
33 33 actions.
34 This shouldn't be confused with files in the 'repo/hooks' directory. Those 34
35 files are copied into each '.git/hooks' folder for each project. Repo-level 35 This shouldn't be confused with files in the 'repo/hooks' directory. Those
36 hooks are associated instead with repo actions. 36 files are copied into each '.git/hooks' folder for each project. Repo-level
37 37 hooks are associated instead with repo actions.
38 Hooks are always python. When a hook is run, we will load the hook into the 38
39 interpreter and execute its main() function. 39 Hooks are always python. When a hook is run, we will load the hook into the
40 40 interpreter and execute its main() function.
41 Combinations of hook option flags: 41
42 - no-verify=False, verify=False (DEFAULT): 42 Combinations of hook option flags:
43 If stdout is a tty, can prompt about running hooks if needed. 43 - no-verify=False, verify=False (DEFAULT):
44 If user denies running hooks, the action is cancelled. If stdout is 44 If stdout is a tty, can prompt about running hooks if needed.
45 not a tty and we would need to prompt about hooks, action is 45 If user denies running hooks, the action is cancelled. If stdout is
46 cancelled. 46 not a tty and we would need to prompt about hooks, action is
47 - no-verify=False, verify=True: 47 cancelled.
48 Always run hooks with no prompt. 48 - no-verify=False, verify=True:
49 - no-verify=True, verify=False: 49 Always run hooks with no prompt.
50 Never run hooks, but run action anyway (AKA bypass hooks). 50 - no-verify=True, verify=False:
51 - no-verify=True, verify=True: 51 Never run hooks, but run action anyway (AKA bypass hooks).
52 Invalid 52 - no-verify=True, verify=True:
53 """ 53 Invalid
54
55 def __init__(self,
56 hook_type,
57 hooks_project,
58 repo_topdir,
59 manifest_url,
60 bypass_hooks=False,
61 allow_all_hooks=False,
62 ignore_hooks=False,
63 abort_if_user_denies=False):
64 """RepoHook constructor.
65
66 Params:
67 hook_type: A string representing the type of hook. This is also used
68 to figure out the name of the file containing the hook. For
69 example: 'pre-upload'.
70 hooks_project: The project containing the repo hooks.
71 If you have a manifest, this is manifest.repo_hooks_project.
72 OK if this is None, which will make the hook a no-op.
73 repo_topdir: The top directory of the repo client checkout.
74 This is the one containing the .repo directory. Scripts will
75 run with CWD as this directory.
76 If you have a manifest, this is manifest.topdir.
77 manifest_url: The URL to the manifest git repo.
78 bypass_hooks: If True, then 'Do not run the hook'.
79 allow_all_hooks: If True, then 'Run the hook without prompting'.
80 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
81 abort_if_user_denies: If True, we'll abort running the hook if the user
82 doesn't allow us to run the hook.
83 """ 54 """
84 self._hook_type = hook_type
85 self._hooks_project = hooks_project
86 self._repo_topdir = repo_topdir
87 self._manifest_url = manifest_url
88 self._bypass_hooks = bypass_hooks
89 self._allow_all_hooks = allow_all_hooks
90 self._ignore_hooks = ignore_hooks
91 self._abort_if_user_denies = abort_if_user_denies
92
93 # Store the full path to the script for convenience.
94 if self._hooks_project:
95 self._script_fullpath = os.path.join(self._hooks_project.worktree,
96 self._hook_type + '.py')
97 else:
98 self._script_fullpath = None
99
100 def _GetHash(self):
101 """Return a hash of the contents of the hooks directory.
102
103 We'll just use git to do this. This hash has the property that if anything
104 changes in the directory we will return a different has.
105
106 SECURITY CONSIDERATION:
107 This hash only represents the contents of files in the hook directory, not
108 any other files imported or called by hooks. Changes to imported files
109 can change the script behavior without affecting the hash.
110
111 Returns:
112 A string representing the hash. This will always be ASCII so that it can
113 be printed to the user easily.
114 """
115 assert self._hooks_project, "Must have hooks to calculate their hash."
116
117 # We will use the work_git object rather than just calling GetRevisionId().
118 # That gives us a hash of the latest checked in version of the files that
119 # the user will actually be executing. Specifically, GetRevisionId()
120 # doesn't appear to change even if a user checks out a different version
121 # of the hooks repo (via git checkout) nor if a user commits their own revs.
122 #
123 # NOTE: Local (non-committed) changes will not be factored into this hash.
124 # I think this is OK, since we're really only worried about warning the user
125 # about upstream changes.
126 return self._hooks_project.work_git.rev_parse(HEAD)
127
128 def _GetMustVerb(self):
129 """Return 'must' if the hook is required; 'should' if not."""
130 if self._abort_if_user_denies:
131 return 'must'
132 else:
133 return 'should'
134
135 def _CheckForHookApproval(self):
136 """Check to see whether this hook has been approved.
137
138 We'll accept approval of manifest URLs if they're using secure transports.
139 This way the user can say they trust the manifest hoster. For insecure
140 hosts, we fall back to checking the hash of the hooks repo.
141
142 Note that we ask permission for each individual hook even though we use
143 the hash of all hooks when detecting changes. We'd like the user to be
144 able to approve / deny each hook individually. We only use the hash of all
145 hooks because there is no other easy way to detect changes to local imports.
146
147 Returns:
148 True if this hook is approved to run; False otherwise.
149
150 Raises:
151 HookError: Raised if the user doesn't approve and abort_if_user_denies
152 was passed to the consturctor.
153 """
154 if self._ManifestUrlHasSecureScheme():
155 return self._CheckForHookApprovalManifest()
156 else:
157 return self._CheckForHookApprovalHash()
158
159 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
160 changed_prompt):
161 """Check for approval for a particular attribute and hook.
162
163 Args:
164 subkey: The git config key under [repo.hooks.<hook_type>] to store the
165 last approved string.
166 new_val: The new value to compare against the last approved one.
167 main_prompt: Message to display to the user to ask for approval.
168 changed_prompt: Message explaining why we're re-asking for approval.
169
170 Returns:
171 True if this hook is approved to run; False otherwise.
172
173 Raises:
174 HookError: Raised if the user doesn't approve and abort_if_user_denies
175 was passed to the consturctor.
176 """
177 hooks_config = self._hooks_project.config
178 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
179
180 # Get the last value that the user approved for this hook; may be None.
181 old_val = hooks_config.GetString(git_approval_key)
182
183 if old_val is not None:
184 # User previously approved hook and asked not to be prompted again.
185 if new_val == old_val:
186 # Approval matched. We're done.
187 return True
188 else:
189 # Give the user a reason why we're prompting, since they last told
190 # us to "never ask again".
191 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
192 else:
193 prompt = ''
194
195 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
196 if sys.stdout.isatty():
197 prompt += main_prompt + ' (yes/always/NO)? '
198 response = input(prompt).lower()
199 print()
200
201 # User is doing a one-time approval.
202 if response in ('y', 'yes'):
203 return True
204 elif response == 'always':
205 hooks_config.SetString(git_approval_key, new_val)
206 return True
207
208 # For anything else, we'll assume no approval.
209 if self._abort_if_user_denies:
210 raise HookError('You must allow the %s hook or use --no-verify.' %
211 self._hook_type)
212
213 return False
214
215 def _ManifestUrlHasSecureScheme(self):
216 """Check if the URI for the manifest is a secure transport."""
217 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
218 parse_results = urllib.parse.urlparse(self._manifest_url)
219 return parse_results.scheme in secure_schemes
220
221 def _CheckForHookApprovalManifest(self):
222 """Check whether the user has approved this manifest host.
223
224 Returns:
225 True if this hook is approved to run; False otherwise.
226 """
227 return self._CheckForHookApprovalHelper(
228 'approvedmanifest',
229 self._manifest_url,
230 'Run hook scripts from %s' % (self._manifest_url,),
231 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
232
233 def _CheckForHookApprovalHash(self):
234 """Check whether the user has approved the hooks repo.
235
236 Returns:
237 True if this hook is approved to run; False otherwise.
238 """
239 prompt = ('Repo %s run the script:\n'
240 ' %s\n'
241 '\n'
242 'Do you want to allow this script to run')
243 return self._CheckForHookApprovalHelper(
244 'approvedhash',
245 self._GetHash(),
246 prompt % (self._GetMustVerb(), self._script_fullpath),
247 'Scripts have changed since %s was allowed.' % (self._hook_type,))
248
249 @staticmethod
250 def _ExtractInterpFromShebang(data):
251 """Extract the interpreter used in the shebang.
252
253 Try to locate the interpreter the script is using (ignoring `env`).
254
255 Args:
256 data: The file content of the script.
257
258 Returns:
259 The basename of the main script interpreter, or None if a shebang is not
260 used or could not be parsed out.
261 """
262 firstline = data.splitlines()[:1]
263 if not firstline:
264 return None
265
266 # The format here can be tricky.
267 shebang = firstline[0].strip()
268 m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
269 if not m:
270 return None
271
272 # If the using `env`, find the target program.
273 interp = m.group(1)
274 if os.path.basename(interp) == 'env':
275 interp = m.group(2)
276
277 return interp
278
279 def _ExecuteHookViaReexec(self, interp, context, **kwargs):
280 """Execute the hook script through |interp|.
281 55
282 Note: Support for this feature should be dropped ~Jun 2021. 56 def __init__(
283 57 self,
284 Args: 58 hook_type,
285 interp: The Python program to run. 59 hooks_project,
286 context: Basic Python context to execute the hook inside. 60 repo_topdir,
287 kwargs: Arbitrary arguments to pass to the hook script. 61 manifest_url,
288 62 bypass_hooks=False,
289 Raises: 63 allow_all_hooks=False,
290 HookError: When the hooks failed for any reason. 64 ignore_hooks=False,
291 """ 65 abort_if_user_denies=False,
292 # This logic needs to be kept in sync with _ExecuteHookViaImport below. 66 ):
293 script = """ 67 """RepoHook constructor.
68
69 Params:
70 hook_type: A string representing the type of hook. This is also used
71 to figure out the name of the file containing the hook. For
72 example: 'pre-upload'.
73 hooks_project: The project containing the repo hooks.
74 If you have a manifest, this is manifest.repo_hooks_project.
75 OK if this is None, which will make the hook a no-op.
76 repo_topdir: The top directory of the repo client checkout.
77 This is the one containing the .repo directory. Scripts will
78 run with CWD as this directory.
79 If you have a manifest, this is manifest.topdir.
80 manifest_url: The URL to the manifest git repo.
81 bypass_hooks: If True, then 'Do not run the hook'.
82 allow_all_hooks: If True, then 'Run the hook without prompting'.
83 ignore_hooks: If True, then 'Do not abort action if hooks fail'.
84 abort_if_user_denies: If True, we'll abort running the hook if the
85 user doesn't allow us to run the hook.
86 """
87 self._hook_type = hook_type
88 self._hooks_project = hooks_project
89 self._repo_topdir = repo_topdir
90 self._manifest_url = manifest_url
91 self._bypass_hooks = bypass_hooks
92 self._allow_all_hooks = allow_all_hooks
93 self._ignore_hooks = ignore_hooks
94 self._abort_if_user_denies = abort_if_user_denies
95
96 # Store the full path to the script for convenience.
97 if self._hooks_project:
98 self._script_fullpath = os.path.join(
99 self._hooks_project.worktree, self._hook_type + ".py"
100 )
101 else:
102 self._script_fullpath = None
103
104 def _GetHash(self):
105 """Return a hash of the contents of the hooks directory.
106
107 We'll just use git to do this. This hash has the property that if
108 anything changes in the directory we will return a different has.
109
110 SECURITY CONSIDERATION:
111 This hash only represents the contents of files in the hook
112 directory, not any other files imported or called by hooks. Changes
113 to imported files can change the script behavior without affecting
114 the hash.
115
116 Returns:
117 A string representing the hash. This will always be ASCII so that
118 it can be printed to the user easily.
119 """
120 assert self._hooks_project, "Must have hooks to calculate their hash."
121
122 # We will use the work_git object rather than just calling
123 # GetRevisionId(). That gives us a hash of the latest checked in version
124 # of the files that the user will actually be executing. Specifically,
125 # GetRevisionId() doesn't appear to change even if a user checks out a
126 # different version of the hooks repo (via git checkout) nor if a user
127 # commits their own revs.
128 #
129 # NOTE: Local (non-committed) changes will not be factored into this
130 # hash. I think this is OK, since we're really only worried about
131 # warning the user about upstream changes.
132 return self._hooks_project.work_git.rev_parse(HEAD)
133
134 def _GetMustVerb(self):
135 """Return 'must' if the hook is required; 'should' if not."""
136 if self._abort_if_user_denies:
137 return "must"
138 else:
139 return "should"
140
141 def _CheckForHookApproval(self):
142 """Check to see whether this hook has been approved.
143
144 We'll accept approval of manifest URLs if they're using secure
145 transports. This way the user can say they trust the manifest hoster.
146 For insecure hosts, we fall back to checking the hash of the hooks repo.
147
148 Note that we ask permission for each individual hook even though we use
149 the hash of all hooks when detecting changes. We'd like the user to be
150 able to approve / deny each hook individually. We only use the hash of
151 all hooks because there is no other easy way to detect changes to local
152 imports.
153
154 Returns:
155 True if this hook is approved to run; False otherwise.
156
157 Raises:
158 HookError: Raised if the user doesn't approve and
159 abort_if_user_denies was passed to the consturctor.
160 """
161 if self._ManifestUrlHasSecureScheme():
162 return self._CheckForHookApprovalManifest()
163 else:
164 return self._CheckForHookApprovalHash()
165
166 def _CheckForHookApprovalHelper(
167 self, subkey, new_val, main_prompt, changed_prompt
168 ):
169 """Check for approval for a particular attribute and hook.
170
171 Args:
172 subkey: The git config key under [repo.hooks.<hook_type>] to store
173 the last approved string.
174 new_val: The new value to compare against the last approved one.
175 main_prompt: Message to display to the user to ask for approval.
176 changed_prompt: Message explaining why we're re-asking for approval.
177
178 Returns:
179 True if this hook is approved to run; False otherwise.
180
181 Raises:
182 HookError: Raised if the user doesn't approve and
183 abort_if_user_denies was passed to the consturctor.
184 """
185 hooks_config = self._hooks_project.config
186 git_approval_key = "repo.hooks.%s.%s" % (self._hook_type, subkey)
187
188 # Get the last value that the user approved for this hook; may be None.
189 old_val = hooks_config.GetString(git_approval_key)
190
191 if old_val is not None:
192 # User previously approved hook and asked not to be prompted again.
193 if new_val == old_val:
194 # Approval matched. We're done.
195 return True
196 else:
197 # Give the user a reason why we're prompting, since they last
198 # told us to "never ask again".
199 prompt = "WARNING: %s\n\n" % (changed_prompt,)
200 else:
201 prompt = ""
202
203 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
204 if sys.stdout.isatty():
205 prompt += main_prompt + " (yes/always/NO)? "
206 response = input(prompt).lower()
207 print()
208
209 # User is doing a one-time approval.
210 if response in ("y", "yes"):
211 return True
212 elif response == "always":
213 hooks_config.SetString(git_approval_key, new_val)
214 return True
215
216 # For anything else, we'll assume no approval.
217 if self._abort_if_user_denies:
218 raise HookError(
219 "You must allow the %s hook or use --no-verify."
220 % self._hook_type
221 )
222
223 return False
224
225 def _ManifestUrlHasSecureScheme(self):
226 """Check if the URI for the manifest is a secure transport."""
227 secure_schemes = (
228 "file",
229 "https",
230 "ssh",
231 "persistent-https",
232 "sso",
233 "rpc",
234 )
235 parse_results = urllib.parse.urlparse(self._manifest_url)
236 return parse_results.scheme in secure_schemes
237
238 def _CheckForHookApprovalManifest(self):
239 """Check whether the user has approved this manifest host.
240
241 Returns:
242 True if this hook is approved to run; False otherwise.
243 """
244 return self._CheckForHookApprovalHelper(
245 "approvedmanifest",
246 self._manifest_url,
247 "Run hook scripts from %s" % (self._manifest_url,),
248 "Manifest URL has changed since %s was allowed."
249 % (self._hook_type,),
250 )
251
252 def _CheckForHookApprovalHash(self):
253 """Check whether the user has approved the hooks repo.
254
255 Returns:
256 True if this hook is approved to run; False otherwise.
257 """
258 prompt = (
259 "Repo %s run the script:\n"
260 " %s\n"
261 "\n"
262 "Do you want to allow this script to run"
263 )
264 return self._CheckForHookApprovalHelper(
265 "approvedhash",
266 self._GetHash(),
267 prompt % (self._GetMustVerb(), self._script_fullpath),
268 "Scripts have changed since %s was allowed." % (self._hook_type,),
269 )
270
271 @staticmethod
272 def _ExtractInterpFromShebang(data):
273 """Extract the interpreter used in the shebang.
274
275 Try to locate the interpreter the script is using (ignoring `env`).
276
277 Args:
278 data: The file content of the script.
279
280 Returns:
281 The basename of the main script interpreter, or None if a shebang is
282 not used or could not be parsed out.
283 """
284 firstline = data.splitlines()[:1]
285 if not firstline:
286 return None
287
288 # The format here can be tricky.
289 shebang = firstline[0].strip()
290 m = re.match(r"^#!\s*([^\s]+)(?:\s+([^\s]+))?", shebang)
291 if not m:
292 return None
293
294 # If the using `env`, find the target program.
295 interp = m.group(1)
296 if os.path.basename(interp) == "env":
297 interp = m.group(2)
298
299 return interp
300
301 def _ExecuteHookViaReexec(self, interp, context, **kwargs):
302 """Execute the hook script through |interp|.
303
304 Note: Support for this feature should be dropped ~Jun 2021.
305
306 Args:
307 interp: The Python program to run.
308 context: Basic Python context to execute the hook inside.
309 kwargs: Arbitrary arguments to pass to the hook script.
310
311 Raises:
312 HookError: When the hooks failed for any reason.
313 """
314 # This logic needs to be kept in sync with _ExecuteHookViaImport below.
315 script = """
294import json, os, sys 316import json, os, sys
295path = '''%(path)s''' 317path = '''%(path)s'''
296kwargs = json.loads('''%(kwargs)s''') 318kwargs = json.loads('''%(kwargs)s''')
@@ -300,210 +322,240 @@ data = open(path).read()
300exec(compile(data, path, 'exec'), context) 322exec(compile(data, path, 'exec'), context)
301context['main'](**kwargs) 323context['main'](**kwargs)
302""" % { 324""" % {
303 'path': self._script_fullpath, 325 "path": self._script_fullpath,
304 'kwargs': json.dumps(kwargs), 326 "kwargs": json.dumps(kwargs),
305 'context': json.dumps(context), 327 "context": json.dumps(context),
306 } 328 }
307 329
308 # We pass the script via stdin to avoid OS argv limits. It also makes 330 # We pass the script via stdin to avoid OS argv limits. It also makes
309 # unhandled exception tracebacks less verbose/confusing for users. 331 # unhandled exception tracebacks less verbose/confusing for users.
310 cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())'] 332 cmd = [interp, "-c", "import sys; exec(sys.stdin.read())"]
311 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) 333 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
312 proc.communicate(input=script.encode('utf-8')) 334 proc.communicate(input=script.encode("utf-8"))
313 if proc.returncode: 335 if proc.returncode:
314 raise HookError('Failed to run %s hook.' % (self._hook_type,)) 336 raise HookError("Failed to run %s hook." % (self._hook_type,))
315 337
316 def _ExecuteHookViaImport(self, data, context, **kwargs): 338 def _ExecuteHookViaImport(self, data, context, **kwargs):
317 """Execute the hook code in |data| directly. 339 """Execute the hook code in |data| directly.
318 340
319 Args: 341 Args:
320 data: The code of the hook to execute. 342 data: The code of the hook to execute.
321 context: Basic Python context to execute the hook inside. 343 context: Basic Python context to execute the hook inside.
322 kwargs: Arbitrary arguments to pass to the hook script. 344 kwargs: Arbitrary arguments to pass to the hook script.
323 345
324 Raises: 346 Raises:
325 HookError: When the hooks failed for any reason. 347 HookError: When the hooks failed for any reason.
326 """ 348 """
327 # Exec, storing global context in the context dict. We catch exceptions 349 # Exec, storing global context in the context dict. We catch exceptions
328 # and convert to a HookError w/ just the failing traceback. 350 # and convert to a HookError w/ just the failing traceback.
329 try: 351 try:
330 exec(compile(data, self._script_fullpath, 'exec'), context) 352 exec(compile(data, self._script_fullpath, "exec"), context)
331 except Exception: 353 except Exception:
332 raise HookError('%s\nFailed to import %s hook; see traceback above.' % 354 raise HookError(
333 (traceback.format_exc(), self._hook_type)) 355 "%s\nFailed to import %s hook; see traceback above."
334 356 % (traceback.format_exc(), self._hook_type)
335 # Running the script should have defined a main() function. 357 )
336 if 'main' not in context: 358
337 raise HookError('Missing main() in: "%s"' % self._script_fullpath) 359 # Running the script should have defined a main() function.
338 360 if "main" not in context:
339 # Call the main function in the hook. If the hook should cause the 361 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
340 # build to fail, it will raise an Exception. We'll catch that convert 362
341 # to a HookError w/ just the failing traceback. 363 # Call the main function in the hook. If the hook should cause the
342 try: 364 # build to fail, it will raise an Exception. We'll catch that convert
343 context['main'](**kwargs) 365 # to a HookError w/ just the failing traceback.
344 except Exception:
345 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
346 'above.' % (traceback.format_exc(), self._hook_type))
347
348 def _ExecuteHook(self, **kwargs):
349 """Actually execute the given hook.
350
351 This will run the hook's 'main' function in our python interpreter.
352
353 Args:
354 kwargs: Keyword arguments to pass to the hook. These are often specific
355 to the hook type. For instance, pre-upload hooks will contain
356 a project_list.
357 """
358 # Keep sys.path and CWD stashed away so that we can always restore them
359 # upon function exit.
360 orig_path = os.getcwd()
361 orig_syspath = sys.path
362
363 try:
364 # Always run hooks with CWD as topdir.
365 os.chdir(self._repo_topdir)
366
367 # Put the hook dir as the first item of sys.path so hooks can do
368 # relative imports. We want to replace the repo dir as [0] so
369 # hooks can't import repo files.
370 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
371
372 # Initial global context for the hook to run within.
373 context = {'__file__': self._script_fullpath}
374
375 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
376 # We don't actually want hooks to define their main with this argument--
377 # it's there to remind them that their hook should always take **kwargs.
378 # For instance, a pre-upload hook should be defined like:
379 # def main(project_list, **kwargs):
380 #
381 # This allows us to later expand the API without breaking old hooks.
382 kwargs = kwargs.copy()
383 kwargs['hook_should_take_kwargs'] = True
384
385 # See what version of python the hook has been written against.
386 data = open(self._script_fullpath).read()
387 interp = self._ExtractInterpFromShebang(data)
388 reexec = False
389 if interp:
390 prog = os.path.basename(interp)
391 if prog.startswith('python2') and sys.version_info.major != 2:
392 reexec = True
393 elif prog.startswith('python3') and sys.version_info.major == 2:
394 reexec = True
395
396 # Attempt to execute the hooks through the requested version of Python.
397 if reexec:
398 try: 366 try:
399 self._ExecuteHookViaReexec(interp, context, **kwargs) 367 context["main"](**kwargs)
400 except OSError as e: 368 except Exception:
401 if e.errno == errno.ENOENT: 369 raise HookError(
402 # We couldn't find the interpreter, so fallback to importing. 370 "%s\nFailed to run main() for %s hook; see traceback "
371 "above." % (traceback.format_exc(), self._hook_type)
372 )
373
374 def _ExecuteHook(self, **kwargs):
375 """Actually execute the given hook.
376
377 This will run the hook's 'main' function in our python interpreter.
378
379 Args:
380 kwargs: Keyword arguments to pass to the hook. These are often
381 specific to the hook type. For instance, pre-upload hooks will
382 contain a project_list.
383 """
384 # Keep sys.path and CWD stashed away so that we can always restore them
385 # upon function exit.
386 orig_path = os.getcwd()
387 orig_syspath = sys.path
388
389 try:
390 # Always run hooks with CWD as topdir.
391 os.chdir(self._repo_topdir)
392
393 # Put the hook dir as the first item of sys.path so hooks can do
394 # relative imports. We want to replace the repo dir as [0] so
395 # hooks can't import repo files.
396 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
397
398 # Initial global context for the hook to run within.
399 context = {"__file__": self._script_fullpath}
400
401 # Add 'hook_should_take_kwargs' to the arguments to be passed to
402 # main. We don't actually want hooks to define their main with this
403 # argument--it's there to remind them that their hook should always
404 # take **kwargs.
405 # For instance, a pre-upload hook should be defined like:
406 # def main(project_list, **kwargs):
407 #
408 # This allows us to later expand the API without breaking old hooks.
409 kwargs = kwargs.copy()
410 kwargs["hook_should_take_kwargs"] = True
411
412 # See what version of python the hook has been written against.
413 data = open(self._script_fullpath).read()
414 interp = self._ExtractInterpFromShebang(data)
403 reexec = False 415 reexec = False
404 else: 416 if interp:
405 raise 417 prog = os.path.basename(interp)
406 418 if prog.startswith("python2") and sys.version_info.major != 2:
407 # Run the hook by importing directly. 419 reexec = True
408 if not reexec: 420 elif prog.startswith("python3") and sys.version_info.major == 2:
409 self._ExecuteHookViaImport(data, context, **kwargs) 421 reexec = True
410 finally: 422
411 # Restore sys.path and CWD. 423 # Attempt to execute the hooks through the requested version of
412 sys.path = orig_syspath 424 # Python.
413 os.chdir(orig_path) 425 if reexec:
414 426 try:
415 def _CheckHook(self): 427 self._ExecuteHookViaReexec(interp, context, **kwargs)
416 # Bail with a nice error if we can't find the hook. 428 except OSError as e:
417 if not os.path.isfile(self._script_fullpath): 429 if e.errno == errno.ENOENT:
418 raise HookError('Couldn\'t find repo hook: %s' % self._script_fullpath) 430 # We couldn't find the interpreter, so fallback to
419 431 # importing.
420 def Run(self, **kwargs): 432 reexec = False
421 """Run the hook. 433 else:
422 434 raise
423 If the hook doesn't exist (because there is no hooks project or because 435
424 this particular hook is not enabled), this is a no-op. 436 # Run the hook by importing directly.
425 437 if not reexec:
426 Args: 438 self._ExecuteHookViaImport(data, context, **kwargs)
427 user_allows_all_hooks: If True, we will never prompt about running the 439 finally:
428 hook--we'll just assume it's OK to run it. 440 # Restore sys.path and CWD.
429 kwargs: Keyword arguments to pass to the hook. These are often specific 441 sys.path = orig_syspath
430 to the hook type. For instance, pre-upload hooks will contain 442 os.chdir(orig_path)
431 a project_list. 443
432 444 def _CheckHook(self):
433 Returns: 445 # Bail with a nice error if we can't find the hook.
434 True: On success or ignore hooks by user-request 446 if not os.path.isfile(self._script_fullpath):
435 False: The hook failed. The caller should respond with aborting the action. 447 raise HookError(
436 Some examples in which False is returned: 448 "Couldn't find repo hook: %s" % self._script_fullpath
437 * Finding the hook failed while it was enabled, or 449 )
438 * the user declined to run a required hook (from _CheckForHookApproval) 450
439 In all these cases the user did not pass the proper arguments to 451 def Run(self, **kwargs):
440 ignore the result through the option combinations as listed in 452 """Run the hook.
441 AddHookOptionGroup(). 453
442 """ 454 If the hook doesn't exist (because there is no hooks project or because
443 # Do not do anything in case bypass_hooks is set, or 455 this particular hook is not enabled), this is a no-op.
444 # no-op if there is no hooks project or if hook is disabled. 456
445 if (self._bypass_hooks or 457 Args:
446 not self._hooks_project or 458 user_allows_all_hooks: If True, we will never prompt about running
447 self._hook_type not in self._hooks_project.enabled_repo_hooks): 459 the hook--we'll just assume it's OK to run it.
448 return True 460 kwargs: Keyword arguments to pass to the hook. These are often
449 461 specific to the hook type. For instance, pre-upload hooks will
450 passed = True 462 contain a project_list.
451 try: 463
452 self._CheckHook() 464 Returns:
453 465 True: On success or ignore hooks by user-request
454 # Make sure the user is OK with running the hook. 466 False: The hook failed. The caller should respond with aborting the
455 if self._allow_all_hooks or self._CheckForHookApproval(): 467 action. Some examples in which False is returned:
456 # Run the hook with the same version of python we're using. 468 * Finding the hook failed while it was enabled, or
457 self._ExecuteHook(**kwargs) 469 * the user declined to run a required hook (from
458 except SystemExit as e: 470 _CheckForHookApproval)
459 passed = False 471 In all these cases the user did not pass the proper arguments to
460 print('ERROR: %s hooks exited with exit code: %s' % (self._hook_type, str(e)), 472 ignore the result through the option combinations as listed in
461 file=sys.stderr) 473 AddHookOptionGroup().
462 except HookError as e: 474 """
463 passed = False 475 # Do not do anything in case bypass_hooks is set, or
464 print('ERROR: %s' % str(e), file=sys.stderr) 476 # no-op if there is no hooks project or if hook is disabled.
465 477 if (
466 if not passed and self._ignore_hooks: 478 self._bypass_hooks
467 print('\nWARNING: %s hooks failed, but continuing anyways.' % self._hook_type, 479 or not self._hooks_project
468 file=sys.stderr) 480 or self._hook_type not in self._hooks_project.enabled_repo_hooks
469 passed = True 481 ):
470 482 return True
471 return passed 483
472 484 passed = True
473 @classmethod 485 try:
474 def FromSubcmd(cls, manifest, opt, *args, **kwargs): 486 self._CheckHook()
475 """Method to construct the repo hook class 487
476 488 # Make sure the user is OK with running the hook.
477 Args: 489 if self._allow_all_hooks or self._CheckForHookApproval():
478 manifest: The current active manifest for this command from which we 490 # Run the hook with the same version of python we're using.
479 extract a couple of fields. 491 self._ExecuteHook(**kwargs)
480 opt: Contains the commandline options for the action of this hook. 492 except SystemExit as e:
481 It should contain the options added by AddHookOptionGroup() in which 493 passed = False
482 we are interested in RepoHook execution. 494 print(
483 """ 495 "ERROR: %s hooks exited with exit code: %s"
484 for key in ('bypass_hooks', 'allow_all_hooks', 'ignore_hooks'): 496 % (self._hook_type, str(e)),
485 kwargs.setdefault(key, getattr(opt, key)) 497 file=sys.stderr,
486 kwargs.update({ 498 )
487 'hooks_project': manifest.repo_hooks_project, 499 except HookError as e:
488 'repo_topdir': manifest.topdir, 500 passed = False
489 'manifest_url': manifest.manifestProject.GetRemote('origin').url, 501 print("ERROR: %s" % str(e), file=sys.stderr)
490 }) 502
491 return cls(*args, **kwargs) 503 if not passed and self._ignore_hooks:
492 504 print(
493 @staticmethod 505 "\nWARNING: %s hooks failed, but continuing anyways."
494 def AddOptionGroup(parser, name): 506 % self._hook_type,
495 """Help options relating to the various hooks.""" 507 file=sys.stderr,
496 508 )
497 # Note that verify and no-verify are NOT opposites of each other, which 509 passed = True
498 # is why they store to different locations. We are using them to match 510
499 # 'git commit' syntax. 511 return passed
500 group = parser.add_option_group(name + ' hooks') 512
501 group.add_option('--no-verify', 513 @classmethod
502 dest='bypass_hooks', action='store_true', 514 def FromSubcmd(cls, manifest, opt, *args, **kwargs):
503 help='Do not run the %s hook.' % name) 515 """Method to construct the repo hook class
504 group.add_option('--verify', 516
505 dest='allow_all_hooks', action='store_true', 517 Args:
506 help='Run the %s hook without prompting.' % name) 518 manifest: The current active manifest for this command from which we
507 group.add_option('--ignore-hooks', 519 extract a couple of fields.
508 action='store_true', 520 opt: Contains the commandline options for the action of this hook.
509 help='Do not abort if %s hooks fail.' % name) 521 It should contain the options added by AddHookOptionGroup() in
522 which we are interested in RepoHook execution.
523 """
524 for key in ("bypass_hooks", "allow_all_hooks", "ignore_hooks"):
525 kwargs.setdefault(key, getattr(opt, key))
526 kwargs.update(
527 {
528 "hooks_project": manifest.repo_hooks_project,
529 "repo_topdir": manifest.topdir,
530 "manifest_url": manifest.manifestProject.GetRemote(
531 "origin"
532 ).url,
533 }
534 )
535 return cls(*args, **kwargs)
536
537 @staticmethod
538 def AddOptionGroup(parser, name):
539 """Help options relating to the various hooks."""
540
541 # Note that verify and no-verify are NOT opposites of each other, which
542 # is why they store to different locations. We are using them to match
543 # 'git commit' syntax.
544 group = parser.add_option_group(name + " hooks")
545 group.add_option(
546 "--no-verify",
547 dest="bypass_hooks",
548 action="store_true",
549 help="Do not run the %s hook." % name,
550 )
551 group.add_option(
552 "--verify",
553 dest="allow_all_hooks",
554 action="store_true",
555 help="Run the %s hook without prompting." % name,
556 )
557 group.add_option(
558 "--ignore-hooks",
559 action="store_true",
560 help="Do not abort if %s hooks fail." % name,
561 )