Files
pytorch/scripts/setup_hooks.py
Zain Rizvi 1b5fdb23b9 [BE] Add pre-push hook for lintrunner to the PyTorch repo (#158389)
Adds a pre-commit hook (technically a pre-push hook) to the PyTorch repo.
**This is currently an opt-in feature**, which one can opt into by running `python scripts/setup_hooks.py` locally.

### Features
- **Run Lintrunner Before Push**: Before every `git push`, automatically runs lintrunner on your changes.
  - Really need to skip the checks? Run `git push --no-verify`
- **Consistent, Isolated, Lintrunner Environment**: During pre-push, Lintrunner runs in it's own virtual en environment that contain all lintrunner dependencies in a consistent, isolated environment.  No more lintrunner failures because you created a new .venv. (Did you know you needed to run `lintrunner init` every time you make a new .venv?)
- **Dependencies Automatically Updated**: If .lintrunner.toml is updated, this will automatically re-run `lintrunner init` to ensure you install the latest dependencies specified

### Installation
- Run `python scripts/setup_hooks.py`. Now every `git push` will first run lintrunner.

### Additional details
- The lintrunner used by the pre-push hook runs in a special per-repo virtual environment managed by the commit-hook tool located under `$USER/.cache/pre-commit`
- Does not affect your regularly used lintrunner
  - Manual invocations of lintrunner will continue to depend on your local environment instead of the special pre-push one. If there's enough interest, we could explore consolidating them.
- Does not run `lintrunner -a` for you.
  - You still need to manually run that (can be changed later though!)
- Have staged/unstaged changes? No worries
  - This runs `git stash` before running the pre-commit hooks and pops back your changes afterwards, so only the changes actaully being pushed will be tested

### Downsides
- No streaming UI updates
  - While you still get the same output from lintrunner that you're used to, the commit-hook framework doesn't show any output while lintrunner is actually running. Instead, it shows the entire output after linter has completed execution, which could be a few minutes (especially if it has to run `lintrunner init` first)
- `uv` installation is required to run the setup script. The setup script will ask users to install uv if it's not available.
  - This is required to be able to install the pre-commit package in a safe way that's available no matter what .venv you are running in.

### Opting out
- Disable hook for a single push: Run `git push --no-verify`
- Disable hook permanently: If something goes wrong and you need to wipe your setup:
  - Delete the `$USER/.cache/pre-commit` folder and the `.git/hooks/pre-push` file in your local repo.
  - You can now rerun `python scripts/setup_hooks.py` to setup your git push hook again if you want.

### Potential Future Changes
Things that could be done to make this even better if folks like these ideas:
- Automatic setup
  - Our `CONTRIBUTING.md` file tells devs to run `make setup-env`.  That could be a good entry point to hook the installation into
- Fix the console output streaming
- Make every lintrunner invocation (including manual ones) use the same repo-specific venv that the commit-hook uses.
Pull Request resolved: https://github.com/pytorch/pytorch/pull/158389
Approved by: https://github.com/seemethere
2025-07-18 19:55:35 +00:00

140 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Bootstrap Git prepush hook.
✓ Requires uv to be installed (fails if not available)
✓ Installs/updates precommit with uv (global, venvproof)
✓ Registers the repo's prepush hook and freezes hook versions
Run this from the repo root (inside or outside any project venv):
python scripts/setup_hooks.py
"""
from __future__ import annotations
import shutil
import subprocess
import sys
from pathlib import Path
# ───────────────────────────────────────────
# Helper utilities
# ───────────────────────────────────────────
def run(cmd: list[str]) -> None:
print(f"$ {' '.join(cmd)}")
subprocess.check_call(cmd)
def which(cmd: str) -> bool:
return shutil.which(cmd) is not None
def ensure_uv() -> None:
if which("uv"):
# Ensure the path uv installs binaries to is part of the system path
print("$ uv tool update-shell")
result = subprocess.run(
["uv", "tool", "update-shell"], capture_output=True, text=True
)
if result.returncode == 0:
# Check if the output indicates changes were made
if (
"Updated" in result.stdout
or "Added" in result.stdout
or "Modified" in result.stdout
):
print(
"⚠️ Shell configuration updated. You may need to restart your terminal for changes to take effect."
)
elif result.stdout.strip():
print(result.stdout)
return
else:
sys.exit(
f"❌ Warning: uv tool update-shell failed: {result.stderr}. uv installed tools may not be available."
)
sys.exit(
"\n❌ uv is required but was not found on your PATH.\n"
" Please install uv first using the instructions at:\n"
" https://docs.astral.sh/uv/getting-started/installation/\n"
" Then rerun python scripts/setup_hooks.py\n"
)
def ensure_tool_installed(tool: str, force_update: bool = False) -> None:
"""
Checks to see if the tool is available and if not (or if force update requested) then
it reinstalls it.
Returns: Whether or not the tool is available on PATH. If it's not, a new terminal
needs to be opened before git pushes work as expected.
"""
if force_update or not which(tool):
print(f"Ensuring latest {tool} via uv …")
run(["uv", "tool", "install", "--force", tool])
if not which(tool):
print(
f"\n⚠️ {tool} installation succeed, but it's not on PATH. Launch a new terminal if your git pushes don't work.\n"
)
if sys.platform.startswith("win"):
print(
"\n⚠️ Lintrunner is not supported on Windows, so there are no pre-push hooks to add. Exiting setup.\n"
)
sys.exit(0)
# ───────────────────────────────────────────
# 1. Install dependencies
# ───────────────────────────────────────────
ensure_uv()
# Ensure pre-commit is installed globally via uv
ensure_tool_installed("pre-commit", force_update=True)
# Don't force a lintrunner update because it might break folks
# who already have it installed in a different way
ensure_tool_installed("lintrunner")
# ───────────────────────────────────────────
# 2. Activate (or refresh) the prepush hook
# ───────────────────────────────────────────
# ── Activate (or refresh) the repos prepush hook ──────────────────────────
# Creates/overwrites .git/hooks/prepush with a tiny shim that will call
# `pre-commit run --hook-stage pre-push` on every `git push`.
# This is why we need to install pre-commit globally.
#
# The --allow-missing-config flag lets pre-commit succeed if someone changes to
# a branch that doesn't have pre-commit installed
run(
[
"uv",
"tool",
"run",
"pre-commit",
"install",
"--hook-type",
"pre-push",
"--allow-missing-config",
]
)
# ── Pin remotehook versions for reproducibility ────────────────────────────
# (Note: we don't have remote hooks right now, but it future-proofs this script)
# 1. `autoupdate` bumps every remote hooks `rev:` in .pre-commit-config.yaml
# to the latest commit on its default branch.
# 2. `--freeze` immediately rewrites each `rev:` to the exact commit SHA,
# ensuring all contributors and CI run identical hook code.
run(["uv", "tool", "run", "pre-commit", "autoupdate", "--freeze"])
print(
"\n✅ precommit is installed globally via uv and the prepush hook is active.\n"
" Lintrunner will now run automatically on every `git push`.\n"
)