mirror of
https://github.com/pytorch/pytorch.git
synced 2025-10-20 21:14:14 +08:00
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
140 lines
5.2 KiB
Python
140 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Bootstrap Git pre‑push hook.
|
||
|
||
✓ Requires uv to be installed (fails if not available)
|
||
✓ Installs/updates pre‑commit with uv (global, venv‑proof)
|
||
✓ Registers the repo's pre‑push 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 pre‑push hook
|
||
# ───────────────────────────────────────────
|
||
|
||
# ── Activate (or refresh) the repo’s pre‑push hook ──────────────────────────
|
||
# Creates/overwrites .git/hooks/pre‑push 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 remote‑hook versions for reproducibility ────────────────────────────
|
||
# (Note: we don't have remote hooks right now, but it future-proofs this script)
|
||
# 1. `autoupdate` bumps every remote hook’s `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✅ pre‑commit is installed globally via uv and the pre‑push hook is active.\n"
|
||
" Lintrunner will now run automatically on every `git push`.\n"
|
||
)
|