diff --git a/src/config.rs b/src/config.rs index 749b800..77bc404 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,6 +64,15 @@ impl Language { Self::System => "system", } } + + /// Return whether the language allows specifying the version. + /// See + pub fn allow_specify_version(self) -> bool { + matches!( + self, + Self::Python | Self::Node | Self::Ruby | Self::Rust | Self::Golang + ) + } } impl Display for Language { @@ -213,7 +222,7 @@ pub struct Config { /// Default is `[pre-commit]`. pub default_install_hook_types: Option>, /// A mapping from language to the default `language_version`. - pub default_language_version: Option>, + pub default_language_version: Option>, /// A configuration-wide default for the stages property of hooks. /// Default to all stages. pub default_stages: Option>, @@ -275,6 +284,56 @@ impl Display for RepoLocation { } } +#[derive(Default, Debug, Clone)] +pub enum LanguageVersion { + /// By default, pre-commit will use the system installed version, + /// if not found, it will try to download and install a version. + #[default] + Default, + /// Use the system installed version. + System, + /// Download and install a specific version. + Specific(String), +} + +impl<'de> Deserialize<'de> for LanguageVersion { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match &*s { + "default" => Ok(LanguageVersion::Default), + "system" => Ok(LanguageVersion::System), + _ => Ok(LanguageVersion::Specific(s)), + } + } +} + +impl LanguageVersion { + pub fn is_default(&self) -> bool { + matches!(self, Self::Default) + } + + pub fn is_system(&self) -> bool { + matches!(self, Self::System) + } + + pub fn as_str(&self) -> &str { + match self { + Self::Default => "default", + Self::System => "system", + Self::Specific(s) => s, + } + } +} + +impl Display for LanguageVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + /// Common hook options. #[derive(Debug, Clone, Default, Deserialize)] pub struct HookOptions { @@ -312,7 +371,7 @@ pub struct HookOptions { /// Run the hook on a specific version of the language. /// Default is `default`. /// See . - pub language_version: Option, + pub language_version: Option, /// Write the output of the hook to a file when the hook fails or verbose is enabled. pub log_file: Option, /// This hook will execute using a single process instead of in parallel. @@ -1113,6 +1172,139 @@ mod tests { "#); } + #[test] + fn language_version() { + let yaml = indoc::indoc! { r" + repos: + - repo: local + hooks: + - id: hook-1 + name: hook 1 + entry: echo hello world + language: system + language_version: default + - id: hook-2 + name: hook 2 + entry: echo hello world + language: system + language_version: system + - id: hook-3 + name: hook 3 + entry: echo hello world + language: system + language_version: '3.8' + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r#" + Ok( + Config { + repos: [ + Local( + LocalRepo { + hooks: [ + ManifestHook { + id: "hook-1", + name: "hook 1", + entry: "echo hello world", + language: System, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: None, + description: None, + language_version: Some( + Default, + ), + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, + }, + ManifestHook { + id: "hook-2", + name: "hook 2", + entry: "echo hello world", + language: System, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: None, + description: None, + language_version: Some( + System, + ), + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, + }, + ManifestHook { + id: "hook-3", + name: "hook 3", + entry: "echo hello world", + language: System, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: None, + description: None, + language_version: Some( + Specific( + "3.8", + ), + ), + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, + }, + ], + }, + ), + ], + default_install_hook_types: None, + default_language_version: None, + default_stages: None, + files: None, + exclude: None, + fail_fast: None, + minimum_pre_commit_version: None, + ci: None, + }, + ) + "#); + } + #[test] fn test_read_config() -> Result<()> { let config = read_config(Path::new("tests/files/uv-pre-commit-config.yaml"))?; diff --git a/src/hook.rs b/src/hook.rs index 0479994..fbf2265 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -13,11 +13,10 @@ use tracing::{debug, error}; use url::Url; use crate::config::{ - self, read_config, read_manifest, Config, Language, LocalHook, ManifestHook, MetaHook, - RemoteHook, Stage, CONFIG_FILE, MANIFEST_FILE, + self, read_config, read_manifest, Config, Language, LanguageVersion, LocalHook, ManifestHook, + MetaHook, RemoteHook, Stage, CONFIG_FILE, MANIFEST_FILE, }; use crate::fs::{Simplified, CWD}; -use crate::languages::DEFAULT_VERSION; use crate::store::Store; use crate::warn_user; @@ -363,9 +362,6 @@ impl HookBuilder { .as_ref() .and_then(|v| v.get(&language).cloned()); } - if options.language_version.is_none() { - options.language_version = Some(language.default_version().to_string()); - } if options.stages.is_none() { options.stages.clone_from(&config.default_stages); @@ -375,14 +371,12 @@ impl HookBuilder { /// Fill in the default values for the hook configuration. fn fill_in_defaults(&mut self) { let options = &mut self.config.options; - options - .language_version - .get_or_insert(DEFAULT_VERSION.to_string()); - options.alias.get_or_insert(String::new()); - options.args.get_or_insert(Vec::new()); + options.language_version.get_or_insert_default(); + options.alias.get_or_insert_default(); + options.args.get_or_insert_default(); options.types.get_or_insert(vec!["file".to_string()]); - options.types_or.get_or_insert(Vec::new()); - options.exclude_types.get_or_insert(Vec::new()); + options.types_or.get_or_insert_default(); + options.exclude_types.get_or_insert_default(); options.always_run.get_or_insert(false); options.fail_fast.get_or_insert(false); options.pass_filenames.get_or_insert(true); @@ -391,23 +385,34 @@ impl HookBuilder { options .stages .get_or_insert(Stage::value_variants().to_vec()); - options.additional_dependencies.get_or_insert(Vec::new()); + options.additional_dependencies.get_or_insert_default(); } /// Check the hook configuration. fn check(&self) { let language = self.config.language; + let options = &self.config.options; if language.environment_dir().is_none() { - if self.config.options.language_version != Some(DEFAULT_VERSION.to_string()) { + if options.additional_dependencies.is_some() { warn_user!( - "Language {} does not need environment, but language_version is set", + "Language {} does not need environment, but additional_dependencies is set", language ); } - - if self.config.options.additional_dependencies.is_some() { + } + if options + .language_version + .as_ref() + .is_some_and(|v| !v.is_default()) + { + if language.environment_dir().is_none() { warn_user!( - "Language {} does not need environment, but additional_dependencies is set", + "Language {} does not need environment, but language_version is set", + language + ); + } else if !language.allow_specify_version() { + warn_user!( + "Language {} does not support specifying version, but language_version is set", language ); } @@ -473,7 +478,7 @@ pub struct Hook { pub fail_fast: bool, pub pass_filenames: bool, pub description: Option, - pub language_version: String, + pub language_version: LanguageVersion, pub log_file: Option, pub require_serial: bool, pub stages: Vec, diff --git a/src/languages/docker.rs b/src/languages/docker.rs index b75c55c..ba39831 100644 --- a/src/languages/docker.rs +++ b/src/languages/docker.rs @@ -11,7 +11,7 @@ use tracing::trace; use crate::fs::CWD; use crate::hook::Hook; -use crate::languages::{LanguageImpl, DEFAULT_VERSION}; +use crate::languages::LanguageImpl; use crate::process::Cmd; use crate::run::run_by_batch; @@ -161,10 +161,6 @@ impl Docker { } impl LanguageImpl for Docker { - fn default_version(&self) -> &str { - DEFAULT_VERSION - } - fn environment_dir(&self) -> Option<&str> { Some("docker") } diff --git a/src/languages/docker_image.rs b/src/languages/docker_image.rs index 8f2b28e..1ee4a0e 100644 --- a/src/languages/docker_image.rs +++ b/src/languages/docker_image.rs @@ -3,17 +3,13 @@ use std::sync::Arc; use crate::hook::Hook; use crate::languages::docker::Docker; -use crate::languages::{LanguageImpl, DEFAULT_VERSION}; +use crate::languages::LanguageImpl; use crate::run::run_by_batch; #[derive(Debug, Copy, Clone)] pub struct DockerImage; impl LanguageImpl for DockerImage { - fn default_version(&self) -> &str { - DEFAULT_VERSION - } - fn environment_dir(&self) -> Option<&str> { None } diff --git a/src/languages/fail.rs b/src/languages/fail.rs index ba3741c..a6798a9 100644 --- a/src/languages/fail.rs +++ b/src/languages/fail.rs @@ -1,16 +1,12 @@ use std::{collections::HashMap, sync::Arc}; use crate::hook::Hook; -use crate::languages::{LanguageImpl, DEFAULT_VERSION}; +use crate::languages::LanguageImpl; #[derive(Debug, Copy, Clone)] pub struct Fail; impl LanguageImpl for Fail { - fn default_version(&self) -> &str { - DEFAULT_VERSION - } - fn environment_dir(&self) -> Option<&str> { None } diff --git a/src/languages/mod.rs b/src/languages/mod.rs index 4517556..f9b2685 100644 --- a/src/languages/mod.rs +++ b/src/languages/mod.rs @@ -20,10 +20,7 @@ static FAIL: fail::Fail = fail::Fail; static DOCKER: docker::Docker = docker::Docker; static DOCKER_IMAGE: docker_image::DockerImage = docker_image::DockerImage; -pub const DEFAULT_VERSION: &str = "default"; - trait LanguageImpl { - fn default_version(&self) -> &str; fn environment_dir(&self) -> Option<&str>; async fn install(&self, hook: &Hook) -> Result<()>; async fn check_health(&self) -> Result<()>; @@ -36,18 +33,6 @@ trait LanguageImpl { } impl Language { - pub fn default_version(&self) -> &str { - match self { - Self::Python => PYTHON.default_version(), - Self::Node => NODE.default_version(), - Self::System => SYSTEM.default_version(), - Self::Fail => FAIL.default_version(), - Self::Docker => DOCKER.default_version(), - Self::DockerImage => DOCKER_IMAGE.default_version(), - _ => todo!(), - } - } - pub fn environment_dir(&self) -> Option<&str> { match self { Self::Python => PYTHON.environment_dir(), diff --git a/src/languages/node.rs b/src/languages/node.rs index a40d7ae..2201cf5 100644 --- a/src/languages/node.rs +++ b/src/languages/node.rs @@ -2,16 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use crate::hook::Hook; -use crate::languages::{LanguageImpl, DEFAULT_VERSION}; +use crate::languages::LanguageImpl; #[derive(Debug, Copy, Clone)] pub struct Node; impl LanguageImpl for Node { - fn default_version(&self) -> &str { - DEFAULT_VERSION - } - fn environment_dir(&self) -> Option<&str> { Some("node_env") } @@ -20,6 +16,7 @@ impl LanguageImpl for Node { // TODO: install node automatically let env = hook.environment_dir().expect("No environment dir found"); fs_err::create_dir_all(env)?; + Ok(()) } diff --git a/src/languages/python/impl.rs b/src/languages/python/impl.rs index ce2a431..08b1fca 100644 --- a/src/languages/python/impl.rs +++ b/src/languages/python/impl.rs @@ -1,23 +1,18 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - +use crate::config::LanguageVersion; use crate::env_vars::EnvVars; use crate::hook::Hook; use crate::languages::python::uv::UvInstaller; use crate::languages::LanguageImpl; use crate::process::Cmd; use crate::run::run_by_batch; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; #[derive(Debug, Copy, Clone)] pub struct Python; impl LanguageImpl for Python { - fn default_version(&self) -> &str { - // TODO find the version of python on the system - "python3" - } - fn environment_dir(&self) -> Option<&str> { Some("py_env") } @@ -40,14 +35,20 @@ impl LanguageImpl for Python { // TODO: Set uv cache dir? tools dir? python dir? // Create venv - uv_cmd("create venv") - .arg("venv") - .arg(&venv) - .arg("--python") - .arg(&hook.language_version) - .check(true) - .output() - .await?; + let mut cmd = uv_cmd("create venv"); + cmd.arg("venv").arg(&venv); + match hook.language_version { + LanguageVersion::Specific(ref version) => { + cmd.arg("--python").arg(version); + } + LanguageVersion::System => { + cmd.arg("--python-preference").arg("only-system"); + } + // uv will try to use system Python and download if not found + LanguageVersion::Default => {} + } + + cmd.check(true).output().await?; patch_cfg_version_info(&venv).await?; diff --git a/src/languages/system.rs b/src/languages/system.rs index e76271b..23acfb5 100644 --- a/src/languages/system.rs +++ b/src/languages/system.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use crate::hook::Hook; -use crate::languages::{LanguageImpl, DEFAULT_VERSION}; +use crate::languages::LanguageImpl; use crate::process::Cmd; use crate::run::run_by_batch; @@ -10,10 +10,6 @@ use crate::run::run_by_batch; pub struct System; impl LanguageImpl for System { - fn default_version(&self) -> &str { - DEFAULT_VERSION - } - fn environment_dir(&self) -> Option<&str> { None }