From e71548bbb99f0ca7c99046ad3cde299f529c1581 Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Sat, 2 Nov 2024 11:14:52 +0100 Subject: [PATCH] Post-merge hook for lockfile sync with pre-check on file changes to avoid unnecessary overhead Fixes #17 --- .gitignore | 1 + .pre-commit-hooks.yaml | 13 +++++++++ README.md | 36 ++++++++++++++++++++++++ hooks/uvsync.py | 62 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 8 ++++++ 5 files changed, 120 insertions(+) create mode 100644 hooks/uvsync.py diff --git a/.gitignore b/.gitignore index 309de57..adeeb08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ *.pyc .venv +*.egg-info \ No newline at end of file diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 8e1da5a..1a58e1c 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -28,3 +28,16 @@ pass_filenames: false additional_dependencies: [] minimum_pre_commit_version: "2.9.2" +- id: uv-sync + name: uv-sync + description: "Automatically run 'uv sync' on your repository after a checkout, pull or rebase" + entry: uv-sync-hook + args: + - "--locked" + language: python + always_run: true + pass_filenames: false + minimum_pre_commit_version: "2.9.2" + stages: + - post-checkout + - post-merge diff --git a/README.md b/README.md index 7a59076..2eb30e4 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,42 @@ To export to an alternative file, modify the `args`: args: ["--frozen", "--output-file=requirements-custom.txt"] ``` +To synchronize your dependencies upon branch checkout or pull: + +```yaml +default_install_hook_types: + - pre-commit + - post-checkout + - post-merge + - post-rewrite +default_stages: + - pre-commit +- repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.5 + hooks: + - id: uv-sync +``` + +To synchronize all dependencies in a workspace. +The hook assumes the lock file is properly generated and will not look up for nested files. + +```yaml +default_install_hook_types: + - pre-commit + - post-checkout + - post-merge + - post-rewrite +default_stages: + - pre-commit +- repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.5 + hooks: + - id: uv-sync + args: ["--frozen,"--all-packages"] +``` + ## License uv-pre-commit is licensed under either of diff --git a/hooks/uvsync.py b/hooks/uvsync.py new file mode 100644 index 0000000..dcdc7c0 --- /dev/null +++ b/hooks/uvsync.py @@ -0,0 +1,62 @@ +""" +UV sync hook + +Should run as post-merge, post-checkout or post-rewrite hook +Detect dependency changes from previous commit and run uv sync if necessary +""" + +import os +import subprocess +import sys + +GIT_DIFF_CMD = ["git", "diff", "--quiet"] +UV_FILES = ["pyproject.toml", "uv.lock", "uv.toml"] + +def git_diff(from_ref: str, to_ref: str) -> subprocess.CompletedProcess: + """ + Run git diff --quiet from_ref to_ref -- pyproject.toml uv.lock uv.toml + Return code 0 means no changes, 1 means changes + """ + return subprocess.run([*GIT_DIFF_CMD, from_ref, to_ref, "--", *UV_FILES]) + + +def detect_lock_changes() -> bool: + """ + According to [documentation](https://pre-commit.com/#post-checkout), + post-checkout hook receives 3 environment variables: + PRE_COMMIT_FROM_REF, PRE_COMMIT_TO_REF, PRE_COMMIT_CHECKOUT_TYPE + As obscure as is the third one, we refer to [git documentation](https://git-scm.com/docs/githooks#_post_checkout) + and know its value is 1 for branch checkout and 0 for file checkout. + + The post-merge hook receives 1 environment variable: + PRE_COMMIT_IS_SQUASH_MERGE: a flag indicating whether the merge is a squash merge or not. + We don't care about it, we'll just use it to detect post-merge hook. + + The post-rewrite hook receives 1 environment variable: + PRE_COMMIT_REWRITE_COMMAND: a flag indicating the rewrite command, it can be rebase or amend. + """ + + # Scenario 1: post-checkout branch + # only branch checkout is relevant + # PRE_COMMIT_FROM_REF and PRE_COMMIT_TO_REF differ + if (from_ref := os.getenv("PRE_COMMIT_FROM_REF")) != ( + to_ref := os.getenv("PRE_COMMIT_TO_REF") + ): + return git_diff(from_ref, to_ref).returncode == 1 + + # Scenario 2: post-merge + # Whatever PRE_COMMIT_IS_SQUASH_MERGE is, we need to compare the current HEAD with its parent + if "PRE_COMMIT_IS_SQUASH_MERGE" in os.environ: + return git_diff("HEAD^", "HEAD").returncode == 1 + + # Scenario 3: post-rewrite + if os.getenv("PRE_COMMIT_REWRITE_COMMAND") == "rebase": + return git_diff("ORIG_HEAD", "HEAD").returncode == 1 + +def main(): + if detect_lock_changes(): + os.execvp("uv", ["uv", "sync", *sys.argv[1:]]) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 0565299..1710412 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,18 @@ [project] name = "uv-pre-commit" version = "0.0.0" +requires-python = ">=3.11" dependencies = [ "uv==0.5.1", ] +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project.scripts] +uv-sync-hook = "hooks:uvsync.main" + [project.optional-dependencies] dev = [ "packaging~=23.1",