## What Makes the local **pre-push** git hook enforce the same branch-coverage gate as CI, so contributors catch coverage regressions before they push (not just in CI). Two files change: the hook (`.pre-commit-config.yaml`) and a doc note (`CONTRIBUTING.md`). ## The hook (`.pre-commit-config.yaml`, `run-tests` pre-push hook) Mirrors `.github/workflows/test.yml`: 1. Runs the full suite with **real torch** + branch coverage: `pytest --cov=NRS --cov-branch --cov-report=xml`. 2. Then the diff gate: `diff-cover coverage.xml --compare-branch=origin/main --branch-coverage --fail-under=90` — **blocks the push if changed code drops below 90% branch coverage**. 3. **Graceful skip** with a warning if `uv` isn't installed (unchanged behavior); **fails loudly** if `origin/main` can't be resolved (nothing to diff against). 4. Cleans up `coverage.xml`/`.coverage` on both success and failure paths, while preserving the exit code so a failing gate still blocks. All existing hooks are untouched: `prevent-push-to-main`, `prevent-commit-to-main`, ruff, and the standard hooks. Only the `run-tests` entry/name changed. Coverage stays on the **pre-push** stage only — commits remain fast. ### Uses `uvx`, not `uv run` (important) The invocations use `uvx --with torch --with pytest-cov --with diff-cover ...`. An earlier `uv run` version was verified to **break the push**: `uv run` syncs the project env and mutates `uv.lock` mid-hook, which conflicts with pre-commit's stash/restore and aborts the push (`Stashed changes conflicted with hook auto-fixes... Rolling back`). `uvx` runs in an ephemeral env and never touches `uv.lock` — matching how the project runs tests locally everywhere else. ## Docs (`CONTRIBUTING.md`) Added a short note right after the `pre-commit install --hook-type pre-push` instruction explaining the gate, the `uv` requirement (skips with a warning otherwise), and that `origin/main` must be fetched for the diff. ## Validation - Ran the hook's exact command: **45 tests pass**, diff-cover passes; `git status` after confirms `uv.lock` is **not** mutated (the stash-conflict path can't recur). - `uvx pre-commit run --hook-stage pre-push` completes with no stash-conflict rollback. - **This very PR's push exercised the hook live** — it ran the gate and pushed cleanly. - Only `.pre-commit-config.yaml` and `CONTRIBUTING.md` changed; `pyproject.toml`/`uv.lock`/tests/`NRS/` untouched. ## Out-of-scope observation The `ruff-format` hook has no `stages:` restriction, so a manual `--all-files` dry-run reformats files broadly. Pre-existing; not addressed here.
53 lines
2.3 KiB
YAML
53 lines
2.3 KiB
YAML
# See https://pre-commit.com for more information
|
|
repos:
|
|
# Standard pre-commit hooks
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v6.0.0
|
|
hooks:
|
|
- id: trailing-whitespace
|
|
- id: end-of-file-fixer
|
|
- id: check-yaml
|
|
- id: check-added-large-files
|
|
args: ['--maxkb=1000']
|
|
- id: check-merge-conflict
|
|
- id: mixed-line-ending
|
|
args: ['--fix=lf']
|
|
|
|
# Ruff linter and formatter
|
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
rev: v0.15.12
|
|
hooks:
|
|
- id: ruff
|
|
args: [--fix, --exit-non-zero-on-fix]
|
|
- id: ruff-format
|
|
|
|
# Main branch protection (pre-commit)
|
|
- repo: local
|
|
hooks:
|
|
- id: prevent-commit-to-main
|
|
name: Prevent commits to main branch
|
|
entry: bash -c 'BRANCH=$(git branch --show-current); if [ "$BRANCH" = "main" ]; then echo "ERROR - Direct commits to main are not allowed. Create a feature branch instead."; exit 1; fi'
|
|
language: system
|
|
stages: [pre-commit]
|
|
always_run: true
|
|
pass_filenames: false
|
|
|
|
# Test execution (pre-push only)
|
|
- repo: local
|
|
hooks:
|
|
- id: run-tests
|
|
name: Run pytest with branch-coverage gate
|
|
entry: bash -c 'if ! command -v uv > /dev/null 2>&1; then echo "WARNING - uv not found, skipping tests and coverage gate"; exit 0; fi; if ! git rev-parse --verify --quiet origin/main > /dev/null 2>&1; then echo "ERROR - origin/main not resolvable locally; fetch origin main and retry"; exit 1; fi; uvx --with torch --with pytest-cov --with diff-cover pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing tests/; status=$?; if [ $status -ne 0 ]; then rm -f coverage.xml .coverage; exit $status; fi; uvx --with diff-cover diff-cover coverage.xml --compare-branch=origin/main --branch-coverage --fail-under=90; cov_status=$?; rm -f coverage.xml .coverage; exit $cov_status'
|
|
language: system
|
|
stages: [pre-push]
|
|
always_run: true
|
|
pass_filenames: false
|
|
|
|
- id: prevent-push-to-main
|
|
name: Prevent pushes to main branch
|
|
entry: bash -c 'BRANCH=$(git branch --show-current); if [ "$BRANCH" = "main" ]; then echo "ERROR - Direct pushes to main are not allowed. Use pull requests instead."; exit 1; fi'
|
|
language: system
|
|
stages: [pre-push]
|
|
always_run: true
|
|
pass_filenames: false
|