From 52bbed9d09bc8d1b758056e00947c0f5df535733 Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Sun, 8 Dec 2024 18:58:54 +0800 Subject: [PATCH 1/2] Support meta hooks --- src/config.rs | 228 ++++++++++++++++++++++++++++++++------------------ src/hook.rs | 119 ++++++++++---------------- 2 files changed, 189 insertions(+), 158 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4762714..937bb34 100644 --- a/src/config.rs +++ b/src/config.rs @@ -275,33 +275,25 @@ impl Display for RepoLocation { } } -/// A remote hook in the configuration file. -/// -/// All keys in manifest hook dict are valid in a config hook dict, but are optional. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct ConfigRemoteHook { - /// The id of the hook. - pub id: String, - /// Override the name of the hook. - pub name: Option, - /// Not documented in the official docs. - pub entry: Option, +#[derive(Debug, Clone, Default, Deserialize)] +pub struct HookOptions { /// Not documented in the official docs. - pub language: Option, - /// Allows the hook to be referenced using an additional id when using pre-commit run pub alias: Option, - /// Override the pattern of files to run on. + /// The pattern of files to run on. pub files: Option, - /// Override the pattern of files to exclude. + /// Exclude files that were matched by `files`. + /// Default is `$^`, which matches nothing. pub exclude: Option, - /// Override the types of files to run on (AND). + /// List of file types to run on (AND). + /// Default is `[file]`, which matches all files. pub types: Option>, - /// Override the types of files to run on (OR). + /// List of file types to run on (OR). + /// Default is `[]`. pub types_or: Option>, - /// Override the types of files to exclude. + /// List of file types to exclude. + /// Default is `[]`. pub exclude_types: Option>, - /// Additional dependencies to install in the environment where the hook runs. + /// Not documented in the official docs. pub additional_dependencies: Option>, /// Additional arguments to pass to the hook. pub args: Option>, @@ -335,6 +327,59 @@ pub struct ConfigRemoteHook { pub minimum_pre_commit_version: Option, } +impl HookOptions { + pub fn merge(&mut self, other: &Self) { + macro_rules! merge_if_none { + ($($field:ident),* $(,)?) => { + $( + if self.$field.is_some() { + self.$field.clone_from(&other.$field); + } + )* + }; + } + + merge_if_none!( + alias, + files, + exclude, + types, + types_or, + exclude_types, + additional_dependencies, + args, + always_run, + fail_fast, + pass_filenames, + description, + language_version, + log_file, + require_serial, + stages, + verbose, + minimum_pre_commit_version, + ); + } +} + +/// A remote hook in the configuration file. +/// +/// All keys in manifest hook dict are valid in a config hook dict, but are optional. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct ConfigRemoteHook { + /// The id of the hook. + pub id: String, + /// Override the name of the hook. + pub name: Option, + /// Not documented in the official docs. + pub entry: Option, + /// Not documented in the official docs. + pub language: Option, + #[serde(flatten)] + pub options: HookOptions, +} + /// A local hook in the configuration file. /// /// It's the same as the manifest hook definition. @@ -348,30 +393,93 @@ pub enum MetaHookID { Identify, } -impl MetaHookID { - pub fn as_str(&self) -> &str { - match self { +impl Display for MetaHookID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { MetaHookID::CheckHooksApply => "check-hooks-apply", MetaHookID::CheckUselessExcludes => "check-useless-excludes", MetaHookID::Identify => "identify", - } + }; + f.write_str(name) } } -impl Display for MetaHookID { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) +impl FromStr for MetaHookID { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "check-hooks-apply" => Ok(MetaHookID::CheckHooksApply), + "check-useless-excludes" => Ok(MetaHookID::CheckUselessExcludes), + "identify" => Ok(MetaHookID::Identify), + _ => Err(()), + } } } -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(deny_unknown_fields)] -pub struct ConfigMetaHook { - pub id: MetaHookID, - // only "system" is allowed - pub language: Option, - // TODO: entry is not allowed +/// A meta hook predefined in pre-commit. +/// +/// It's the same as the manifest hook definition. +#[derive(Debug, Clone)] +pub struct ConfigMetaHook(ManifestHook); + +impl<'de> Deserialize<'de> for ConfigMetaHook { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let hook = ConfigRemoteHook::deserialize(deserializer)?; + + let id = MetaHookID::from_str(&hook.id) + .map_err(|()| serde::de::Error::custom("Unknown meta hook id"))?; + if hook.language != Some(Language::System) { + return Err(serde::de::Error::custom( + "language must be system for meta hook", + )); + } + if hook.entry.is_some() { + return Err(serde::de::Error::custom( + "entry is not allowed for meta hook", + )); + } + + let mut defaults = match id { + MetaHookID::CheckHooksApply => ManifestHook { + id: "check-hooks-apply".to_string(), + name: "Check hooks apply".to_string(), + language: Language::System, + entry: "a".to_string(), + options: HookOptions { + files: Some(r"\.yaml$".to_string()), // TODO + ..Default::default() + }, + }, + MetaHookID::CheckUselessExcludes => ManifestHook { + id: "check-useless-excludes".to_string(), + name: "Check useless excludes".to_string(), + language: Language::System, + entry: "a".to_string(), + options: HookOptions { + files: Some(r"\.yaml$".to_string()), // TODO + ..Default::default() + }, + }, + MetaHookID::Identify => ManifestHook { + id: "identify".to_string(), + name: "identify".to_string(), + language: Language::System, + entry: "a".to_string(), + options: HookOptions { + verbose: Some(true), + ..Default::default() + }, + }, + }; + + defaults.options.merge(&hook.options); + + Ok(ConfigMetaHook(defaults)) + } } #[derive(Debug, Clone)] @@ -506,54 +614,8 @@ pub struct ManifestHook { pub entry: String, /// The language of the hook. Tells pre-commit how to install and run the hook. pub language: Language, - /// Not documented in the official docs. - pub alias: Option, - /// The pattern of files to run on. - pub files: Option, - /// Exclude files that were matched by `files`. - /// Default is `$^`, which matches nothing. - pub exclude: Option, - /// List of file types to run on (AND). - /// Default is `[file]`, which matches all files. - pub types: Option>, - /// List of file types to run on (OR). - /// Default is `[]`. - pub types_or: Option>, - /// List of file types to exclude. - /// Default is `[]`. - pub exclude_types: Option>, - /// Not documented in the official docs. - pub additional_dependencies: Option>, - /// Additional arguments to pass to the hook. - pub args: Option>, - /// This hook will run even if there are no matching files. - /// Default is false. - pub always_run: Option, - /// If this hook fails, don't run any more hooks. - /// Default is false. - pub fail_fast: Option, - /// Append filenames that would be checked to the hook entry as arguments. - /// Default is true. - pub pass_filenames: Option, - /// A description of the hook. For metadata only. - pub description: Option, - /// Run the hook on a specific version of the language. - /// Default is `default`. - /// See . - 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. - /// Default is false. - pub require_serial: Option, - /// Select which git hook(s) to run for. - /// Default all stages are selected. - /// See . - pub stages: Option>, - /// Print the output of the hook even if it passes. - /// Default is false. - pub verbose: Option, - pub minimum_pre_commit_version: Option, + #[serde(flatten)] + pub options: HookOptions, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/hook.rs b/src/hook.rs index 97704d5..5c5c01f 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -303,36 +303,6 @@ impl HookBuilder { /// Update the hook from the project level hook configuration. fn update(&mut self, config: &ConfigRemoteHook) -> &mut Self { - macro_rules! update_if_some { - ($($field:ident),* $(,)?) => { - $( - if config.$field.is_some() { - self.config.$field.clone_from(&config.$field); - } - )* - }; - } - update_if_some!( - alias, - files, - exclude, - types, - types_or, - exclude_types, - additional_dependencies, - args, - always_run, - fail_fast, - pass_filenames, - description, - language_version, - log_file, - require_serial, - stages, - verbose, - minimum_pre_commit_version, - ); - if let Some(name) = &config.name { self.config.name.clone_from(name); } @@ -343,62 +313,64 @@ impl HookBuilder { self.config.language.clone_from(language); } + self.config.options.merge(&config.options); + self } /// Combine the hook configuration with the project level hook configuration. fn combine(&mut self, config: &ConfigWire) { + let options = &mut self.config.options; let language = self.config.language; - if self.config.language_version.is_none() { - self.config.language_version = config + if options.language_version.is_none() { + options.language_version = config .default_language_version .as_ref() .and_then(|v| v.get(&language).cloned()); } - if self.config.language_version.is_none() { - self.config.language_version = Some(language.default_version().to_string()); + if options.language_version.is_none() { + options.language_version = Some(language.default_version().to_string()); } - if self.config.stages.is_none() { - self.config.stages.clone_from(&config.default_stages); + if options.stages.is_none() { + options.stages.clone_from(&config.default_stages); } } /// Fill in the default values for the hook configuration. fn fill_in_defaults(&mut self) { - self.config + let options = &mut self.config.options; + options .language_version .get_or_insert(DEFAULT_VERSION.to_string()); - self.config.alias.get_or_insert(String::new()); - self.config.args.get_or_insert(Vec::new()); - self.config.types.get_or_insert(vec!["file".to_string()]); - self.config.types_or.get_or_insert(Vec::new()); - self.config.exclude_types.get_or_insert(Vec::new()); - self.config.always_run.get_or_insert(false); - self.config.fail_fast.get_or_insert(false); - self.config.pass_filenames.get_or_insert(true); - self.config.require_serial.get_or_insert(false); - self.config.verbose.get_or_insert(false); - self.config + options.alias.get_or_insert(String::new()); + options.args.get_or_insert(Vec::new()); + 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.always_run.get_or_insert(false); + options.fail_fast.get_or_insert(false); + options.pass_filenames.get_or_insert(true); + options.require_serial.get_or_insert(false); + options.verbose.get_or_insert(false); + options .stages .get_or_insert(Stage::value_variants().to_vec()); - self.config - .additional_dependencies - .get_or_insert(Vec::new()); + options.additional_dependencies.get_or_insert(Vec::new()); } /// Check the hook configuration. fn check(&self) { let language = self.config.language; if language.environment_dir().is_none() { - if self.config.language_version != Some(DEFAULT_VERSION.to_string()) { + if self.config.options.language_version != Some(DEFAULT_VERSION.to_string()) { warn_user!( "Language {} does not need environment, but language_version is set", language ); } - if self.config.additional_dependencies.is_some() { + if self.config.options.additional_dependencies.is_some() { warn_user!( "Language {} does not need environment, but additional_dependencies is set", language @@ -412,6 +384,7 @@ impl HookBuilder { self.check(); self.fill_in_defaults(); + let options = self.config.options; Hook { repo: self.repo, path: None, @@ -419,30 +392,26 @@ impl HookBuilder { name: self.config.name, entry: self.config.entry, language: self.config.language, - alias: self.config.alias.expect("alias not set"), - files: self.config.files, - exclude: self.config.exclude, - types: self.config.types.expect("types not set"), - types_or: self.config.types_or.expect("types_or not set"), - exclude_types: self.config.exclude_types.expect("exclude_types not set"), - additional_dependencies: self - .config + alias: options.alias.expect("alias not set"), + files: options.files, + exclude: options.exclude, + types: options.types.expect("types not set"), + types_or: options.types_or.expect("types_or not set"), + exclude_types: options.exclude_types.expect("exclude_types not set"), + additional_dependencies: options .additional_dependencies .expect("additional_dependencies should not be None"), - args: self.config.args.expect("args not set"), - always_run: self.config.always_run.expect("always_run not set"), - fail_fast: self.config.fail_fast.expect("fail_fast not set"), - pass_filenames: self.config.pass_filenames.expect("pass_filenames not set"), - description: self.config.description, - language_version: self - .config - .language_version - .expect("language_version not set"), - log_file: self.config.log_file, - require_serial: self.config.require_serial.expect("require_serial not set"), - stages: self.config.stages.expect("stages not set"), - verbose: self.config.verbose.expect("verbose not set"), - minimum_pre_commit_version: self.config.minimum_pre_commit_version, + args: options.args.expect("args not set"), + always_run: options.always_run.expect("always_run not set"), + fail_fast: options.fail_fast.expect("fail_fast not set"), + pass_filenames: options.pass_filenames.expect("pass_filenames not set"), + description: options.description, + language_version: options.language_version.expect("language_version not set"), + log_file: options.log_file, + require_serial: options.require_serial.expect("require_serial not set"), + stages: options.stages.expect("stages not set"), + verbose: options.verbose.expect("verbose not set"), + minimum_pre_commit_version: options.minimum_pre_commit_version, } } } From 154018f9fd2fcc2945c5c73936d41f78774c4737 Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Sun, 8 Dec 2024 20:01:35 +0800 Subject: [PATCH 2/2] Add tests --- src/config.rs | 329 ++++++++++++++---- src/hook.rs | 43 ++- ...prefligit__config__tests__read_config.snap | 314 +++++++++-------- ...efligit__config__tests__read_manifest.snap | 192 +++++----- 4 files changed, 558 insertions(+), 320 deletions(-) diff --git a/src/config.rs b/src/config.rs index 937bb34..bc7cfc9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::path::Path; use std::str::FromStr; use anyhow::Result; +use fancy_regex as regex; use serde::{Deserialize, Deserializer, Serialize}; use url::Url; @@ -275,6 +276,7 @@ impl Display for RepoLocation { } } +/// Common hook options. #[derive(Debug, Clone, Default, Deserialize)] pub struct HookOptions { /// Not documented in the official docs. @@ -328,18 +330,18 @@ pub struct HookOptions { } impl HookOptions { - pub fn merge(&mut self, other: &Self) { - macro_rules! merge_if_none { + pub fn update(&mut self, other: &Self) { + macro_rules! update_if_some { ($($field:ident),* $(,)?) => { $( - if self.$field.is_some() { + if other.$field.is_some() { self.$field.clone_from(&other.$field); } )* }; } - merge_if_none!( + update_if_some!( alias, files, exclude, @@ -419,7 +421,7 @@ impl FromStr for MetaHookID { /// A meta hook predefined in pre-commit. /// -/// It's the same as the manifest hook definition. +/// It's the same as the manifest hook definition but with only a few predefined id allowed. #[derive(Debug, Clone)] pub struct ConfigMetaHook(ManifestHook); @@ -432,7 +434,7 @@ impl<'de> Deserialize<'de> for ConfigMetaHook { let id = MetaHookID::from_str(&hook.id) .map_err(|()| serde::de::Error::custom("Unknown meta hook id"))?; - if hook.language != Some(Language::System) { + if hook.language.is_some_and(|l| l != Language::System) { return Err(serde::de::Error::custom( "language must be system for meta hook", )); @@ -448,9 +450,9 @@ impl<'de> Deserialize<'de> for ConfigMetaHook { id: "check-hooks-apply".to_string(), name: "Check hooks apply".to_string(), language: Language::System, - entry: "a".to_string(), + entry: "a".to_string(), // TODO: direct call to the hook options: HookOptions { - files: Some(r"\.yaml$".to_string()), // TODO + files: Some(format!("^{}$", regex::escape(CONFIG_FILE))), ..Default::default() }, }, @@ -460,7 +462,7 @@ impl<'de> Deserialize<'de> for ConfigMetaHook { language: Language::System, entry: "a".to_string(), options: HookOptions { - files: Some(r"\.yaml$".to_string()), // TODO + files: Some(format!("^{}$", regex::escape(CONFIG_FILE))), ..Default::default() }, }, @@ -476,12 +478,18 @@ impl<'de> Deserialize<'de> for ConfigMetaHook { }, }; - defaults.options.merge(&hook.options); + defaults.options.update(&hook.options); Ok(ConfigMetaHook(defaults)) } } +impl From for ManifestHook { + fn from(hook: ConfigMetaHook) -> Self { + hook.0 + } +} + #[derive(Debug, Clone)] pub struct ConfigRemoteRepo { pub repo: Url, @@ -693,24 +701,26 @@ mod tests { name: "cargo fmt", entry: "cargo fmt --", language: System, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -782,24 +792,26 @@ mod tests { name: None, entry: None, language: None, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -890,24 +902,26 @@ mod tests { name: "cargo fmt", entry: "cargo fmt", language: Rust, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -926,6 +940,193 @@ mod tests { "###); } + #[test] + fn meta_hooks() { + // Invalid rev + let yaml = indoc::indoc! { r" + repos: + - repo: meta + rev: v1.0.0 + hooks: + - name: typos + alias: typo + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r###" + Err( + Error("repos: Invalid meta repo: unknown field `rev`, expected `hooks`", line: 2, column: 3), + ) + "###); + + // Invalid meta hook id + let yaml = indoc::indoc! { r" + repos: + - repo: meta + hooks: + - id: hello + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r###" + Err( + Error("repos: Invalid meta repo: Unknown meta hook id", line: 2, column: 3), + ) + "###); + + // Invalid language + let yaml = indoc::indoc! { r" + repos: + - repo: meta + hooks: + - id: check-hooks-apply + language: python + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r###" + Err( + Error("repos: Invalid meta repo: language must be system for meta hook", line: 2, column: 3), + ) + "###); + + // Invalid entry + let yaml = indoc::indoc! { r" + repos: + - repo: meta + hooks: + - id: check-hooks-apply + entry: echo hell world + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r###" + Err( + Error("repos: Invalid meta repo: entry is not allowed for meta hook", line: 2, column: 3), + ) + "###); + + // Valid meta hook + let yaml = indoc::indoc! { r" + repos: + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + - id: identify + "}; + let result = serde_yaml::from_str::(yaml); + insta::assert_debug_snapshot!(result, @r###" + Ok( + ConfigWire { + repos: [ + Meta( + ConfigMetaRepo { + repo: "meta", + hooks: [ + ConfigMetaHook( + ManifestHook { + id: "check-hooks-apply", + name: "Check hooks apply", + entry: "a", + language: System, + options: HookOptions { + alias: None, + files: Some( + "^\\.pre-commit-config\\.yaml$", + ), + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, + }, + ), + ConfigMetaHook( + ManifestHook { + id: "check-useless-excludes", + name: "Check useless excludes", + entry: "a", + language: System, + options: HookOptions { + alias: None, + files: Some( + "^\\.pre-commit-config\\.yaml$", + ), + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, + }, + ), + ConfigMetaHook( + ManifestHook { + id: "identify", + name: "identify", + entry: "a", + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: Some( + true, + ), + 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 5c5c01f..9ad5ebb 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -13,8 +13,8 @@ use tracing::{debug, error}; use url::Url; use crate::config::{ - self, read_config, read_manifest, ConfigLocalHook, ConfigRemoteHook, ConfigRepo, ConfigWire, - Language, ManifestHook, Stage, CONFIG_FILE, MANIFEST_FILE, + self, read_config, read_manifest, ConfigLocalHook, ConfigMetaHook, ConfigRemoteHook, + ConfigRepo, ConfigWire, Language, ManifestHook, Stage, CONFIG_FILE, MANIFEST_FILE, }; use crate::fs::{Simplified, CWD}; use crate::languages::DEFAULT_VERSION; @@ -47,7 +47,9 @@ pub enum Repo { Local { hooks: Vec, }, - Meta, + Meta { + hooks: Vec, + }, } impl Repo { @@ -72,8 +74,11 @@ impl Repo { Self::Local { hooks } } - pub fn meta() -> Result { - todo!() + /// Construct a meta repo. + pub fn meta(hooks: Vec) -> Self { + Self::Meta { + hooks: hooks.into_iter().map(ManifestHook::from).collect(), + } } /// Get a hook by id. @@ -81,16 +86,17 @@ impl Repo { let hooks = match self { Repo::Remote { ref hooks, .. } => hooks, Repo::Local { ref hooks } => hooks, - Repo::Meta => return None, + Repo::Meta { ref hooks } => hooks, }; hooks.iter().find(|hook| hook.id == id) } + /// Get the path to the repo. pub fn path(&self) -> &Path { match self { Repo::Remote { ref path, .. } => path, Repo::Local { .. } => &CWD, - Repo::Meta => todo!(), + Repo::Meta { .. } => &CWD, } } } @@ -100,7 +106,7 @@ impl Display for Repo { match self { Repo::Remote { url, rev, .. } => write!(f, "{url}@{rev}"), Repo::Local { .. } => write!(f, "local"), - Repo::Meta => write!(f, "meta"), + Repo::Meta { .. } => write!(f, "meta"), } } } @@ -195,8 +201,9 @@ impl Project { let repo = Repo::local(repo.hooks.clone()); repos.push(Rc::new(repo)); } - ConfigRepo::Meta(_) => { - todo!() + ConfigRepo::Meta(repo) => { + let repo = Repo::meta(repo.hooks.clone()); + repos.push(Rc::new(repo)); } } } @@ -273,8 +280,18 @@ impl Project { hooks.push(hook); } } - ConfigRepo::Meta(_) => { - todo!() + ConfigRepo::Meta(repo_config) => { + for hook_config in &repo_config.hooks { + let repo = Rc::clone(repo); + let hook_config = ManifestHook::from(hook_config.clone()); + let mut builder = HookBuilder::new(repo, hook_config); + builder.combine(&self.config); + let mut hook = builder.build(); + + let path = hook.repo.path().to_path_buf(); + hook = hook.with_path(path); + hooks.push(hook); + } } } } @@ -313,7 +330,7 @@ impl HookBuilder { self.config.language.clone_from(language); } - self.config.options.merge(&config.options); + self.config.options.update(&config.options); self } diff --git a/src/snapshots/prefligit__config__tests__read_config.snap b/src/snapshots/prefligit__config__tests__read_config.snap index c9a2299..6d141bf 100644 --- a/src/snapshots/prefligit__config__tests__read_config.snap +++ b/src/snapshots/prefligit__config__tests__read_config.snap @@ -28,24 +28,26 @@ ConfigWire { name: None, entry: None, language: None, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -74,24 +76,26 @@ ConfigWire { name: None, entry: None, language: None, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -105,30 +109,32 @@ ConfigWire { name: "cargo fmt", entry: "cargo fmt --", language: System, - alias: None, - files: None, - exclude: None, - types: Some( - [ - "rust", - ], - ), - types_or: None, - exclude_types: None, - additional_dependencies: None, - args: None, - always_run: None, - fail_fast: None, - pass_filenames: Some( - false, - ), - description: None, - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: Some( + [ + "rust", + ], + ), + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: Some( + false, + ), + description: None, + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -142,32 +148,34 @@ ConfigWire { name: "cargo dev generate-all", entry: "cargo dev generate-all", language: System, - alias: None, - files: Some( - "^crates/(uv-cli|uv-settings)/", - ), - exclude: None, - types: Some( - [ - "rust", - ], - ), - types_or: None, - exclude_types: None, - additional_dependencies: None, - args: None, - always_run: None, - fail_fast: None, - pass_filenames: Some( - false, - ), - description: None, - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + options: HookOptions { + alias: None, + files: Some( + "^crates/(uv-cli|uv-settings)/", + ), + exclude: None, + types: Some( + [ + "rust", + ], + ), + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: Some( + false, + ), + description: None, + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -196,29 +204,31 @@ ConfigWire { name: None, entry: None, language: None, - alias: None, - files: None, - exclude: None, - types: None, - types_or: Some( - [ - "yaml", - "json5", - ], - ), - exclude_types: None, - additional_dependencies: None, - args: None, - always_run: None, - fail_fast: None, - pass_filenames: None, - description: None, - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: None, + types_or: Some( + [ + "yaml", + "json5", + ], + ), + exclude_types: None, + additional_dependencies: None, + args: None, + always_run: None, + fail_fast: None, + pass_filenames: None, + description: None, + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, @@ -247,53 +257,57 @@ ConfigWire { name: None, entry: None, language: None, - 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: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + 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: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ConfigRemoteHook { id: "ruff", name: None, entry: None, language: None, - alias: None, - files: None, - exclude: None, - types: None, - types_or: None, - exclude_types: None, - additional_dependencies: None, - args: Some( - [ - "--fix", - "--exit-non-zero-on-fix", - ], - ), - always_run: None, - fail_fast: None, - pass_filenames: None, - description: None, - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: None, + options: HookOptions { + alias: None, + files: None, + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: None, + args: Some( + [ + "--fix", + "--exit-non-zero-on-fix", + ], + ), + always_run: None, + fail_fast: None, + pass_filenames: None, + description: None, + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: None, + }, }, ], }, diff --git a/src/snapshots/prefligit__config__tests__read_manifest.snap b/src/snapshots/prefligit__config__tests__read_manifest.snap index ec5839c..30501ba 100644 --- a/src/snapshots/prefligit__config__tests__read_manifest.snap +++ b/src/snapshots/prefligit__config__tests__read_manifest.snap @@ -9,111 +9,117 @@ ManifestWire { name: "pip-compile", entry: "uv pip compile", language: Python, - alias: None, - files: Some( - "^requirements\\.(in|txt)$", - ), - exclude: None, - types: None, - types_or: None, - exclude_types: None, - additional_dependencies: Some( - [], - ), - args: Some( - [], - ), - always_run: None, - fail_fast: None, - pass_filenames: Some( - false, - ), - description: Some( - "Automatically run 'uv pip compile' on your requirements", - ), - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: Some( - "2.9.2", - ), + options: HookOptions { + alias: None, + files: Some( + "^requirements\\.(in|txt)$", + ), + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: Some( + [], + ), + args: Some( + [], + ), + always_run: None, + fail_fast: None, + pass_filenames: Some( + false, + ), + description: Some( + "Automatically run 'uv pip compile' on your requirements", + ), + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: Some( + "2.9.2", + ), + }, }, ManifestHook { id: "uv-lock", name: "uv-lock", entry: "uv lock", language: Python, - alias: None, - files: Some( - "^(uv\\.lock|pyproject\\.toml|uv\\.toml)$", - ), - exclude: None, - types: None, - types_or: None, - exclude_types: None, - additional_dependencies: Some( - [], - ), - args: Some( - [], - ), - always_run: None, - fail_fast: None, - pass_filenames: Some( - false, - ), - description: Some( - "Automatically run 'uv lock' on your project dependencies", - ), - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: Some( - "2.9.2", - ), + options: HookOptions { + alias: None, + files: Some( + "^(uv\\.lock|pyproject\\.toml|uv\\.toml)$", + ), + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: Some( + [], + ), + args: Some( + [], + ), + always_run: None, + fail_fast: None, + pass_filenames: Some( + false, + ), + description: Some( + "Automatically run 'uv lock' on your project dependencies", + ), + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: Some( + "2.9.2", + ), + }, }, ManifestHook { id: "uv-export", name: "uv-export", entry: "uv export", language: Python, - alias: None, - files: Some( - "^uv\\.lock$", - ), - exclude: None, - types: None, - types_or: None, - exclude_types: None, - additional_dependencies: Some( - [], - ), - args: Some( - [ - "--frozen", - "--output-file=requirements.txt", - ], - ), - always_run: None, - fail_fast: None, - pass_filenames: Some( - false, - ), - description: Some( - "Automatically run 'uv export' on your project dependencies", - ), - language_version: None, - log_file: None, - require_serial: None, - stages: None, - verbose: None, - minimum_pre_commit_version: Some( - "2.9.2", - ), + options: HookOptions { + alias: None, + files: Some( + "^uv\\.lock$", + ), + exclude: None, + types: None, + types_or: None, + exclude_types: None, + additional_dependencies: Some( + [], + ), + args: Some( + [ + "--frozen", + "--output-file=requirements.txt", + ], + ), + always_run: None, + fail_fast: None, + pass_filenames: Some( + false, + ), + description: Some( + "Automatically run 'uv export' on your project dependencies", + ), + language_version: None, + log_file: None, + require_serial: None, + stages: None, + verbose: None, + minimum_pre_commit_version: Some( + "2.9.2", + ), + }, }, ], }