Skip to content

Official Air pre-commit hook #269

Closed
Closed

Description

@DavisVaughan

https://github.com/pre-commit/pre-commit

Pre-commit itself seems to be written in python, so requires python to install
https://pre-commit.com/#install

Ruff example: https://github.com/astral-sh/ruff-pre-commit

I think the vibe of how the Ruff one works is that they have this pre commit hook yaml:
https://github.com/astral-sh/ruff-pre-commit/blob/main/.pre-commit-hooks.yaml

Note the ruff format --force-exclude call.

And I think ruff itself gets installed from being listed as a requirement here:
https://github.com/astral-sh/ruff-pre-commit/blob/main/pyproject.toml

They do tag releases synced to each ruff version
https://github.com/astral-sh/ruff-pre-commit/tags

And this is how a user would reference that tag and call the hook:
https://github.com/astral-sh/ruff/blob/568cf88c6c5b5551a675ae2b13deedec0fe226cb/.pre-commit-config.yaml#L76-L79

Activity

  1. lorenzwalthert commented on Feb 28, 2025

    @lorenzwalthert

    @DavisVaughan your observations are all correct IMO. Python must not necessarily be available, you can also install pre-commit with brew for example, then it will manage the python installation I think. Note that pre-commit always clones a repo using a tag or a hash from rev: field in the .pre-commit-config.yaml, which is then installed (with all its dependencies).

    Unifying approach
    Hence, you could also place the .pre-commit-config.yaml file in the posit-dev/air repo directly. Then, if you push new tags to this repo (which I guess you do anyways if you release a new version), pre-commit autoupdate will recognise them and pre-commit autoupdate will pick it up. Note pre-commit autoupdate --bleeding-edge or so uses always the latest commit hash from the default branch, but few people seem to use that.

    Splitting approach
    You could also follow ruff and create a repo like posit-dev/air-pre-commit. If you don't publish the crates (which I assume you don't currently according to #98), this approach is not available to you I think, since your air-pre-commit repo can't list air as a dependency that can be resolved and installed by a package manager for rust. Here's how you'd make hook updates available to your users:

    • bump the dependency of the posit-dev/air-pre-commit on air.
    • push a new tag corresponding to the new version to posit-dev/air-pre-commit to ensure that pre-commit autoupdate fetches the latest version of the hook.

    There are many examples where the pre-commit hooks configs are directly in the root of the actual project. I'd say considerations pro and con of unifying vs splitting:

    • easier update process if unifying (as described above).
    • git clone a lightweight git repo and then installing air as a dependency from a package server like pypi where potentially binaries are available (splitting) is probably faster than cloning.
    • more flexibility with splitting (as repos can evolve separately).
  2. DavisVaughan commented on Feb 28, 2025

    @DavisVaughan
    CollaboratorAuthor

    Noting that what we really need is something like this pre-commit/pre-commit#1453 to download pre installed binaries using pre-commit

  3. DavisVaughan commented on Feb 28, 2025

    @DavisVaughan
    CollaboratorAuthor

    IIUC with Unifying Approach we'd set language: rust and pre-commit would cargo install --bins on the fly for our users (even bootstrapping rustup as required). While this is nice, I think we should not do that because we've gone through all the work of making easy to install binaries - we should use them if at all possible.

    So that likely leaves us with Splitting Approach, where we need to decide which language to use. A few ideas:

    language: R

    Create a pseudo-R package at posit-dev/air-pre-commit. The R package would be in charge of downloading the correct binary depending on the platform, and then running it. This would be pretty simple I think, we just need to map the OS and Architecture to the right GitHub Artifact URL

    https://github.com/posit-dev/air/releases/download/<VERSION>/air-<ARCH>-<OS>.tar.gz
    

    For VERSION, we'd put a constant in the package as VERSION <- 0.4.1 which would correspond to the tag that we release at, so when people set rev: 0.4.1 in their pre-commit-config.yml they get the right version.

    I'm not exactly sure how caching of the binary between commits would work here. It sounds like maybe it is up to the R package, and we could use something like rappdirs for the location.

    IIUC pre-commit would build the R package the first time around and then reuse the built package for subsequent calls until the tag is updated

    language: Python

    Be like ruff and compile binary wheels and upload an air python package to pypi that just makes air available. Don't love this idea as it is a lot of extra work on our end for little benefit.

    language: system

    https://pre-commit.com/#system

    We could write a "system" hook that assumes that the user has already installed the binary. In this case:

    • We could manage it through the posit-dev/air repo (but there are still arguments for keeping it separate, lightweight clone times for example)
    • It would be very fast as we don't have to wait for R to start up and shut down
    • But it would go against the ethos of pre-commit a bit by requiring that the air binary already be installed
    • It does not let you easily pin a version, which seems like a footgun

    I don't mind this that much, and if language: download ever gets implemented we could switch to that.

  4. lorenzwalthert commented on Feb 28, 2025

    @lorenzwalthert

    So that likely leaves us with Splitting Approach, where we need to decide which language to use. A few ideas:

    With both unifying and splitting, you will point to a git repo and specify a language property, that's not unique to splitting. If you choose language: rust , the git repo pre-commit will install from has to be installable with cargo (into some pre-commit managed directory in your system's cache, not the working directory). So if you have language: rust:

    • With the unifying approach, the git repo from posit-dev/air is cloned and installed from source. The package has to expose the CLI that you then put into entry:.
    • With the splitting approach, the rust package at posit-dev/air-pre-commit exists for the mere reason of having a dependency on the package you actually want to use and installing that dependency (with a package manager from a remote package repo) will expose a CLI program like ruff that you then put into entry: field (FWIW for language: r it's not a CLI program, but for many other languages it is). This in my understanding would not work currently, since you don't distribute air through standard rust package repositories (like CRAN for R).

    To sum it up: whether or not you choose a splitting or unifying approach, you need to choose a pre-commit language. But if you choose unifying approach and language:r, posit-dev/air has to be a rust and R Package at the same time which brings additional complexity you want to avoid.

  5. lorenzwalthert commented on Feb 28, 2025

    @lorenzwalthert

    On which pre-commit language to use: pre-commit supports standard package management mechanisms for R, rust, python and other languages. If airs usage is limited to the command line and positron / RStudio, you may not see a lot of benefit distributing it through standard package repos like CRAN, pypi etc. {styler} has been used in many different third-party tools that want to format code and I think it was beneficial to have it installable as an R package through standard mechanisms. But that's up to you. In that regard, the fact that {styler} is a pure R implementation is also a benefit. Also, a number of R users don't feel comfortable leaving the R console and an R API was certainly beneficial for {styler}.

    If you go for language: r, you get the side benefit of R bindings for other use cases than just pre-commit. As per the docs, the repo needs to use {renv} to manage dependencies (none in your case). As you correctly point out, you would update the tag of the repo in sync with the pointer to the binary in https://github.com/posit-dev/air/releases/download/<VERSION>/air-<ARCH>-<OS>.tar.gz in the R package source code (or just depend on DESCRIPTION to read off the version value that you should keep in sync with the tag).

    I'm not exactly sure how caching of the binary between commits would work here. It sounds like maybe it is up to the R package, and we could use something like rappdirs for the location.

    There is one virtual environment per hook repo and rev for 2nd class languages and lower, so if you use language: r or language: rust, a new virtual environment is created if rev: changes as a consequence of pre-commit autoupdate. For R specifically, there is also a health check, which means if the R version changes (and R packages installed with previous R version are not available anymore), the repo will be re-installed.

    IIUC pre-commit would build the R package the first time around and then reuse the built package for subsequent calls until the tag is updated

    Correct.

    language: system indeed seems easy but problematic, and language: r is quite slow, which kind of defeats the purpose of air 😀. I'd go for language: rust, I don't think the overhead with rust bootstrapping is that big or complicated for the user, in particular if they use other hooks already (like ruff). Plus it's much easier to manage on your side, in particular with the unified approach.
    Also, you can even expose multiple hooks in the hook repo, one with rust and one with system. The entry: can be the same. The user can also override language from .pre-commit-hook.yaml in the hook repo in his .pre-commit-config.yaml but it's not really recommended.

  6. lorenzwalthert commented on Feb 28, 2025

    @lorenzwalthert

    Sorry my comments get so long. Hope they are not too convoluted.

  7. SamEdwardes commented on Mar 20, 2025

    @SamEdwardes

    In lieu of an official pre-commit hook, you can use this if you already have air installed:

    repos:
        # R
        - repo: local
          hooks:
              # https://github.com/posit-dev/air
              - id: air
                name: Format R code with air
                entry: air format .
                language: system
                files: \.R$
  8. lorenzwalthert commented on Mar 21, 2025

    @lorenzwalthert

    @SamEdwardes without pass_filenames: False, I think you are formatting the working directory and the files staged for commit. I think you probably just want to format the staged files, so probably remove the . in air format .?

  9. SamEdwardes commented on Mar 22, 2025

    @SamEdwardes

    Thank you for the suggestion! I am new to pre-commit and appreciate the feedback.

  10. grst commented on Apr 24, 2025

    @grst

    Splitting approach
    You could also follow ruff and create a repo like posit-dev/air-pre-commit. If you don't publish the crates (which I assume you don't currently according to #98), this approach is not available to you I think, since your air-pre-commit repo can't list air as a dependency that can be resolved and installed by a package manager for rust. Here's how you'd make hook updates available to your users:

    This has another advantage: pre-commit always updates to the latest tag, no matter if it's a pre-release or not. By splitting the repos, one can create only tags for stable releases you actually want users to be updated to.

    language: Python
    Be like ruff and compile binary wheels and upload an air python package to pypi that just makes air available. Don't love this idea as it is a lot of extra work on our end for little benefit.

    I think that's a great idea! Also consider that pip is a widely abundant package manager that can be used without admin priviledges. And one could even use uv for installing air on-the-fly: uvx air format .

    Why do you think it would be a lot of extra work? In the end it's just one github action that publishes a release on PyPI whenever a tag/release is created on Github.

  11. tanho63 commented on Jul 25, 2025

    @tanho63

    Stumbled on this via Davis on bluesky, thanks for the comprehensive exploration! One solution that would work for me and my org in particular (but I can understand why this isn't a good global solve) is using language: docker_image.

    Minimal Dockerfile here:

    FROM ubuntu:24.04
    WORKDIR /air
    RUN apt-get update \
      && apt-get install -qq -y curl \
      && curl -LsSf https://github.com/posit-dev/air/releases/latest/download/air-installer.sh | sh \
      && apt-get clean \
      && rm -rf /var/lib/apt/lists/
    
    ENTRYPOINT ["/root/.local/bin/air", "format"]

    and usage like:

    repos:
      repo: local
      hooks: 
        -   id: air-style-files
            name: style files with air
            entry: tan/air:latest
            language: docker_image
            files: '\.[R|r]$'

    gives me

    > git commit -m 'test air'
    style files with air.....................................................Failed
    - hook id: air-style-files
    - files were modified by this hook
    
    > git status
    On branch tan/air
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
            modified:   .pre-commit-config.yaml
            modified:   R/update.R
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   R/update.R
    

    which behaves more like what the original style-files pre-commit hook from lorenzwalthert/precommit does in the sense that it only styles the files that were being committed.

    (Yes, I could probably figure out the PATH thing better than just whacking it in the ENTRYPOINT).

    A nice thing about supporting this more officially is that it can be versioned to match github release tags and maintained via GHA. It is a little overkill / tiny bit slower than running an installed binary via language: system, but the image is "only" 100MB locally and runs perfectly fine afaict

  12. jpvelez commented on Sep 21, 2025

    @jpvelez

    Just wanted to chime on as a user that this would be super valuable!

  13. DavisVaughan commented on Sep 22, 2025

    @DavisVaughan
    CollaboratorAuthor

    Our general recommendation is that your team combines Format on Save in your IDE with our GitHub Action, which serves as a "backup check" and a way to ensure external contributions from non-team members are also formatted.

    If you are interested in pre-commit, can you help us understand why this doesn't work for you?

    I talked with @tanho63 at conf and he said that his team doesn't / isn't allowed to use GitHub Actions, so I understand why it would be useful for him, but I'd be interested in more use cases.

  14. jpvelez commented on Sep 22, 2025

    @jpvelez
  15. DavisVaughan commented on Sep 22, 2025

    @DavisVaughan
    CollaboratorAuthor
  16. 13 remaining items

  17. DavisVaughan commented on Feb 27, 2026

    @DavisVaughan
    CollaboratorAuthor

    Would any brave souls be willing to try this in their .pre-commit-config.yaml? It seems to work for me when I add it to dplyr, change some files, and run pre-commit locally.

    repos:
    - repo: https://github.com/posit-dev/air-pre-commit
      # Air version
      rev: 0.8.2
      hooks:
        # Run the formatter
        - id: air-format

    We distribute air as a package on PyPI now https://pypi.org/project/air-formatter/ (it's literally just the air binary, no python code in there). While this is odd, it means we can tap into pre-commit's great support for downloading PyPI packages on the fly.

    As a side benefit you can also invoke air via uv now, which is pretty awesome

    uvx --from air-formatter air --help
  18. DavisVaughan commented on Feb 27, 2026

    @DavisVaughan
    CollaboratorAuthor

    In particular, @tanho63 since we talked about this a few months back!

  19. tanho63 commented on Feb 27, 2026

    @tanho63

    First thing Monday!

  20. novica commented on Feb 28, 2026

    @novica

    Hi @DavisVaughan

    I am trying to recreate a python-project-template for R, and I reached the pre-commit task.

    I tried to set up what you are suggesting above, and it seems to work:

    [r-project-template]$ just pre-commit
    uvx pre-commit run --all-files --hook-stage push
    air format...............................................................Passed
    fix end of files.........................................................Passed
    trim trailing whitespace.................................................Passed
    check for added large files..............................................Passed
    check for merge conflicts................................................Passed
    check yaml...............................................................Passed
    check toml...............................................................Passed
    check json...............................................................Passed
    pretty format json.......................................................Passed
    codespell................................................................Passed
    

    I agree it is kind of strange that uvx is required to run these.

  21. DavisVaughan commented on Feb 28, 2026

    @DavisVaughan
    CollaboratorAuthor

    @novica uvx is not required to run the pre-commit hook

  22. novica commented on Feb 28, 2026

    @novica

    @DavisVaughan yes. I was not being precise in my wording.

    I think I need to run uvx to parse the yaml with pre-commit to have the hook set up. Or maybe I am doing an extra step -- but it works :)

  23. tanho63 commented on Mar 3, 2026

    @tanho63

    This worked as expected (and is drop-in compatible with prek as well)! Does air automatically only format recognized file extensions e.g. .R/.Rmd/.qmd or do we also need to supply the yaml param files: [.R|.r|.Rmd|.qmd] ?

  24. DavisVaughan commented on Mar 3, 2026

    @DavisVaughan
    CollaboratorAuthor

    @tanho63 air does not support Rmd nor Qmd from the command line. We don't currently have a quarto parser for that, but one is in the works. It will only format R files, should do .R and .r IIUC.

  25. DavisVaughan commented on Mar 5, 2026

    @DavisVaughan
    CollaboratorAuthor

    I'm going to call this closed then! Use air-pre-commit as described here https://github.com/posit-dev/air-pre-commit

  26. tanho63 commented on Mar 5, 2026

    @tanho63

    Thanks so much, this is gonna be super useful! I think it might be helpful if the doc for integrating in pre-commit linked the list of github release tags so that users know exactly what values are actually possible to pin to, i.e.

    ::: callout-note
    ## Only Air ≥0.8.2 can be pinned as a `rev`.
    :::

    should link to https://github.com/posit-dev/air-pre-commit/releases

  27. jpvelez commented on Mar 9, 2026

    @jpvelez

    This is working great!

    And, fwiw @tanho63 @DavisVaughan you may want to check out panache, which is aiming to be the (precommitable) linter / formatter for quarto, and which can already delegate to air for R cells (as well as ruff for python cells, other tools for javascript, etc.)

    Ultimately it may make for sense to have folks use a quarto linter/reformatter that uses air, rather than having air reformat qmd files, and only R qmd files at that.

  28. DavisVaughan commented on Mar 9, 2026

    @DavisVaughan
    CollaboratorAuthor

    Ultimately it may make for sense to have folks use a quarto linter/reformatter that uses air

    Yea I tend to agree, since Air would never be able to help with the Python chunks if you have a mixed R/Python qmd.

    Quarto itself will likely have something like this eventually, though I cant promise any timeline at all. A rust based quarto parser is going to make that feasible though https://github.com/quarto-dev/q2

    And some discussion https://github.com/orgs/quarto-dev/discussions/12082

  29. jpvelez commented on Mar 10, 2026

    @jpvelez

    @DavisVaughan I wasn't aware of q2! Exciting times.

  30. maikol-solis commented on Apr 7, 2026

    @maikol-solis

    This worked as expected (and is drop-in compatible with prek as well)! Does air automatically only format recognized file extensions e.g. .R/.Rmd/.qmd or do we also need to supply the yaml param files: [.R|.r|.Rmd|.qmd] ?

    @tanho63 You need this instead: https://github.com/jolars/panache

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions