Skip to content

Commit

Permalink
Post-merge hook for lockfile sync with pre-check on file changes to a…
Browse files Browse the repository at this point in the history
…void unnecessary overhead

Fixes astral-sh#17
  • Loading branch information
lachaib committed Nov 11, 2024
1 parent 716ae58 commit e71548b
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__
*.pyc
.venv
*.egg-info
13 changes: 13 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions hooks/uvsync.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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",
Expand Down

0 comments on commit e71548b

Please sign in to comment.