pygittools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Will Bowers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: pygittools
3
+ Version: 0.1.0
4
+ Summary: Hooks and task storage in Git
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pygit2>=1.12.0
10
+ Dynamic: license-file
11
+
12
+ # PyGitTools
13
+
14
+ Authentication, hooks, and pygit2 workflows used by PyGitWeb. Can be used headless for many purposes.
15
+
16
+ ## Hook framework
17
+
18
+ `pygittools.hooks` provides a thin `Hook` base class plus one subclass per git hook type
19
+ (client- and server-side). Receive-pack hooks (`PreReceive`, `PostReceive`) parse their
20
+ ref-update lines via `parse_ref_updates(stdin)`.
21
+
22
+ `pygittools.hooks_notify` adds `PreReceiveNotify` / `PostReceiveNotify`: minimal hook
23
+ variants that POST `?project=<name>` to a pygitweb `/_internal/notify` endpoint so its
24
+ long-poll `updates=true` subscribers wake up immediately on each push. Notification
25
+ failures never reject the push.
26
+
27
+ See `hook_samples/` for ready-to-use scripts.
@@ -0,0 +1,16 @@
1
+ # PyGitTools
2
+
3
+ Authentication, hooks, and pygit2 workflows used by PyGitWeb. Can be used headless for many purposes.
4
+
5
+ ## Hook framework
6
+
7
+ `pygittools.hooks` provides a thin `Hook` base class plus one subclass per git hook type
8
+ (client- and server-side). Receive-pack hooks (`PreReceive`, `PostReceive`) parse their
9
+ ref-update lines via `parse_ref_updates(stdin)`.
10
+
11
+ `pygittools.hooks_notify` adds `PreReceiveNotify` / `PostReceiveNotify`: minimal hook
12
+ variants that POST `?project=<name>` to a pygitweb `/_internal/notify` endpoint so its
13
+ long-poll `updates=true` subscribers wake up immediately on each push. Notification
14
+ failures never reject the push.
15
+
16
+ See `hook_samples/` for ready-to-use scripts.
@@ -0,0 +1,10 @@
1
+ import pygittools.__meta__
2
+
3
+ """
4
+ Python hooks and metadata storage in Git
5
+ """
6
+ __version__ = pygittools.__meta__.__version__
7
+ __author__ = pygittools.__meta__.__author__
8
+ __description__ = pygittools.__meta__.__description__
9
+ __url__ = pygittools.__meta__.__url__
10
+ __license__ = pygittools.__meta__.__license__
@@ -0,0 +1,9 @@
1
+ """
2
+ Metadata for PyGitTools - this is the canonical source of all information below.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Will Bowers"
7
+ __license__ = "MIT"
8
+ __description__ = "Python hooks and metadata storage in Git"
9
+ __url__ = "https://pygitweb.com"
@@ -0,0 +1,44 @@
1
+ # Sample Hooks
2
+
3
+ Place these as desired in `.git/hooks/` WITHOUT the suffix.
4
+
5
+ You will just need UV with pygittools in your virtual environment.
6
+
7
+ ## post-receive.notify and post-commit.notify
8
+
9
+ Wake pygitweb's long-poll subscribers (`updates=true`) on every push (server-side) and
10
+ every local commit (client-side). They share two environment variables:
11
+
12
+ - `PYGITWEB_NOTIFY_URL` — the running server's notify endpoint (default
13
+ `http://127.0.0.1:8000/_internal/notify`)
14
+ - `PYGITWEB_PROJECT` — project path as known to pygitweb (default: the bare repo's
15
+ directory name with `.git` stripped, or for non-bare clones the working-tree directory
16
+ name)
17
+
18
+ The pygitweb summary page can install both at once via the **Hooks** row (the `update`
19
+ bundle installs `post-commit.notify` and `post-receive.notify` together).
20
+
21
+ ## pre-receive.protected-pattern
22
+
23
+ Reject pushes to protected refs when any newly introduced non-merge commit message does
24
+ not match a pattern. Merge commits are skipped by default; set `PYGITWEB_REJECT_MERGE_COMMITS=1`
25
+ to refuse merge commits and require squash or rebase instead.
26
+
27
+ Environment variables:
28
+
29
+ - `PYGITWEB_PROTECTED_REF_PATTERN` — refs to protect (default `^refs/heads/(main|master)$`)
30
+ - `PYGITWEB_COMMIT_MSG_PATTERN` — message regex (default `^(\S+): (.+)`)
31
+ - `PYGITWEB_REJECT_MERGE_COMMITS` — set to `1`/`true`/`yes` to reject merge commits
32
+
33
+ ## post-commit.push-remotes
34
+
35
+ After each local commit, force-with-lease push the current branch to every configured
36
+ remote when the branch name matches a pattern. Uses the git CLI (`git push
37
+ --force-with-lease`), so SSH remotes honor `~/.ssh/config` like a normal push. Push
38
+ failures are logged on stderr but do not affect the commit (post-commit hooks cannot undo
39
+ a commit).
40
+
41
+ Environment variables:
42
+
43
+ - `PYGITWEB_PUSH_BRANCH_PATTERN` — branch name regex (default `^wip/`)
44
+ - `GIT` — git executable (default: `git` on `PATH`)
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import sys
8
+ import pygit2
9
+ from pygittools.hooks import HookResult
10
+ from pygittools.hooks_patterns import CommitMsgPattern
11
+
12
+ PATTERN = r"^(\S+): (.+)"
13
+
14
+
15
+ def main() -> int:
16
+ repo = pygit2.Repository(".")
17
+ result = CommitMsgPattern(repo, PATTERN).run(sys.argv[1])
18
+ if result.value == HookResult.FAILURE:
19
+ print("Commit message does not match pattern:", PATTERN, file=sys.stderr)
20
+ return int(result.value)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ raise SystemExit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import os
8
+
9
+ import pygit2
10
+
11
+ from pygittools.hooks_notify import PostCommitNotify
12
+
13
+ NOTIFY_URL = os.environ.get("PYGITWEB_NOTIFY_URL", "http://127.0.0.1:8000/_internal/notify")
14
+ PROJECT = os.environ.get("PYGITWEB_PROJECT") or os.path.basename(os.getcwd()).removesuffix(".git")
15
+
16
+
17
+ def main() -> int:
18
+ repo = pygit2.Repository(".")
19
+ result = PostCommitNotify(repo, NOTIFY_URL, PROJECT).run()
20
+ return int(result.value)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ raise SystemExit(main())
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import os
8
+
9
+ import pygit2
10
+
11
+ from pygittools.hooks_push import PostCommitPushRemotes
12
+
13
+ BRANCH_PATTERN = os.environ.get("PYGITWEB_PUSH_BRANCH_PATTERN", r"^wip/")
14
+
15
+
16
+ def main() -> int:
17
+ repo = pygit2.Repository(".")
18
+ result = PostCommitPushRemotes(repo, BRANCH_PATTERN).run()
19
+ return int(result.value)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ raise SystemExit(main())
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import os
8
+ import sys
9
+
10
+ import pygit2
11
+
12
+ from pygittools.hooks import parse_ref_updates
13
+ from pygittools.hooks_notify import PostReceiveNotify
14
+
15
+ NOTIFY_URL = os.environ.get("PYGITWEB_NOTIFY_URL", "http://127.0.0.1:8000/_internal/notify")
16
+ PROJECT = os.environ.get("PYGITWEB_PROJECT") or os.path.basename(os.getcwd()).removesuffix(".git")
17
+
18
+
19
+ def main() -> int:
20
+ repo = pygit2.Repository(".")
21
+ updates = parse_ref_updates(sys.stdin)
22
+ result = PostReceiveNotify(repo, NOTIFY_URL, PROJECT).run(updates)
23
+ return int(result.value)
24
+
25
+
26
+ if __name__ == "__main__":
27
+ raise SystemExit(main())
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import pygit2
8
+ from pygittools.hooks_ruff import PreCommitRuff
9
+
10
+
11
+ def main() -> int:
12
+ repo = pygit2.Repository(".")
13
+ result = PreCommitRuff(repo).run()
14
+ return int(result.value)
15
+
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env -S uv run python
2
+
3
+ from __future__ import annotations
4
+
5
+ __VERSION__ = "1"
6
+
7
+ import os
8
+ import sys
9
+
10
+ import pygit2
11
+
12
+ from pygittools.hooks import HookResult, parse_ref_updates
13
+ from pygittools.hooks_patterns import PreReceiveProtectedPattern
14
+
15
+ PROTECTED_REF_PATTERN = os.environ.get("PYGITWEB_PROTECTED_REF_PATTERN", r"^refs/heads/(main|master)$")
16
+ COMMIT_MSG_PATTERN = os.environ.get("PYGITWEB_COMMIT_MSG_PATTERN", r"^(\S+): (.+)")
17
+ REJECT_MERGE_COMMITS = os.environ.get("PYGITWEB_REJECT_MERGE_COMMITS", "").lower() in {
18
+ "1",
19
+ "true",
20
+ "yes",
21
+ }
22
+
23
+
24
+ def main() -> int:
25
+ repo = pygit2.Repository(".")
26
+ updates = parse_ref_updates(sys.stdin)
27
+ result = PreReceiveProtectedPattern(
28
+ repo,
29
+ PROTECTED_REF_PATTERN,
30
+ COMMIT_MSG_PATTERN,
31
+ reject_merge_commits=REJECT_MERGE_COMMITS,
32
+ ).run(updates)
33
+ return int(result.value)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
@@ -0,0 +1,336 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import IO
5
+
6
+ import pygit2
7
+
8
+
9
+ class HookResult(Enum):
10
+ SUCCESS = 0
11
+ FAILURE = 1
12
+
13
+
14
+ @dataclass
15
+ class Hook:
16
+ repo: pygit2.Repository
17
+
18
+
19
+ RefUpdate = tuple[str, str, str]
20
+
21
+
22
+ def parse_ref_updates(stream: IO[str] | None = None) -> list[RefUpdate]:
23
+ """Parse `<old-oid> <new-oid> <ref-name>` lines (one per ref) from a receive-pack hook's stdin."""
24
+ source: IO[str] = stream if stream is not None else sys.stdin
25
+ updates: list[RefUpdate] = []
26
+ for raw in source:
27
+ parts: list[str] = raw.strip().split(maxsplit=2)
28
+ if len(parts) == 3:
29
+ updates.append((parts[0], parts[1], parts[2]))
30
+ return updates
31
+
32
+
33
+ """
34
+ Pre-Commit Hook (Client-side)
35
+ Runs before a commit is made, before a commit message is written (if not supplied by -m).
36
+ Use this hook to:
37
+ - Check for uncommitted changes
38
+ - Run tests, lints, security checks, etc.
39
+ This can be bypassed with --no-verify by the user.
40
+ """
41
+
42
+
43
+ class PreCommit(Hook):
44
+ def __init__(self, repo: pygit2.Repository):
45
+ super().__init__(repo)
46
+
47
+ def run(self) -> HookResult:
48
+ return HookResult.SUCCESS
49
+
50
+
51
+ """
52
+ Prepare-Commit-Msg Hook (Client-side)
53
+ Runs right after the default log message is prepared and before the editor is started.
54
+ Use this hook to:
55
+ - Edit the message file in place (e.g. strip template comments)
56
+ - Insert a standard prefix/suffix (e.g. branch name, ticket ID)
57
+ - Add Signed-off-by from a template
58
+ Takes 1–3 parameters: message file path, source (message|template|merge|squash|commit),
59
+ and optionally commit hash for amend.
60
+ """
61
+
62
+
63
+ class PrepareCommitMsg(Hook):
64
+ def __init__(self, repo: pygit2.Repository):
65
+ super().__init__(repo)
66
+
67
+ def run(
68
+ self,
69
+ message_file: str,
70
+ source: str = "",
71
+ commit_hash: str | None = None,
72
+ ) -> HookResult:
73
+ return HookResult.SUCCESS
74
+
75
+
76
+ """
77
+ Commit-Msg Hook (Client-side)
78
+ Runs after the commit message is prepared; can be bypassed with --no-verify.
79
+ Use this hook to:
80
+ - Enforce a project standard format (e.g. conventional commits)
81
+ - Validate or normalize the message in place
82
+ - Reject the commit (e.g. duplicate Signed-off-by, missing ticket reference)
83
+ Takes one parameter: the path to the file holding the proposed commit log message.
84
+ """
85
+
86
+
87
+ class CommitMsg(Hook):
88
+ def __init__(self, repo: pygit2.Repository):
89
+ super().__init__(repo)
90
+
91
+ def run(self, message_file: str) -> HookResult:
92
+ return HookResult.SUCCESS
93
+
94
+
95
+ """
96
+ Post-Commit Hook (Client-side)
97
+ Runs after a commit is made. Cannot affect the outcome of git commit.
98
+ Use this hook to:
99
+ - Notify (e.g. log, webhook, chat)
100
+ - Run post-commit checks or backups
101
+ - Update external metadata or caches
102
+ """
103
+
104
+
105
+ class PostCommit(Hook):
106
+ def __init__(self, repo: pygit2.Repository):
107
+ super().__init__(repo)
108
+
109
+ def run(self) -> HookResult:
110
+ return HookResult.SUCCESS
111
+
112
+
113
+ """
114
+ Pre-Merge-Commit Hook (Client-side)
115
+ Runs after a merge has been carried out successfully and before the merge commit
116
+ message is finalized; can be bypassed with --no-verify.
117
+ Use this hook to:
118
+ - Validate the merged tree (e.g. run tests on the result)
119
+ - Inspect or adjust the merge commit message
120
+ - Abort the merge commit if checks fail
121
+ Takes no parameters.
122
+ """
123
+
124
+
125
+ class PreMergeCommit(Hook):
126
+ def __init__(self, repo: pygit2.Repository):
127
+ super().__init__(repo)
128
+
129
+ def run(self) -> HookResult:
130
+ return HookResult.SUCCESS
131
+
132
+
133
+ """
134
+ Pre-Rebase Hook (Client-side)
135
+ Called by git rebase; can be used to prevent a branch from being rebased.
136
+ Use this hook to:
137
+ - Block rebasing certain branches (e.g. main)
138
+ - Run checks before rewriting history
139
+ Takes one or two parameters: upstream ref, and optionally the branch being rebased
140
+ (absent when rebasing the current branch).
141
+ """
142
+
143
+
144
+ class PreRebase(Hook):
145
+ def __init__(self, repo: pygit2.Repository):
146
+ super().__init__(repo)
147
+
148
+ def run(self, upstream: str, branch: str | None = None) -> HookResult:
149
+ return HookResult.SUCCESS
150
+
151
+
152
+ """
153
+ Post-Checkout Hook (Client-side)
154
+ Runs after git checkout, git switch, or git clone (when a worktree is updated).
155
+ Use this hook to:
156
+ - Restore working tree metadata (e.g. permissions, ACLs)
157
+ - Auto-display differences from the previous HEAD
158
+ - Run repository validity checks or refresh generated files
159
+ Takes three parameters: previous HEAD ref, new HEAD ref, and a flag (1 = branch checkout, 0 = file checkout).
160
+ """
161
+
162
+
163
+ class PostCheckout(Hook):
164
+ def __init__(self, repo: pygit2.Repository):
165
+ super().__init__(repo)
166
+
167
+ def run(self, prev_head: str, new_head: str, branch_checkout: str) -> HookResult:
168
+ return HookResult.SUCCESS
169
+
170
+
171
+ """
172
+ Post-Merge Hook (Client-side)
173
+ Runs after a successful git merge (e.g. after git pull). Cannot affect the outcome.
174
+ Use this hook to:
175
+ - Restore working tree metadata in conjunction with pre-commit
176
+ - Run post-merge checks or notifications
177
+ Takes one parameter: a status flag indicating whether the merge was a squash merge.
178
+ """
179
+
180
+
181
+ class PostMerge(Hook):
182
+ def __init__(self, repo: pygit2.Repository):
183
+ super().__init__(repo)
184
+
185
+ def run(self, squash: str) -> HookResult:
186
+ return HookResult.SUCCESS
187
+
188
+
189
+ """
190
+ Pre-Push Hook (Client-side)
191
+ Called by git push; can be used to prevent a push.
192
+ Use this hook to:
193
+ - Run tests or lint before pushing
194
+ - Enforce branch naming or ref permissions
195
+ - Validate commits being pushed
196
+ Takes two parameters: remote name and remote URL. Ref updates are provided on stdin.
197
+ """
198
+
199
+
200
+ class PrePush(Hook):
201
+ def __init__(self, repo: pygit2.Repository):
202
+ super().__init__(repo)
203
+
204
+ def run(self, remote_name: str, remote_url: str) -> HookResult:
205
+ return HookResult.SUCCESS
206
+
207
+
208
+ # -----------------------------------------------------------------------------
209
+ # Server-side hooks (run in $GIT_DIR on receive-pack / push)
210
+ # -----------------------------------------------------------------------------
211
+
212
+ """
213
+ Update Hook (Server-side)
214
+ Invoked by git-receive-pack once per ref being updated, before the ref is updated.
215
+ Use this hook to:
216
+ - Enforce fast-forward only (reject non-FF updates)
217
+ - Implement per-ref access control
218
+ - Log or validate old → new for specific refs
219
+ Takes three parameters: ref name, old object name, new object name.
220
+ """
221
+
222
+
223
+ class Update(Hook):
224
+ def __init__(self, repo: pygit2.Repository):
225
+ super().__init__(repo)
226
+
227
+ def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
228
+ return HookResult.SUCCESS
229
+
230
+
231
+ """
232
+ Post-Update Hook (Server-side)
233
+ Invoked by git-receive-pack once after all refs have been updated.
234
+ Use this hook to:
235
+ - Notify or trigger CI for updated refs
236
+ - Run git update-server-info for dumb transports (e.g. HTTP)
237
+ - Update caches or derived data
238
+ Takes a variable number of parameters: the name of each ref that was updated.
239
+ """
240
+
241
+
242
+ class PostUpdate(Hook):
243
+ def __init__(self, repo: pygit2.Repository):
244
+ super().__init__(repo)
245
+
246
+ def run(self, *ref_names: str) -> HookResult:
247
+ return HookResult.SUCCESS
248
+
249
+
250
+ """
251
+ Push-To-Checkout Hook (Server-side)
252
+ Invoked when a push updates the currently checked-out branch and receive.denyCurrentBranch is updateInstead.
253
+ Use this hook to:
254
+ - Override how the working tree and index are updated to match the new commit
255
+ - Run git read-tree -u -m to emulate a reverse fetch
256
+ - Refuse the push by exiting non-zero (without modifying index or worktree)
257
+ Takes one parameter: the commit object name the tip of the current branch will be updated to.
258
+ """
259
+
260
+
261
+ class PushToCheckout(Hook):
262
+ def __init__(self, repo: pygit2.Repository):
263
+ super().__init__(repo)
264
+
265
+ def run(self, new_commit: str) -> HookResult:
266
+ return HookResult.SUCCESS
267
+
268
+
269
+ """
270
+ Pre-Auto-GC Hook
271
+ Invoked by git gc --auto before automatic garbage collection runs.
272
+ Use this hook to:
273
+ - Prevent or delay gc when the repo is busy (e.g. long-running operations)
274
+ - Run housekeeping or consistency checks before gc
275
+ - Notify or log that auto-gc is about to run
276
+ Takes no parameters. Exiting with non-zero status prevents gc from running.
277
+ """
278
+
279
+
280
+ class PreAutoGc(Hook):
281
+ def __init__(self, repo: pygit2.Repository):
282
+ super().__init__(repo)
283
+
284
+ def run(self) -> HookResult:
285
+ return HookResult.SUCCESS
286
+
287
+
288
+ """
289
+ Pre-Receive Hook (Server-side)
290
+ Invoked by git-receive-pack once before any refs are updated. Receives ref updates on
291
+ stdin as `<old-oid> <new-oid> <ref-name>` lines (one per ref). Exiting non-zero rejects
292
+ the entire push (no refs are updated).
293
+ Use this hook to:
294
+ - Reject pushes that violate global policy (e.g. force-push to protected refs)
295
+ - Notify a long-poll change queue so subscribers can react quickly
296
+ """
297
+
298
+
299
+ class PreReceive(Hook):
300
+ def __init__(self, repo: pygit2.Repository):
301
+ super().__init__(repo)
302
+
303
+ def run(self, ref_updates: list[RefUpdate]) -> HookResult:
304
+ return HookResult.SUCCESS
305
+
306
+
307
+ """
308
+ Post-Receive Hook (Server-side)
309
+ Invoked by git-receive-pack once after all refs have been updated. Receives ref updates
310
+ on stdin in the same format as pre-receive. Exit code does not affect the receive.
311
+ Use this hook to:
312
+ - Notify caches / long-poll subscribers (refs are visible at this point)
313
+ - Trigger CI, mirroring, or webhook delivery
314
+ """
315
+
316
+
317
+ class PostReceive(Hook):
318
+ def __init__(self, repo: pygit2.Repository):
319
+ super().__init__(repo)
320
+
321
+ def run(self, ref_updates: list[RefUpdate]) -> HookResult:
322
+ return HookResult.SUCCESS
323
+
324
+
325
+ # -----------------------------------------------------------------------------
326
+ # Hooks skipped (not implemented in this module)
327
+ # -----------------------------------------------------------------------------
328
+ #
329
+ # E-mail / git-am hooks (skipped by design):
330
+ # - applypatch-msg (message file; used by git am)
331
+ # - pre-applypatch (no params; used by git am)
332
+ # - post-applypatch (no params; used by git am)
333
+ #
334
+ # Other stdin-only / protocol hooks (skipped: pkt-line or transactional state):
335
+ # - reference-transaction (state string + ref updates on stdin)
336
+ # - proc-receive (pkt-line protocol on stdin/stdout)