Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/hooks.py: 56%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# hooks.py -- for dealing with git hooks
2# Copyright (C) 2012-2013 Jelmer Vernooij and others.
3#
4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
5# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
6# General Public License as published by the Free Software Foundation; version 2.0
7# or (at your option) any later version. You can redistribute it and/or
8# modify it under the terms of either of these two licenses.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16# You should have received a copy of the licenses; if not, see
17# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
18# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
19# License, Version 2.0.
20#
22"""Access to hooks."""
24__all__ = [
25 "CommitMsgShellHook",
26 "Hook",
27 "PostCommitShellHook",
28 "PostReceiveShellHook",
29 "PreCommitShellHook",
30 "PreReceiveShellHook",
31 "ShellHook",
32 "UpdateShellHook",
33]
35import os
36import subprocess
37from collections.abc import Callable, Sequence
38from typing import Any
40from .errors import HookError
43class Hook:
44 """Generic hook object."""
46 def execute(self, *args: Any) -> Any: # noqa: ANN401
47 """Execute the hook with the given args.
49 Args:
50 args: argument list to hook
51 Raises:
52 HookError: hook execution failure
53 Returns:
54 a hook may return a useful value
55 """
56 raise NotImplementedError(self.execute)
59class ShellHook(Hook):
60 """Hook by executable file.
62 Implements standard githooks(5) [0]:
64 [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html
65 """
67 def __init__(
68 self,
69 name: str,
70 path: str,
71 numparam: int,
72 pre_exec_callback: Callable[..., Any] | None = None,
73 post_exec_callback: Callable[..., Any] | None = None,
74 cwd: str | None = None,
75 ) -> None:
76 """Setup shell hook definition.
78 Args:
79 name: name of hook for error messages
80 path: absolute path to executable file
81 numparam: number of requirements parameters
82 pre_exec_callback: closure for setup before execution
83 Defaults to None. Takes in the variable argument list from the
84 execute functions and returns a modified argument list for the
85 shell hook.
86 post_exec_callback: closure for cleanup after execution
87 Defaults to None. Takes in a boolean for hook success and the
88 modified argument list and returns the final hook return value
89 if applicable
90 cwd: working directory to switch to when executing the hook
91 """
92 self.name = name
93 self.filepath = path
94 self.numparam = numparam
96 self.pre_exec_callback = pre_exec_callback
97 self.post_exec_callback = post_exec_callback
99 self.cwd = cwd
101 def execute(self, *args: Any) -> Any: # noqa: ANN401
102 """Execute the hook with given args."""
103 if len(args) != self.numparam:
104 raise HookError(
105 f"Hook {self.name} executed with wrong number of args. Expected {self.numparam}. Saw {len(args)}. args: {args}"
106 )
108 if self.pre_exec_callback is not None:
109 args = self.pre_exec_callback(*args)
111 try:
112 ret = subprocess.call(
113 [os.path.relpath(self.filepath, self.cwd), *list(args)], cwd=self.cwd
114 )
115 if ret != 0:
116 if self.post_exec_callback is not None:
117 self.post_exec_callback(0, *args)
118 raise HookError(f"Hook {self.name} exited with non-zero status {ret}")
119 if self.post_exec_callback is not None:
120 return self.post_exec_callback(1, *args)
121 except FileNotFoundError: # no file. silent failure.
122 if self.post_exec_callback is not None:
123 self.post_exec_callback(0, *args)
126class PreCommitShellHook(ShellHook):
127 """pre-commit shell hook."""
129 def __init__(self, cwd: str, controldir: str) -> None:
130 """Initialize pre-commit hook.
132 Args:
133 cwd: Working directory for hook execution
134 controldir: Path to the git control directory (.git)
135 """
136 filepath = os.path.join(controldir, "hooks", "pre-commit")
138 ShellHook.__init__(self, "pre-commit", filepath, 0, cwd=cwd)
141class PostCommitShellHook(ShellHook):
142 """post-commit shell hook."""
144 def __init__(self, controldir: str) -> None:
145 """Initialize post-commit hook.
147 Args:
148 controldir: Path to the git control directory (.git)
149 """
150 filepath = os.path.join(controldir, "hooks", "post-commit")
152 ShellHook.__init__(self, "post-commit", filepath, 0, cwd=controldir)
155class CommitMsgShellHook(ShellHook):
156 """commit-msg shell hook."""
158 def __init__(self, controldir: str) -> None:
159 """Initialize commit-msg hook.
161 Args:
162 controldir: Path to the git control directory (.git)
163 """
164 filepath = os.path.join(controldir, "hooks", "commit-msg")
166 def prepare_msg(*args: bytes) -> tuple[str, ...]:
167 import tempfile
169 (fd, path) = tempfile.mkstemp()
171 with os.fdopen(fd, "wb") as f:
172 f.write(args[0])
174 return (path,)
176 def clean_msg(success: int, *args: str) -> bytes | None:
177 if success:
178 with open(args[0], "rb") as f:
179 new_msg = f.read()
180 os.unlink(args[0])
181 return new_msg
182 os.unlink(args[0])
183 return None
185 ShellHook.__init__(
186 self, "commit-msg", filepath, 1, prepare_msg, clean_msg, controldir
187 )
190class PostReceiveShellHook(ShellHook):
191 """post-receive shell hook."""
193 def __init__(self, controldir: str) -> None:
194 """Initialize post-receive hook.
196 Args:
197 controldir: Path to the git control directory (.git)
198 """
199 self.controldir = controldir
200 filepath = os.path.join(controldir, "hooks", "post-receive")
201 ShellHook.__init__(self, "post-receive", path=filepath, numparam=0)
203 def execute(
204 self, client_refs: Sequence[tuple[bytes, bytes, bytes]]
205 ) -> bytes | None:
206 """Execute the post-receive hook.
208 Args:
209 client_refs: List of tuples containing (old_sha, new_sha, ref_name)
210 for each updated reference
212 Returns:
213 Output from the hook execution or None if hook doesn't exist
215 Raises:
216 HookError: If hook execution fails
217 """
218 # do nothing if the script doesn't exist
219 if not os.path.exists(self.filepath):
220 return None
222 try:
223 env = os.environ.copy()
224 env["GIT_DIR"] = self.controldir
226 p = subprocess.Popen(
227 self.filepath,
228 stdin=subprocess.PIPE,
229 stdout=subprocess.PIPE,
230 stderr=subprocess.PIPE,
231 env=env,
232 )
234 # client_refs is a list of (oldsha, newsha, ref)
235 in_data = b"\n".join([b" ".join(ref) for ref in client_refs])
237 out_data, err_data = p.communicate(in_data)
239 if (p.returncode != 0) or err_data:
240 err_msg = (
241 f"post-receive exit code: {p.returncode}\n"
242 f"stdout:\n{out_data.decode('utf-8', 'backslashreplace')}\n"
243 f"stderr:\n{err_data.decode('utf-8', 'backslashreplace')}"
244 )
245 raise HookError(err_msg)
246 return out_data
247 except OSError as err:
248 raise HookError(repr(err)) from err
251class PreReceiveShellHook(ShellHook):
252 """pre-receive shell hook."""
254 def __init__(self, controldir: str) -> None:
255 """Initialize pre-receive hook.
257 Args:
258 controldir: Path to the git control directory (.git)
259 """
260 self.controldir = controldir
261 filepath = os.path.join(controldir, "hooks", "pre-receive")
262 ShellHook.__init__(self, "pre-receive", path=filepath, numparam=0)
264 def execute(
265 self, client_refs: Sequence[tuple[bytes, bytes, bytes]]
266 ) -> tuple[bytes, bytes]:
267 """Execute the pre-receive hook.
269 Args:
270 client_refs: List of tuples containing (old_sha, new_sha, ref_name)
271 for each reference to be updated
273 Returns:
274 Tuple of (stdout, stderr) from hook execution
276 Raises:
277 HookError: If hook execution fails (exits with non-zero status)
278 """
279 # do nothing if the script doesn't exist
280 if not os.path.exists(self.filepath):
281 return (b"", b"")
283 try:
284 env = os.environ.copy()
285 env["GIT_DIR"] = self.controldir
287 p = subprocess.Popen(
288 self.filepath,
289 stdin=subprocess.PIPE,
290 stdout=subprocess.PIPE,
291 stderr=subprocess.PIPE,
292 env=env,
293 )
295 # client_refs is a list of (oldsha, newsha, ref)
296 in_data = b"\n".join([b" ".join(ref) for ref in client_refs])
298 out_data, err_data = p.communicate(in_data)
300 if p.returncode != 0:
301 raise HookError(
302 f"pre-receive hook exited with status {p.returncode}: {err_data.decode('utf-8', 'backslashreplace')}"
303 )
304 return (out_data, err_data)
305 except OSError as err:
306 raise HookError(repr(err)) from err
309class UpdateShellHook(ShellHook):
310 """update shell hook."""
312 def __init__(self, controldir: str) -> None:
313 """Initialize update hook.
315 Args:
316 controldir: Path to the git control directory (.git)
317 """
318 self.controldir = controldir
319 filepath = os.path.join(controldir, "hooks", "update")
320 ShellHook.__init__(self, "update", path=filepath, numparam=3)
322 def execute(
323 self, ref_name: bytes, old_sha: bytes, new_sha: bytes
324 ) -> tuple[bytes, bytes]:
325 """Execute the update hook for a single ref.
327 Args:
328 ref_name: Name of the reference being updated
329 old_sha: Old SHA of the reference
330 new_sha: New SHA of the reference
332 Returns:
333 Tuple of (stdout, stderr) from hook execution
335 Raises:
336 HookError: If hook execution fails (exits with non-zero status)
337 """
338 # do nothing if the script doesn't exist
339 if not os.path.exists(self.filepath):
340 return (b"", b"")
342 try:
343 env = os.environ.copy()
344 env["GIT_DIR"] = self.controldir
346 p = subprocess.Popen(
347 [
348 self.filepath,
349 ref_name.decode("utf-8", "backslashreplace"),
350 old_sha.decode("utf-8", "backslashreplace"),
351 new_sha.decode("utf-8", "backslashreplace"),
352 ],
353 stdout=subprocess.PIPE,
354 stderr=subprocess.PIPE,
355 env=env,
356 )
358 out_data, err_data = p.communicate()
360 if p.returncode != 0:
361 raise HookError(
362 f"update hook exited with status {p.returncode}: {err_data.decode('utf-8', 'backslashreplace')}"
363 )
364 return (out_data, err_data)
365 except OSError as err:
366 raise HookError(repr(err)) from err