diff --git a/.gitignore b/.gitignore index 88ba5ea..e08c794 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ +target/ .DS_Store -__pycache__/ -*.pyc - diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 5e31071..a6e2c6f 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -2,23 +2,23 @@ name: cargo fmt description: Format rust files with cargo fmt entry: pre_commit_rust fmt - language: python + language: rust types: [rust] - minimum_pre_commit_version: 2.9.2 + minimum_pre_commit_version: 2.21.0 require_serial: true - id: cargo-check name: cargo check description: Run cargo check for compilation errors entry: pre_commit_rust check - language: python + language: rust types: [rust] - minimum_pre_commit_version: 2.9.2 + minimum_pre_commit_version: 2.21.0 require_serial: true - id: cargo-clippy name: cargo clippy description: Run cargo clippy lint tool entry: pre_commit_rust clippy - language: python + language: rust types: [rust] - minimum_pre_commit_version: 2.9.2 + minimum_pre_commit_version: 2.21.0 require_serial: true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a0e5d87 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,313 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "clap" +version = "4.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a932373bab67b984c790ddf2c9ca295d8e3af3b7ef92de5a5bacdccdee4b09b" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "pre-commit-rust" +version = "0.3.0" +dependencies = [ + "anyhow", + "clap", + "glob", +] + +[[package]] +name = "proc-macro2" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.36.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..97b690c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pre-commit-rust" +version = "0.3.0" +edition = "2021" +authors = ["Kyle Kosic "] + +[[bin]] +name = "pre_commit_rust" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.70" +clap = { version = "4.1.13", features = ["derive"] } +glob = "0.3.1" diff --git a/pre_commit_rust.py b/pre_commit_rust.py deleted file mode 100644 index 1ff5898..0000000 --- a/pre_commit_rust.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import glob -import subprocess -from functools import lru_cache -from pathlib import Path - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.set_defaults(func=None) - sps = parser.add_subparsers(dest="cmd") - - sp = sps.add_parser("fmt") - sp.add_argument( - "--config", - type=str, - help="Comma-separated key=value config pairs for rustfmt", - ) - sp.set_defaults(func=run_fmt) - add_files_nargs(sp) - - sp = sps.add_parser("check") - sp.add_argument( - "--features", - type=str, - help="Space or comma-separated list of features to check", - ) - sp.add_argument( - "--all-features", - action="store_true", - help="Activate all available features", - ) - sp.set_defaults(func=run_check) - add_files_nargs(sp) - - sp = sps.add_parser("clippy") - sp.set_defaults(func=run_clippy) - add_files_nargs(sp) - - args = parser.parse_args() - - if args.func is None: - parser.print_help() - return 1 - - run_dirs = get_run_dirs(args.files) - if not run_dirs: - return 0 - - failed = sum(args.func(args, d) for d in run_dirs) - return int(failed > 0) - - -def add_files_nargs(parser: argparse.ArgumentParser): - parser.add_argument( - "files", - nargs="*", - type=Path, - ) - - -def get_run_dirs(changed_files: list[Path]) -> set[Path]: - root_dirs = find_cargo_root_dirs() - run_dirs: set[Path] = set() - for path in changed_files: - if not is_rust_file(path): - continue - roots = [d for d in root_dirs if path.is_relative_to(d)] - if not roots: - continue - root = max(roots, key=path_len) - run_dirs.add(root) - return run_dirs - - -def find_cargo_root_dirs() -> list[Path]: - return [Path(p).parent for p in glob.glob("**/Cargo.toml", recursive=True)] - - -def is_rust_file(path: Path) -> bool: - if path.suffix == ".rs": - return True - elif path.name in ["Cargo.toml", "Cargo.lock"]: - return True - return False - - -@lru_cache -def path_len(path: Path) -> int: - return len(path.parts) - - -def run_fmt(args: argparse.Namespace, directory: Path) -> int: - cmd = "cargo fmt --" - if args.config: - cmd += f" --config {args.config}" - return run_action(cmd, directory) - - -def run_check(args: argparse.Namespace, directory: Path) -> int: - cmd = "cargo check" - if args.features is not None: - cmd += f" --features={args.features}" - if args.all_features: - cmd += f" --all-features" - return run_action(cmd, directory) - - -def run_clippy(_: argparse.Namespace, directory: Path) -> int: - cmd = "cargo clippy -- -D warnings" - return run_action(cmd, directory) - - -def run_action(cmd: str, directory: Path) -> int: - proc = subprocess.run(cmd, cwd=directory, shell=True) - return proc.returncode - - -if __name__ == "__main__": - exit(main()) diff --git a/setup.py b/setup.py deleted file mode 100644 index f3a2f0c..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup - -setup( - name="pre_commit_rust", - version="0.1.0", - py_modules=["pre_commit_rust"], - entry_points={"console_scripts": ["pre_commit_rust=pre_commit_rust:main"]}, -) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f4e7d32 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,173 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; + +use anyhow::{bail, Context, Result}; +use clap::{Parser, Subcommand}; +use glob::glob; + +/// Pre-commit hook for running cargo fmt/check/clippy against a repo. +/// The target repo may contain multiple independent cargo projects or workspaces. +#[derive(Debug, Parser)] +struct Opts { + #[command(subcommand)] + cmd: Cmd, + /// List of chaned files to target + #[clap(global = true)] + files: Vec, +} + +#[derive(Debug, Subcommand)] +enum Cmd { + /// Run the rustfmt (cargo fmt) hook + Fmt { + /// Comma-separated key=value config pairs for rustfmt + #[clap(long)] + config: Option, + }, + /// Run the cargo check hook + Check { + /// Comma-separated list of features to check + #[clap(long)] + features: Option, + /// Activate all available features + #[clap(long)] + all_features: bool, + }, + /// Run the cargo clippy hook + Clippy, +} + +fn main() -> ExitCode { + let opts = Opts::parse(); + + let run_dirs = get_run_dirs(&opts.files); + + let err_count = run_dirs + .into_iter() + .map(|dir| match &opts.cmd { + Cmd::Fmt { config } => run_fmt(dir, config), + Cmd::Check { + features, + all_features, + } => run_check(dir, features, *all_features), + Cmd::Clippy => run_clippy(dir), + }) + .filter(|res| match res { + Ok(()) => false, + Err(e) => { + eprintln!("{}", e); + true + } + }) + .count(); + + if err_count > 0 { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} + +const NOT_FOUND: &str = "failed to run 'cargo'"; + +fn run_fmt(dir: PathBuf, config: &Option) -> Result<()> { + let mut cmd = cargo(); + cmd.args(["fmt", "--"]); + + if let Some(config) = config { + cmd.args(["--config", config]); + } + + cmd.current_dir(dir); + let status = cmd.status()?; + if !status.success() { + bail!("cargo fmt modified files"); + } + Ok(()) +} + +fn run_check(dir: PathBuf, features: &Option, all_features: bool) -> Result<()> { + let mut cmd = cargo(); + cmd.arg("check"); + + if all_features { + cmd.arg("--all-features"); + } else if let Some(features) = features { + cmd.args(["--features", features]); + } + + cmd.current_dir(dir); + let status = cmd.status().context(NOT_FOUND)?; + if !status.success() { + bail!("cargo check failed"); + } + Ok(()) +} + +fn run_clippy(dir: PathBuf) -> Result<()> { + let status = cargo() + .args(["clippy", "--", "-D", "warnings"]) + .current_dir(dir) + .status() + .context(NOT_FOUND)?; + if !status.success() { + bail!("cargo clippy failed"); + } + Ok(()) +} + +fn get_run_dirs(changed_files: &[PathBuf]) -> HashSet { + let root_dirs = find_cargo_root_dirs(); + let mut run_dirs: HashSet = HashSet::new(); + let current_dir = std::env::current_dir().unwrap(); + for path in changed_files { + if !is_rust_file(path) { + continue; + } + if let Some(root) = root_dirs + .iter() + .filter(|d| path.starts_with(d)) + .max_by_key(|path| path.components().count()) + { + run_dirs.insert(current_dir.join(root)); + } + } + run_dirs +} + +fn find_cargo_root_dirs() -> Vec { + let mut dirs = Vec::new(); + for entry in glob("**/Cargo.toml").unwrap() { + match entry { + Ok(path) => dirs.push(path.parent().unwrap().into()), + Err(e) => eprintln!("{e:?}"), + } + } + dirs +} + +fn is_rust_file>(path: P) -> bool { + let path = path.as_ref(); + if let Some(ext) = path.extension() { + if ext == "rs" { + return true; + } + } + if let Some(name) = path.file_name() { + let name = name.to_string_lossy(); + if ["Cargo.toml", "Cargo.lock"].contains(&name.as_ref()) { + return true; + } + } + false +} + +fn cargo() -> Command { + /// The compile-time location of cargo. Used to access the pre-commit managed environment + /// of cargo for subcommands; + const CARGO_HOME: &str = std::env!("CARGO_HOME"); + + let bin = PathBuf::from(CARGO_HOME).join("bin").join("cargo"); + Command::new(bin) +}