Write pretty and concise Git hooks in Python.
Project description
GitHooks
Write pretty and concise Git hooks in Python. GitHooks lets you write an entire Git hook directly in Python, without using YAML. It’s ideal when you want full control and all your logic contained in a single file.
- Installing
- Hooks
- Common config
- Dedicated
commit-msgmethods - Creating a symlink
- Changelog
- License
Installing
You can install via pip:
pip install githooks
No dependency required.
Old version
GitHooks was previously named SimpleGitHooks, you can install latest old version by command pip install simplegithooks but it's recommended to use the latest githooks.
Hooks
PreCommit
Write a simple pre-commit Git hook file e.g.: helpers/pre-commit and then install it with a command githooks pre-commit --install helpers/pre-commit.py:
#!/usr/bin/env python
from githooks import PreCommit, Priority
pre_commit = PreCommit(__file__)
pre_commit.ignore_file("helpers/pre-commit.py")
pre_commit.check_content_for("❌", "FIXME")
pre_commit.check_content_for("🚧", "NotImplemented")
pre_commit.check_content_for("⚠️", "secure", priority=Priority.MEDIUM)
pre_commit.check_command("ruff check")
pre_commit.report()
Let's say you have such file in staged changes main.py because you've forgot to finish:
import math
def add(b, c):
# TODO add typing
return b + c
def divide(a, b):
# secure dividing by zero
return a / b
def sqrt():
# FIXME
raise NotImplementedError
And when you try to commit this file using git commit -m "message" the output will be:
What happened here? Let's focus only on checks that prevents us from commit this change (notice a locker icon):
- by default all checks prevents commit (Priority.HIGH), unless you explicitly pass level
priority=Priority.MEDIUMor lower level check_content_for("❌", "FIXME", "error")failed becauseFIXMEwas found inmain.pycheck_content_for("🚧", "NotImplemented", "fail")failed becauseNotImplementedwas found inmain.pycheck_command("ruff check")failed because commandruff checkreturned non-zero output (because of unused importmath)
Then if you fix issues the code now looks more on less like this:
import math
from typing import Any
def add(b:Any, c:Any):
return b + c
def divide(a, b):
# secure dividing by zero
return a / b
def sqrt(x):
return math.sqrt(x)
The output after commit will be:
Now check_content_for("⚠️", "secure", priority=Priority.MEDIUM) failed because phrase secure was found in main.py, yet this is not preventing us from commit changes, so commit command was succeeded but with warning Commit allowed conditionally.
Still we can do better 😉, so let's try harder:
import math
from typing import Any
def add(b:Any, c:Any):
return b + c
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return float("inf")
def sqrt(x):
return math.sqrt(x)
Finally we reached our goal:
PrePush
Write simple pre-push Git hook e.g.: .git/hooks/pre-push and then instal it with a command githooks pre-push --install helpers/pre-push.py:
#!/usr/bin/env python
from githooks import PrePush
pre_push = PrePush(__file__)
pre_push.ignore_files(["pre_push_example.py", "README.md"])
pre_push.check_command("pytest", rc_success_set={0, 5})
pre_push.report()
You'll get similar outputs like for pre-commit:
CommitMsg
Write simple commit-msg Git hook e.g.: .git/hooks/commit-msg and then instal it with a command githooks commit-msg --install helpers/commit-msg.py:
#!/usr/bin/env python
from githooks import CommitMsg
commit_msg = CommitMsg(__file__)
commit_msg.check_title_conventional_commit()
commit_msg.check_content_forbidden_words_set()
commit_msg.check_content_spelling()
commit_msg.check_title_length(60)
commit_msg.insert_into_content(0, 7, "🎯 ")
commit_msg.report()
You'll get similar outputs like for pre-commit, but also with it as commit-msg is just after pre-commit:
Common config
ignore_files for ignoring files
pre_commit.ignore_file("src/obsolete.py")
pre_commit.ignore_files(["src/stub1.py", "src/stub2.py"])
Support for Python's pathlib.Path pattern matching
pre_commit.ignore_files(["pre-commit.py", "*.svg", "README.md"])
check_content_for search for lines in files that match substring
pre_commit.check_content_for("FIXME", "❌", "error")
pre_commit.check_content_for("NotImplemented", "🚧", "fail")
pre_commit.check_content_for("TODO", "⚠️", "warning", priority=Priority.MEDIUM)
check_command for checking commands execution
pre_commit.check_command("ruff check . --fix --diff", priority=Priority.MEDIUM)
pre_commit.check_command("ruff check . --fix --show-fixes")
pre_commit.check_command("ruff format .")
pre_commit.check_command("echo false && false", priority=Priority.INFO)
Check commands which RC=0 means failure
pre_commit.check_command("true", rc_success_set={1}) # ❯ true (ERROR, RC==1 SUCCESS) 🔒
pre_commit.check_command("false", rc_success_set={1}) # ❯ false (OK, RC==1 SUCCESS)
report full report including outputs, results, summary and ending
outputs for table-formatted color-aware outputs when using check_command
pre_commit.check_command("ruff check . --fix --diff", priority=Priority.MEDIUM)
print(pre_commit.outputs())
Example of an output:
┌─────────────────────────────────┐
│ ruff check . --fix --show-fixes │
├─────────────────────────────────┤
│ All checks passed! │
└─────────────────────────────────┘
results for all or filtered results
All results:
print(pre_commit.results())
Filtered results:
print(pre_commit.results("error"))
print(pre_commit.results("warning"))
Example of results:
Results:
❌ FIXME not found
🚧 NotImplemented not found
⚠️ TODO not found
❯ ruff check . --fix --diff (OK)
❯ ruff check . --fix --show-fixes (OK)
❯ ruff format . (OK)
❯ mypy --explicit-package-bases --ignore-missing-imports . (OK)
❯ echo false && false (ERROR, priority=Priority.INFO)
❯ cd . && pytest (OK)
summary for quick summary
print(pre_commit.summary())
Example of a summary:
Summary:
(nothing to show)
ending for the return code of the git hook
This will finish git hook script withe the git hook result:
sys.exit(pre_commit.ending)
Possible outputs are:
🟢 Commit clean.
🟡 Commit allowed (caution).
🔴 Commit aborted.
support for other git hooks
GitHooks supports other hooks which can be run like a regular command, but with dedicated interface
check_other_hook
Runs other file for provided relative path, e.g.:
pre_commit.check_other_hook("pre-commit.old")
check_other_hooks
Runs other files if they match provided rules for names, e.g.:
pre_commit.check_other_hooks(prefix_in=["pre-commit"], suffix_not_in=["err", "sample"])
Dedicated commit-msg methods
custom_check
commit_msg.custom_check(
commit_msg.title == 42,
glyph=Glyphs.question_mark_white,
glyph_space=1,
category="custom check for 'title == 42'",
msg=f"custom check {'ok' if commit_msg.title == 42 else f"fail '{commit_msg.title}' != 42"}",
priority=Priority.LOW,
)
check_title_regex_fullmatch
commit_msg.check_title_regex_fullmatch(
r"""^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([\w\-]+\))?(!)?: .+$""",
priority=Priority.HIGH,
)
check_title_regex_search
commit_msg.check_title_regex_search(r"^(feat|fix)", priority=Priority.MEDIUM)
check_title_regex_match
commit_msg.check_title_regex_match(r"[A-Za-z]+", priority=Priority.MEDIUM)
check_title_contains_words_set
commit_msg.check_title_contains_words_set({"client"})
check_content_contains_words_set
commit_msg.check_content_contains_words_set({"fix"})
check_title_conventional_commit
commit_msg.check_title_conventional_commit()
check_content_forbidden_words_set
commit_msg.check_content_forbidden_words_set({":"}, glyph=Glyphs.no_entry, priority=Priority.LOW)
commit_msg.check_content_forbidden_words_set()
check_content_spelling
commit_msg.check_content_spelling()
check_title_length
commit_msg.check_title_length(60)
insert_into_content
commit_msg.insert_into_content(0, 7, "🎯 ")
Creating a symlink
Run githooks pre-commit --install path/to/pre_commit.py or githooks pre-push --install path/to/pre_push.py to create a symlink for you repository:
If a hook file already exists, an additional message e.g. WARNING: file '/home/user/project/.git/hooks/pre-commit' already exists and will be overwritten. will be shown as below:
Auto confirmation
Pass -y or --yes or --assume-yes to skip confirmation with typing CREATE_SYMBOLIC_LINK. You will still get final result and warning if file or symbolic link already exists.
Troubleshooting
If you pass a bad hook name you'll receive a hint if there is a typo e.g. Unknown or unsupported hook: preccomyt, did you mean: pre-commit.
In case of any problem while creating a symlink you'll get Failure, couldn't create the symbolic link. instead of success message.
Changelog
See Changelog.
License
This repository is licensed under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file githooks-1.3.0.tar.gz.
File metadata
- Download URL: githooks-1.3.0.tar.gz
- Upload date:
- Size: 30.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20c3cc30d747ad719d236406022a1e3427bb9079e76a82132f87cc1345205472
|
|
| MD5 |
d9c60ff0db4df6729735d7b0e8a0f755
|
|
| BLAKE2b-256 |
65df104516d1a0d7af641faff8fa0a43943775afd6b2e092f07f293be2797c6b
|
File details
Details for the file githooks-1.3.0-py3-none-any.whl.
File metadata
- Download URL: githooks-1.3.0-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
865ca6a233706c7ba6bf23d503cc9e2b94042f8534da8d042f691fd1a59cb727
|
|
| MD5 |
89561198e40862519c64cd8443e90aa4
|
|
| BLAKE2b-256 |
56294d2ab5b2177d6398795790cfae643920d60e14fa7f3bc863788df9ef28e3
|