git-a-grip
Personal pre-commit hooks.
repos:
- repo: https://github.com/dannybrown37/git-a-grip
rev: v0.3.1
hooks:
- id: commitizen-early
- id: ruff-check
- id: ruff-format
- id: pytest
args: [tests/, -q]
The commitizen and ruff hooks reach their tool through
sys.executable -m <tool> inside the env pre-commit builds for this repo, so
a consuming project needs no cz or ruff on PATH, no venv and no
uv/uvx of its own. (pytest is the exception — see below.)
Each hook pays only for what it uses: commitizen is a dependency of the
package, while ruff is declared by the two ruff hooks themselves, through
additional_dependencies in .pre-commit-hooks.yaml. You still pass
nothing. Pin your own ruff by setting additional_dependencies: [ruff==x.y.z] on the hook.
commitizen-early (pre-commit stage)
Rejects a non-conventional commit message in about a third of a second, instead of after the whole slow hook suite has run.
Git runs pre-commit -> prepare-commit-msg -> editor -> commit-msg as
separate invocations, so a stages: [commit-msg] commitizen hook can only
ever fail after your tests. Nothing in .pre-commit-config.yaml reorders
that. This hook instead recovers the message from the git commit process's
own argv while the pre-commit stage is still running, and checks it first.
Pair it with the upstream commitizen hook, which still catches the cases
argv cannot reach (interactive editor, merge, rebase) — this one exits 0 and
defers whenever it finds no message:
- repo: https://github.com/dannybrown37/git-a-grip
rev: v0.3.1
hooks:
- id: commitizen-early
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.17.0
hooks:
- id: commitizen
stages: [commit-msg]
Put it first and give it fail_fast: true if you want it to short-circuit
the rest of the stage.
git-release (command, not a hook)
Bump, tag and push, in that order, exiting 0. For repos that release from a
laptop rather than from CI. Give it an alias that says what it does — not
gp, which reads as git push right up until it publishes something:
alias release='uvx --from git-a-grip git-release'
This repo itself no longer uses it: releases here are cut by CI once the
checks on main pass (see below). The command remains for projects with no
such pipeline, where the alternative is remembering the four commands by
hand.
On main it bumps and pushes; on any other branch it just pushes, so it can
replace git push outright. Refuses to run against a dirty tree, and pushes
anyway when there are no bumpable commits.
This exists because a pre-push hook cannot do this cleanly. Git chooses
which sha to push before hooks run, so a commit created afterwards leaves two
options: cancel the push, or let git push the now-superseded sha and have it
rejected as a non-fast-forward. Both end in error: failed to push some refs
on top of a release that worked. Running as a command puts the bump before
the push and the problem disappears.
Configure what the bump rewrites via [tool.commitizen] in the consuming
repo (version_provider, version_files).
A
bump-on-pushpre-push hook did this up to v0.2.1 and was removed in v0.3.0 for the reason above. If you pin an older rev, that hook still exists there; on upgrading, drop- id: bump-on-pushand use this command.
ruff-check and ruff-format (pre-commit stage)
ruff check --fix and ruff format, with the fixes re-staged so they are
part of the commit you just made rather than a dirty working tree you have to
git add and amend. Only the violations ruff could not fix stop the commit,
via ruff's own exit code.
Pass ruff's flags through args:
- id: ruff-check
args: [--config, .ruff.toml]
--force-exclude is always passed, so the exclude in your ruff config still
applies to the paths pre-commit hands over explicitly. The re-staged set is
narrowed by content digest — a file ruff did not change is never touched, and
because pre-commit stashes unstaged changes while a hook runs, re-adding a
file cannot sweep in an edit you deliberately left unstaged.
The ruff version is this repo's pinned dependency. To hold a repo at a different one:
- id: ruff-format
additional_dependencies: [ruff==0.16.1]
pytest (pre-commit stage)
Runs the test suite from the repo root. This hook can't use the isolated env
pre-commit builds here — a test suite needs the consuming project's
dependencies — so it shells out to a runner that resolves that environment,
uv run pytest by default. Everything else in args goes to pytest:
- id: pytest
args: [tests/, -q]
- id: pytest
args: ['--runner=uv run --extra api pytest', tests/, -q]
It runs from the repo root regardless of where git was invoked, and drops the
VIRTUAL_ENV/PYTHONPATH that pre-commit exports for its own hook env —
which would otherwise point the runner at an environment holding none of your
project's dependencies. Narrow when it runs with files: (default
^(src/|tests/).*).
pre-commit-audit (command, not a hook)
Audit every local repo's pre-commit setup at once, so a hook that drifted or never got installed shows up as a line rather than a surprise:
uvx --from git-a-grip pre-commit-audit
It walks the given trees (default: this repo's sibling directories), stops at
each git working tree, and reports four things: which of this repo's hooks
each project uses and the rev it pins, third-party hooks grouped by source
repo and rev, one-off repo: local hooks with their entry, and repos with no
usable config at all. --json emits the same data unformatted.
pre-commit-audit ~/projects ~/work
pre-commit-audit --json | jq '.[] | select(.hooks == [])'
Installing the commands
The hooks need no installation — pre-commit builds this repo an isolated env
from the rev you pin. The two commands (git-release, pre-commit-audit)
are ordinary console scripts, published to PyPI:
uvx --from git-a-grip pre-commit-audit # one-off
uv tool install git-a-grip # both commands, on PATH
That install carries only what the commands import — commitizen and pyyaml — not the ruff the hooks use. To run the hook entry points by hand as well, ask for the extra:
uv tool install 'git-a-grip[hooks]'
Straight from a tag works too, and is the way to run something not yet released:
uvx --from git+https://github.com/dannybrown37/git-a-grip@v0.3.1 git-release
Releasing
Merge to main. That is the whole gesture.
ci.yml runs lint, tests and the install proofs on the merged commit; only
if they all pass does its bump job run cz bump, which writes the version
and changelog, commits, and tags. Pushing that tag triggers publish.yml,
which builds and uploads to PyPI via trusted publishing. A push with no
bumpable commits (docs, chores) ends after the checks and releases nothing.
Nothing is tagged before the checks pass, so a red build cannot leave a version number stranded on a release that never shipped.
Development
uv sync
uv run pytest
This repo eats its own dog food: both hooks are wired into its own
.pre-commit-config.yaml.
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 git_a_grip-0.4.0.tar.gz.
File metadata
- Download URL: git_a_grip-0.4.0.tar.gz
- Upload date:
- Size: 55.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44c89c714b8c289dbcd64da3fc672cf07b335b3b17d79b3808b09ceab534505a
|
|
| MD5 |
ef1c9687cbda61773c5d3e21e4d1d022
|
|
| BLAKE2b-256 |
bce4af642057607cf4633f7ca3596feaf025fcf37c02c465e1fb329b914defaf
|
Provenance
The following attestation bundles were made for git_a_grip-0.4.0.tar.gz:
Publisher:
publish.yml on dannybrown37/git-a-grip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_a_grip-0.4.0.tar.gz -
Subject digest:
44c89c714b8c289dbcd64da3fc672cf07b335b3b17d79b3808b09ceab534505a - Sigstore transparency entry: 2327686541
- Sigstore integration time:
-
Permalink:
dannybrown37/git-a-grip@0ea008d30b9069ed97cc4f24b88953cc572103aa -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/dannybrown37
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0ea008d30b9069ed97cc4f24b88953cc572103aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file git_a_grip-0.4.0-py3-none-any.whl.
File metadata
- Download URL: git_a_grip-0.4.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51b6dc01e6ebb189406b42469ee4b005143b27fdd8060954841e64f5ec4a85d1
|
|
| MD5 |
8d601e6a4c42374db9dacaa76ee13bff
|
|
| BLAKE2b-256 |
161d41ef6fb505db09b36ead6eeb86ef31dc1f98996654ec990b12f66a6188e8
|
Provenance
The following attestation bundles were made for git_a_grip-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on dannybrown37/git-a-grip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_a_grip-0.4.0-py3-none-any.whl -
Subject digest:
51b6dc01e6ebb189406b42469ee4b005143b27fdd8060954841e64f5ec4a85d1 - Sigstore transparency entry: 2327686637
- Sigstore integration time:
-
Permalink:
dannybrown37/git-a-grip@0ea008d30b9069ed97cc4f24b88953cc572103aa -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/dannybrown37
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0ea008d30b9069ed97cc4f24b88953cc572103aa -
Trigger Event:
push
-
Statement type: