Skip to content

k88hudson-cfa/precommit-example

Repository files navigation

A "native-first" philosophy for pre-commit

pre-commit is designed to support cross-platform, language-agnostic commit hooks. While this is useful for some tasks that truly need to be consistent across languages and repositories, the philosophy demonstrated here is different. It starts with the following principle:

Good tooling is as little tooling as possible

"Simple" is a deceptive concept, because tools can appear to be simple while simply hiding or delaying complexity until things change or go wrong. It is important to consider the entire set of environments you are maintaining, which includes local dev, git hooks, CI, and editor integration, as well as debugging errors and maintaining or adding new tasks.

I believe the default usage of pre-commit (which encodes commands in separately-maintained repositories) has some problems. Instead, I recommend a "native-first" approach that includes the following:

  • Using a modern, reliable package/environment manager for the language you are working in (uv for python, cargo for Rust, pnpm for Typescript, etc.)
  • A human-readable, minimal set of tasks that serves as the source of truth
  • Consistency across local development, GitHub actions, and hooks
  • Preferring the native / default configuration for each tool so that it is easy to look up in docs and debug

Implementation

First, you should choose a dependency manager and add dependencies like linters, test runners etc. natively. Where possible, use the default suggested configuration options for those tools. For example, for a python project, you might choose uv for dependency management and ruff for linting/formatting. You would add ruff by running uv add ruff, and then a minimal set of configuration to the [tool.ruff] section of pyproject.toml

Next, you should think about what environments you need to support. This is probably some or all of:

  • Local development via command line (Linux, Mac, maybe Windows?)
  • An IDE/editor (e.g., vscode, which can apply formatting on save)
  • Hooks (via pre-commit)
  • CI (GitHub via GitHub actions)
  • Cloud infrastructure (e.g., deploying an app or running batch jobs)

Define tasks

You should then define a set of tasks (using a tool like just, Make, or potentially an integrated script runner for your package manager) that cover all the basic things you need to do.

You may have slightly different tasks for the environments you listed above.

An example:

# Install all dependencies
install:
    uv lock --upgrade
    uv sync --all-extras --frozen

# Run the application
run: install
    uv run main.py

# Run unit tests
test *args: install
    uv run pytest {{ args }}

# Read-only check for formatting/lint rules
check:
    uv run ruff format --check
    uv run ruff check --fix

# Modify files to comply with formatting/lint rules
fix: install
    uv run ruff format
    uv run ruff check --fix

Configure pre-commit

In addition to your base tasks, define some tasks specifically to be called in your hooks. These will likely just reference other tasks:

pre-commit: install
    @just check

pre-push: install
    @just check
    @just test

You may also want to add a setup task to help people install or update hooks. Note that if most of the details are in the tasks themselves, you will not have to update hooks very often.

setup: install hook

hook: install
    uv run pre-commit install --install-hooks --overwrite

unhook: install
    uv run pre-commit uninstall

Finally, you need to set up .pre-commit-config.yaml to reference your tasks:

repos:
  - repo: local
    hooks:
      - id: pre-commit
        name: Runs before every commit
        entry: just
        args: [pre-commit]
        language: system
        pass_filenames: false

      - id: pre-push
        name: Runs before pushing to a remote
        entry: just
        args: [pre-push]
        language: system
        pass_filenames: false
        stages: [pre-push]

Note that hooks can be optionally installed; it is always possible to call commands directly from the command line (e.g. just check)

Set up GitHub actions

GitHub actions (for CI) should also reference tasks. For example:

name: CI

on:
  push:
    branches: main
  pull_request:
    branches: main

jobs:
  check-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install just
        uses: extractions/setup-just@v3

      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: "latest"

      - name: Set up python
        run: just setup-python

      - name: Install dependencies
        run: just install

      - name: Run linting/formatting
        run: just check

      - name: Run tests
        run: just test

Editor setup

Example

The rest of this README describes how to setup and run the example in this repository.

Prerequisites

This project can be developed on Mac, Linux, WSL, or Windows, and requires the following dependencies:

  • just for task running
  • uv for package and environment management

We use uv for python versioning.

Mac, Linux, WSL

Install just (or check the official docs)

OS Command
Mac brew install just
Debian/Ubuntu apt install just
Arch pacman -S just

Install uv (or check the official docs):

curl -LsSf https://astral.sh/uv/install.sh \| sh

Windows

Install uv:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 \| iex"`

If you have a shell installed (Git for Windows, GitHub Desktop, or Cygwin), you can install and use just:

[TODO does this work properly?]

curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST

If you don't, you can call uv commands directly from powershell (for example, install of calling just test you can run uv run pytest). Take a look at the justfile to see what the corresponding commands are.

Setup

To set up git pre-commit/pre-push hooks, a compatible version of python and dependencies for this project, run the following command:

just setup

If you wish to skip setting up hooks, you can skip this.

You can now call just --list to see all available commands.

About

A different philosophy for precommit

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published