git-a-grip 0.4.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,120 @@
1
+ # Check every push and PR; on a green main, cut the release tag.
2
+ #
3
+ # The release is CI's consequence, not its cause: nothing is bumped or tagged
4
+ # until lint, tests, and the install proofs have passed on the merged commit.
5
+ # Pushing the tag is what triggers publish.yml.
6
+ name: ci
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+
13
+ jobs:
14
+ check:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ # The floor in requires-python and whatever is current: the two
20
+ # versions where a syntax or stdlib assumption actually breaks.
21
+ python-version: ['3.11', '3.13']
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: astral-sh/setup-uv@v5
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+ enable-cache: true
29
+
30
+ - run: uv sync
31
+
32
+ - name: Lint
33
+ run: uv run ruff check --config .ruff.toml src tests
34
+
35
+ - name: Format
36
+ run: uv run ruff format --check --config .ruff.toml src tests
37
+
38
+ - name: Test
39
+ run: uv run pytest -q
40
+
41
+ - name: Build
42
+ run: uv build
43
+
44
+ # The commands must work from a bare wheel with no dev group and no
45
+ # `hooks` extra -- that is what `uv tool install git-a-grip` gets, and
46
+ # it is exactly what the dependency split could break.
47
+ - name: Commands run from the built wheel alone
48
+ run: |
49
+ uv run --isolated --no-project --with dist/*.whl \
50
+ pre-commit-audit --help
51
+ uv run --isolated --no-project --with dist/*.whl \
52
+ git-release --help
53
+
54
+ # The hooks are the other half of the split. try-repo builds them from
55
+ # .pre-commit-hooks.yaml in the isolated env a consumer would get, so
56
+ # this fails if ruff stops arriving via additional_dependencies -- which
57
+ # the repo's own config, using `language: system` locally, cannot catch.
58
+ - name: Hooks install and run as a consumer would get them
59
+ run: uv run --with pre-commit pre-commit try-repo . --all-files
60
+
61
+ - name: This repo's own config still passes
62
+ run: uv run --with pre-commit pre-commit run --all-files
63
+
64
+ bump:
65
+ needs: [check]
66
+ # Only a green push to main releases, and never the bump's own commit --
67
+ # that would recurse.
68
+ if: >-
69
+ github.event_name == 'push'
70
+ && github.ref == 'refs/heads/main'
71
+ && !startsWith(github.event.head_commit.message, 'bump:')
72
+ runs-on: ubuntu-latest
73
+ permissions:
74
+ contents: write
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ with:
78
+ # Full history so commitizen can read the tags it bumps from, and a
79
+ # PAT because pushes made with the default GITHUB_TOKEN do not
80
+ # trigger workflows -- the tag would land and publish.yml would
81
+ # never fire.
82
+ fetch-depth: 0
83
+ token: ${{ secrets.BUMP_TOKEN }}
84
+
85
+ - uses: astral-sh/setup-uv@v5
86
+
87
+ - name: Configure git identity
88
+ run: |
89
+ git config user.name "github-actions[bot]"
90
+ git config user.email "github-actions[bot]@users.noreply.github.com"
91
+
92
+ - name: Bump version and tag
93
+ id: cz
94
+ run: |
95
+ PRE_SHA="$(git rev-parse HEAD)"
96
+ # --no-raise 21: "no commits eligible for a bump" is the normal
97
+ # outcome of a docs- or chore-only push, not a failure.
98
+ uv run cz --no-raise 21 bump --yes
99
+ POST_SHA="$(git rev-parse HEAD)"
100
+ if [[ "$PRE_SHA" == "$POST_SHA" ]]; then
101
+ echo "bumped=false" >>"$GITHUB_OUTPUT"
102
+ exit 0
103
+ fi
104
+ echo "bumped=true" >>"$GITHUB_OUTPUT"
105
+
106
+ # cz bump rewrites the version in pyproject.toml only, so uv.lock's
107
+ # embedded git-a-grip version drifts. Re-lock and fold that into the
108
+ # bump commit, keeping the tag on the commit it names.
109
+ uv lock
110
+ if ! git diff --quiet -- uv.lock; then
111
+ TAG="$(git describe --tags --exact-match HEAD)"
112
+ git tag -d "$TAG"
113
+ git add uv.lock
114
+ git commit --amend --no-edit
115
+ git tag -a "$TAG" -m "$TAG"
116
+ fi
117
+
118
+ - name: Push bump commit and tag
119
+ if: steps.cz.outputs.bumped == 'true'
120
+ run: git push origin "HEAD:${{ github.ref_name }}" --tags
@@ -0,0 +1,53 @@
1
+ # Publish to PyPI when ci.yml's bump job pushes a version tag.
2
+ #
3
+ # By the time a tag exists, CI has already passed on that commit -- the bump
4
+ # job is gated on it -- so this workflow only builds and uploads.
5
+ #
6
+ # Authentication is PyPI trusted publishing (OIDC), so there is no API token
7
+ # in this repo's secrets. It requires a one-time pending-publisher entry on
8
+ # pypi.org naming this repo, this workflow file, and the `pypi` environment.
9
+ name: publish
10
+
11
+ on:
12
+ push:
13
+ tags: ['v*']
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+
22
+ # The tag is the version. Publishing anything else would put a release
23
+ # on PyPI that no tag in this repo describes, and PyPI never lets that
24
+ # version number be reused.
25
+ - name: Tag matches the packaged version
26
+ run: |
27
+ packaged="v$(uv version --short)"
28
+ if [ "$packaged" != "${GITHUB_REF_NAME}" ]; then
29
+ echo "tag ${GITHUB_REF_NAME} != packaged ${packaged}" >&2
30
+ exit 1
31
+ fi
32
+
33
+ - run: uv build
34
+
35
+ - uses: actions/upload-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+
40
+ publish:
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ environment: pypi
44
+ permissions:
45
+ # The OIDC token trusted publishing exchanges for an upload token.
46
+ id-token: write
47
+ steps:
48
+ - uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+
53
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
@@ -0,0 +1,58 @@
1
+ default_install_hook_types: [pre-commit, commit-msg]
2
+ default_stages: [pre-commit]
3
+
4
+ repos:
5
+ # Dogfood: this repo's own hooks, run from the working tree rather than a
6
+ # pinned rev, so a change that breaks them fails here before it ships.
7
+ - repo: local
8
+ hooks:
9
+ - id: commitizen-early
10
+ name: Commitizen check (early)
11
+ entry: uv run commitizen-early
12
+ language: system
13
+ always_run: true
14
+ pass_filenames: false
15
+ fail_fast: true
16
+
17
+ - repo: https://github.com/commitizen-tools/commitizen
18
+ rev: v4.17.0
19
+ hooks:
20
+ - id: commitizen
21
+ stages: [commit-msg]
22
+
23
+ - repo: https://github.com/pre-commit/pre-commit-hooks
24
+ rev: v4.6.0
25
+ hooks:
26
+ - id: end-of-file-fixer
27
+ - id: check-merge-conflict
28
+ - id: check-toml
29
+ - id: check-yaml
30
+ - id: mixed-line-ending
31
+
32
+ - repo: https://github.com/gitleaks/gitleaks
33
+ rev: v8.30.1
34
+ hooks:
35
+ - id: gitleaks
36
+
37
+ - repo: local
38
+ hooks:
39
+ - id: ruff-check
40
+ name: Ruff check
41
+ entry: uv run ruff-check-hook --config .ruff.toml
42
+ language: system
43
+ types: [python]
44
+ require_serial: true
45
+
46
+ - id: ruff-format
47
+ name: Ruff format
48
+ entry: uv run ruff-format-hook --config .ruff.toml
49
+ language: system
50
+ types: [python]
51
+ require_serial: true
52
+
53
+ - id: tests
54
+ name: Tests
55
+ entry: uv run pytest-hook tests/ -q
56
+ language: system
57
+ files: ^(src/|tests/).*
58
+ pass_filenames: false
@@ -0,0 +1,47 @@
1
+ - id: commitizen-early
2
+ name: Commitizen check (early)
3
+ description: >-
4
+ Reject a non-conventional commit message during the pre-commit stage,
5
+ before the slow hooks run, by recovering it from git's argv.
6
+ entry: commitizen-early
7
+ language: python
8
+ stages: [pre-commit]
9
+ always_run: true
10
+ pass_filenames: false
11
+
12
+ - id: ruff-check
13
+ name: Ruff check
14
+ description: >-
15
+ Lint with `ruff check --fix`, re-staging the files it fixed so the fix is
16
+ part of the commit rather than left dirty in the working tree.
17
+ entry: ruff-check-hook
18
+ language: python
19
+ types: [python]
20
+ # ruff is not a dependency of the package -- only these two hooks need it,
21
+ # and pulling it into `pip install git-a-grip` would tax the commands with
22
+ # a compiler-sized wheel they never call. Override the version per repo by
23
+ # setting your own `additional_dependencies: [ruff==x.y.z]`.
24
+ additional_dependencies: [ruff>=0.6]
25
+ # Both ruff hooks call `git add`; running them in parallel races index.lock.
26
+ require_serial: true
27
+
28
+ - id: ruff-format
29
+ name: Ruff format
30
+ description: >-
31
+ Format with `ruff format`, re-staging the files it rewrote.
32
+ entry: ruff-format-hook
33
+ language: python
34
+ types: [python]
35
+ additional_dependencies: [ruff>=0.6]
36
+ require_serial: true
37
+
38
+ - id: pytest
39
+ name: Tests
40
+ description: >-
41
+ Run the consuming repo's test suite from the repo root, through a runner
42
+ that resolves the project's own environment (`uv run pytest` by default,
43
+ override with `--runner=...`).
44
+ entry: pytest-hook
45
+ language: python
46
+ files: ^(src/|tests/).*
47
+ pass_filenames: false
@@ -0,0 +1,67 @@
1
+ line-length = 79
2
+ show-fixes = true
3
+ target-version = "py311"
4
+
5
+ [lint]
6
+ select = [
7
+ "A", # https://beta.ruff.rs/docs/rules/#flake8-builtins-a
8
+ "ANN", # https://beta.ruff.rs/docs/rules/#flake8-annotations-ann
9
+ "ARG", # https://beta.ruff.rs/docs/rules/#flake8-unused-arguments-arg
10
+ "B", # https://beta.ruff.rs/docs/rules/#flake8-bugbear-b
11
+ "C4", # https://beta.ruff.rs/docs/rules/#flake8-comprehensions-c4
12
+ "COM", # https://beta.ruff.rs/docs/rules/#flake8-commas-com
13
+ "C90", # https://beta.ruff.rs/docs/rules/#mccabe-c90
14
+ "D200", # One-line docstring should fit on one line
15
+ "D201", # No blank lines allowed before function docstring
16
+ "D202", # No blank lines allowed after function docstring
17
+ "D205", # 1 blank line required between summary line and description
18
+ "D212", # Multi-line docstring summary should start at the first line
19
+ "E", # default! https://beta.ruff.rs/docs/rules/#error-e
20
+ "EM", # https://beta.ruff.rs/docs/rules/#flake8-errmsg-em
21
+ "ERA", # https://beta.ruff.rs/docs/rules/#eradicate-era
22
+ "EXE", # https://beta.ruff.rs/docs/rules/#flake8-executable-exe
23
+ "F", # default! https://beta.ruff.rs/docs/rules/#pyflakes-f
24
+ "FBT", # https://beta.ruff.rs/docs/rules/#flake8-boolean-trap-fbt
25
+ "G", # https://beta.ruff.rs/docs/rules/#flake8-logging-format-g
26
+ "I", # https://beta.ruff.rs/docs/rules/#isort-i
27
+ "ICN", # https://beta.ruff.rs/docs/rules/#flake8-import-conventions-icn
28
+ "ISC", # https://beta.ruff.rs/docs/rules/#flake8-implicit-str-concat-isc
29
+ "N", # https://beta.ruff.rs/docs/rules/#pep8-naming-n
30
+ "PGH", # https://beta.ruff.rs/docs/rules/#pygrep-hooks-pgh
31
+ "PIE", # https://beta.ruff.rs/docs/rules/#flake8-pie-pie
32
+ "PL", # https://beta.ruff.rs/docs/rules/#pylint-pl
33
+ "PLE", # https://beta.ruff.rs/docs/rules/#error-ple
34
+ "PLR", # https://beta.ruff.rs/docs/rules/#refactor-plr
35
+ "PLW", # https://beta.ruff.rs/docs/rules/#warning-plw
36
+ "PT", # https://beta.ruff.rs/docs/rules/#flake8-pytest-style-pt
37
+ "PTH", # https://beta.ruff.rs/docs/rules/#flake8-use-pathlib-pth
38
+ "PYI", # https://beta.ruff.rs/docs/rules/#flake8-pyi-pyi
39
+ "Q", # https://beta.ruff.rs/docs/rules/#flake8-quotes-q
40
+ "RET", # https://beta.ruff.rs/docs/rules/#flake8-return-ret
41
+ "RSE", # https://beta.ruff.rs/docs/rules/#flake8-raise-rse
42
+ "RUF", # https://beta.ruff.rs/docs/rules/#ruff-specific-rules-ruf
43
+ "S", # https://beta.ruff.rs/docs/rules/#flake8-bandit-s
44
+ "SLF", # https://beta.ruff.rs/docs/rules/#flake8-self-slf
45
+ "SIM", # https://beta.ruff.rs/docs/rules/#flake8-simplify-sim
46
+ "TCH", # https://beta.ruff.rs/docs/rules/#flake8-type-checking-tch
47
+ "TID", # https://beta.ruff.rs/docs/rules/#flake8-tidy-imports-tid
48
+ "TRY", # https://beta.ruff.rs/docs/rules/#tryceratops-try
49
+ "W", # https://beta.ruff.rs/docs/rules/#warning-w
50
+ "UP", # https://beta.ruff.rs/docs/rules/#pyupgrade-up
51
+ "YTT", # https://beta.ruff.rs/docs/rules/#flake8-2020-ytt
52
+ ]
53
+ ignore = [
54
+ "S311", # not using randomness for cryptography
55
+ "S101", # assert is awesome
56
+ "I001", # I do not care for its sorting choices
57
+ "S113", # requests calls without timeouts are fine
58
+ "PLR0913", # sometimes tests need 6+ fixtures
59
+ "PLC0415", # deliberate lazy imports (CLI startup time, circular imports)
60
+ ]
61
+
62
+ [lint.flake8-quotes]
63
+ inline-quotes = "single"
64
+
65
+ [format]
66
+ quote-style = "single"
67
+ docstring-code-format = true
@@ -0,0 +1,39 @@
1
+ ## v0.4.0 (2026-08-02)
2
+
3
+ ### Feat
4
+
5
+ - prepare as a package for pypi, license, et al
6
+ - add pre-commit-audit command to help keep local repos in line
7
+
8
+ ### Fix
9
+
10
+ - have CI handle releases after successful checks, max automation
11
+
12
+ ## v0.3.1 (2026-08-02)
13
+
14
+ ### Fix
15
+
16
+ - refuse to release on unrecognised arguments
17
+
18
+ ## v0.3.0 (2026-08-02)
19
+
20
+ ### Feat
21
+
22
+ - remove bump-on-push in favour of the git-release command
23
+ - git-release command that bumps before pushing
24
+
25
+ ## v0.2.0 (2026-08-02)
26
+
27
+ ### Feat
28
+
29
+ - update wording, relase python hooks
30
+
31
+ ### Fix
32
+
33
+ - skip hooks on the bump commit so pre-push cannot re-enter itself
34
+
35
+ ## v0.1.0 (2026-08-01)
36
+
37
+ ### Feat
38
+
39
+ - commitizen-early and bump-on-push pre-commit hooks
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danny Brown
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,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-a-grip
3
+ Version: 0.4.0
4
+ Summary: Pre-commit hooks that fail fast on bad commit messages, re-stage what they fix, and run your tests -- plus a release command and a cross-repo hook audit.
5
+ Project-URL: Homepage, https://github.com/dannybrown37/git-a-grip
6
+ Project-URL: Source, https://github.com/dannybrown37/git-a-grip
7
+ Project-URL: Issues, https://github.com/dannybrown37/git-a-grip/issues
8
+ Project-URL: Changelog, https://github.com/dannybrown37/git-a-grip/blob/main/CHANGELOG.md
9
+ Author-email: Danny Brown <dannybrown37@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: commitizen,git,hooks,pre-commit,ruff
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Classifier: Topic :: Software Development :: Version Control :: Git
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: commitizen>=4.0
21
+ Requires-Dist: pyyaml>=6.0
22
+ Provides-Extra: hooks
23
+ Requires-Dist: ruff>=0.6; extra == 'hooks'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # git-a-grip
27
+
28
+ Personal [pre-commit](https://pre-commit.com) hooks.
29
+
30
+ ```yaml
31
+ repos:
32
+ - repo: https://github.com/dannybrown37/git-a-grip
33
+ rev: v0.3.1
34
+ hooks:
35
+ - id: commitizen-early
36
+ - id: ruff-check
37
+ - id: ruff-format
38
+ - id: pytest
39
+ args: [tests/, -q]
40
+ ```
41
+
42
+ The commitizen and ruff hooks reach their tool through
43
+ `sys.executable -m <tool>` inside the env pre-commit builds for this repo, so
44
+ a consuming project needs no `cz` or `ruff` on PATH, no venv and no
45
+ `uv`/`uvx` of its own. (`pytest` is the exception — see below.)
46
+
47
+ Each hook pays only for what it uses: commitizen is a dependency of the
48
+ package, while ruff is declared by the two ruff hooks themselves, through
49
+ `additional_dependencies` in `.pre-commit-hooks.yaml`. You still pass
50
+ nothing. Pin your own ruff by setting `additional_dependencies:
51
+ [ruff==x.y.z]` on the hook.
52
+
53
+ ## `commitizen-early` (pre-commit stage)
54
+
55
+ Rejects a non-conventional commit message in about a third of a second,
56
+ instead of after the whole slow hook suite has run.
57
+
58
+ Git runs `pre-commit` -> `prepare-commit-msg` -> editor -> `commit-msg` as
59
+ separate invocations, so a `stages: [commit-msg]` commitizen hook can only
60
+ ever fail *after* your tests. Nothing in `.pre-commit-config.yaml` reorders
61
+ that. This hook instead recovers the message from the `git commit` process's
62
+ own argv while the pre-commit stage is still running, and checks it first.
63
+
64
+ Pair it with the upstream `commitizen` hook, which still catches the cases
65
+ argv cannot reach (interactive editor, merge, rebase) — this one exits 0 and
66
+ defers whenever it finds no message:
67
+
68
+ ```yaml
69
+ - repo: https://github.com/dannybrown37/git-a-grip
70
+ rev: v0.3.1
71
+ hooks:
72
+ - id: commitizen-early
73
+
74
+ - repo: https://github.com/commitizen-tools/commitizen
75
+ rev: v4.17.0
76
+ hooks:
77
+ - id: commitizen
78
+ stages: [commit-msg]
79
+ ```
80
+
81
+ Put it first and give it `fail_fast: true` if you want it to short-circuit
82
+ the rest of the stage.
83
+
84
+ ## `git-release` (command, not a hook)
85
+
86
+ Bump, tag and push, in that order, exiting 0. For repos that release from a
87
+ laptop rather than from CI. Give it an alias that says what it does — not
88
+ `gp`, which reads as `git push` right up until it publishes something:
89
+
90
+ ```bash
91
+ alias release='uvx --from git-a-grip git-release'
92
+ ```
93
+
94
+ This repo itself no longer uses it: releases here are cut by CI once the
95
+ checks on `main` pass (see below). The command remains for projects with no
96
+ such pipeline, where the alternative is remembering the four commands by
97
+ hand.
98
+
99
+ On `main` it bumps and pushes; on any other branch it just pushes, so it can
100
+ replace `git push` outright. Refuses to run against a dirty tree, and pushes
101
+ anyway when there are no bumpable commits.
102
+
103
+ This exists because a pre-push hook *cannot* do this cleanly. Git chooses
104
+ which sha to push before hooks run, so a commit created afterwards leaves two
105
+ options: cancel the push, or let git push the now-superseded sha and have it
106
+ rejected as a non-fast-forward. Both end in `error: failed to push some refs`
107
+ on top of a release that worked. Running as a command puts the bump before
108
+ the push and the problem disappears.
109
+
110
+ Configure what the bump rewrites via `[tool.commitizen]` in the consuming
111
+ repo (`version_provider`, `version_files`).
112
+
113
+ > A `bump-on-push` pre-push hook did this up to v0.2.1 and was removed in
114
+ > v0.3.0 for the reason above. If you pin an older rev, that hook still
115
+ > exists there; on upgrading, drop `- id: bump-on-push` and use this command.
116
+
117
+ ## `ruff-check` and `ruff-format` (pre-commit stage)
118
+
119
+ `ruff check --fix` and `ruff format`, with the fixes **re-staged** so they are
120
+ part of the commit you just made rather than a dirty working tree you have to
121
+ `git add` and amend. Only the violations ruff could not fix stop the commit,
122
+ via ruff's own exit code.
123
+
124
+ Pass ruff's flags through `args`:
125
+
126
+ ```yaml
127
+ - id: ruff-check
128
+ args: [--config, .ruff.toml]
129
+ ```
130
+
131
+ `--force-exclude` is always passed, so the `exclude` in your ruff config still
132
+ applies to the paths pre-commit hands over explicitly. The re-staged set is
133
+ narrowed by content digest — a file ruff did not change is never touched, and
134
+ because pre-commit stashes unstaged changes while a hook runs, re-adding a
135
+ file cannot sweep in an edit you deliberately left unstaged.
136
+
137
+ The ruff version is this repo's pinned dependency. To hold a repo at a
138
+ different one:
139
+
140
+ ```yaml
141
+ - id: ruff-format
142
+ additional_dependencies: [ruff==0.16.1]
143
+ ```
144
+
145
+ ## `pytest` (pre-commit stage)
146
+
147
+ Runs the test suite from the repo root. This hook can't use the isolated env
148
+ pre-commit builds here — a test suite needs the *consuming* project's
149
+ dependencies — so it shells out to a runner that resolves that environment,
150
+ `uv run pytest` by default. Everything else in `args` goes to pytest:
151
+
152
+ ```yaml
153
+ - id: pytest
154
+ args: [tests/, -q]
155
+
156
+ - id: pytest
157
+ args: ['--runner=uv run --extra api pytest', tests/, -q]
158
+ ```
159
+
160
+ It runs from the repo root regardless of where git was invoked, and drops the
161
+ `VIRTUAL_ENV`/`PYTHONPATH` that pre-commit exports for its own hook env —
162
+ which would otherwise point the runner at an environment holding none of your
163
+ project's dependencies. Narrow when it runs with `files:` (default
164
+ `^(src/|tests/).*`).
165
+
166
+ ## `pre-commit-audit` (command, not a hook)
167
+
168
+ Audit every local repo's pre-commit setup at once, so a hook that drifted or
169
+ never got installed shows up as a line rather than a surprise:
170
+
171
+ ```bash
172
+ uvx --from git-a-grip pre-commit-audit
173
+ ```
174
+
175
+ It walks the given trees (default: this repo's sibling directories), stops at
176
+ each git working tree, and reports four things: which of this repo's hooks
177
+ each project uses and the `rev` it pins, third-party hooks grouped by source
178
+ repo and rev, one-off `repo: local` hooks with their entry, and repos with no
179
+ usable config at all. `--json` emits the same data unformatted.
180
+
181
+ ```bash
182
+ pre-commit-audit ~/projects ~/work
183
+ pre-commit-audit --json | jq '.[] | select(.hooks == [])'
184
+ ```
185
+
186
+ ## Installing the commands
187
+
188
+ The hooks need no installation — pre-commit builds this repo an isolated env
189
+ from the `rev` you pin. The two commands (`git-release`, `pre-commit-audit`)
190
+ are ordinary console scripts, published to PyPI:
191
+
192
+ ```bash
193
+ uvx --from git-a-grip pre-commit-audit # one-off
194
+ uv tool install git-a-grip # both commands, on PATH
195
+ ```
196
+
197
+ That install carries only what the commands import — commitizen and pyyaml —
198
+ not the ruff the hooks use. To run the hook entry points by hand as well, ask
199
+ for the extra:
200
+
201
+ ```bash
202
+ uv tool install 'git-a-grip[hooks]'
203
+ ```
204
+
205
+ Straight from a tag works too, and is the way to run something not yet
206
+ released:
207
+
208
+ ```bash
209
+ uvx --from git+https://github.com/dannybrown37/git-a-grip@v0.3.1 git-release
210
+ ```
211
+
212
+ ## Releasing
213
+
214
+ Merge to `main`. That is the whole gesture.
215
+
216
+ `ci.yml` runs lint, tests and the install proofs on the merged commit; only
217
+ if they all pass does its `bump` job run `cz bump`, which writes the version
218
+ and changelog, commits, and tags. Pushing that tag triggers `publish.yml`,
219
+ which builds and uploads to PyPI via trusted publishing. A push with no
220
+ bumpable commits (docs, chores) ends after the checks and releases nothing.
221
+
222
+ Nothing is tagged before the checks pass, so a red build cannot leave a
223
+ version number stranded on a release that never shipped.
224
+
225
+ ## Development
226
+
227
+ ```bash
228
+ uv sync
229
+ uv run pytest
230
+ ```
231
+
232
+ This repo eats its own dog food: both hooks are wired into its own
233
+ `.pre-commit-config.yaml`.