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.
- git_a_grip-0.4.0/.github/workflows/ci.yml +120 -0
- git_a_grip-0.4.0/.github/workflows/publish.yml +53 -0
- git_a_grip-0.4.0/.gitignore +6 -0
- git_a_grip-0.4.0/.pre-commit-config.yaml +58 -0
- git_a_grip-0.4.0/.pre-commit-hooks.yaml +47 -0
- git_a_grip-0.4.0/.ruff.toml +67 -0
- git_a_grip-0.4.0/CHANGELOG.md +39 -0
- git_a_grip-0.4.0/LICENSE +21 -0
- git_a_grip-0.4.0/PKG-INFO +233 -0
- git_a_grip-0.4.0/README.md +208 -0
- git_a_grip-0.4.0/pyproject.toml +66 -0
- git_a_grip-0.4.0/src/git_a_grip/__init__.py +1 -0
- git_a_grip-0.4.0/src/git_a_grip/audit.py +288 -0
- git_a_grip-0.4.0/src/git_a_grip/commitizen_early.py +153 -0
- git_a_grip-0.4.0/src/git_a_grip/cz.py +23 -0
- git_a_grip-0.4.0/src/git_a_grip/pytest_hook.py +92 -0
- git_a_grip-0.4.0/src/git_a_grip/release.py +148 -0
- git_a_grip-0.4.0/src/git_a_grip/restage.py +55 -0
- git_a_grip-0.4.0/src/git_a_grip/ruff_hooks.py +57 -0
- git_a_grip-0.4.0/tests/test_audit.py +150 -0
- git_a_grip-0.4.0/tests/test_commitizen_early.py +56 -0
- git_a_grip-0.4.0/tests/test_packaging.py +74 -0
- git_a_grip-0.4.0/tests/test_pytest_hook.py +79 -0
- git_a_grip-0.4.0/tests/test_release.py +149 -0
- git_a_grip-0.4.0/tests/test_restage.py +63 -0
- git_a_grip-0.4.0/tests/test_ruff_hooks.py +110 -0
- git_a_grip-0.4.0/uv.lock +517 -0
|
@@ -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,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
|
git_a_grip-0.4.0/LICENSE
ADDED
|
@@ -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`.
|