From af4f5911bf7c5301ea763c5856d55582002b48cd Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Tue, 2 Dec 2025 16:50:10 +0000 Subject: [PATCH 1/5] build: add figment for multi-provider configuration --- Cargo.lock | 151 ++++++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + 2 files changed, 140 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d58fca343..bb11af55a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -95,7 +95,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -195,6 +195,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -671,7 +680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -701,6 +710,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml 0.8.23", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.26" @@ -1278,6 +1301,12 @@ dependencies = [ "str_stack", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "insta" version = "1.44.3" @@ -1326,7 +1355,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1611,7 +1640,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1702,6 +1731,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1842,6 +1894,7 @@ dependencies = [ "dunce", "etcetera", "fancy-regex", + "figment", "fs-err", "futures", "hex", @@ -1885,7 +1938,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", - "toml", + "toml 0.9.8", "tracing", "tracing-subscriber", "unicode-width 0.2.2", @@ -1927,6 +1980,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "psm" version = "0.1.28" @@ -2209,7 +2275,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2382,6 +2448,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.3" @@ -2516,7 +2591,6 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.52.0", "windows-sys 0.59.0", ] @@ -2608,7 +2682,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2774,6 +2848,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + [[package]] name = "toml" version = "0.9.8" @@ -2783,13 +2869,22 @@ dependencies = [ "foldhash", "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -2799,6 +2894,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.4" @@ -2808,6 +2917,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.4" @@ -2926,6 +3041,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-id" version = "0.3.6" @@ -3202,7 +3326,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3498,6 +3622,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] [[package]] name = "winsafe" diff --git a/Cargo.toml b/Cargo.toml index 171617226..508d1cb74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } unicode-width = { version = "0.2.0" } walkdir = { version = "2.5.0" } which = { version = "8.0.0" } +figment = { version = "0.10.19", features = ["env", "toml"] } [target.'cfg(unix)'.dependencies] prek-pty = { workspace = true } From 8259d432da3414518438bc1ed180ad3164839011 Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Tue, 2 Dec 2025 22:05:27 +0000 Subject: [PATCH 2/5] feat(WIP): figment settings --- Cargo.lock | 25 +++ Cargo.toml | 1 + docs/cli.md | 30 ++-- src/cli/mod.rs | 41 ++--- src/main.rs | 18 +- src/settings.rs | 440 ++++++++++++++++++++++++++++++++++++++++++++++++ src/store.rs | 13 +- 7 files changed, 515 insertions(+), 53 deletions(-) create mode 100644 src/settings.rs diff --git a/Cargo.lock b/Cargo.lock index bb11af55a..04e26e3e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -717,8 +717,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic", + "parking_lot", "pear", "serde", + "tempfile", "toml 0.8.23", "uncased", "version_check", @@ -1725,6 +1727,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + [[package]] name = "path-clean" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 508d1cb74..6d8d2fd43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ fs-err = { version = "3.1.0" } assert_cmd = { version = "2.0.16", features = ["color"] } assert_fs = { version = "1.1.2" } etcetera = { version = "0.11.0" } +figment = { version = "0.10.19", features = ["test"] } insta = { version = "1.40.0", features = ["filters"] } insta-cmd = { version = "0.6.0" } markdown = { version = "1.0.0" } diff --git a/docs/cli.md b/docs/cli.md index 56c9f9621..538597700 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -60,7 +60,7 @@ prek install [OPTIONS] [HOOK|PROJECT]...
--allow-missing-config

Allow a missing pre-commit configuration file

--cd, -C dir

Change to directory before running

--color color

Whether to use color in output

-

May also be set with the PREK_COLOR environment variable.

[default: auto]

Possible values:

+

Possible values:

  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • always: Enables colored output regardless of the detected environment
  • @@ -127,7 +127,7 @@ prek install-hooks [OPTIONS] [HOOK|PROJECT]...
    --cd, -C dir

    Change to directory before running

    --color color

    Whether to use color in output

    -

    May also be set with the PREK_COLOR environment variable.

    [default: auto]

    Possible values:

    +

    Possible values:

    • auto: Enables colored output only when the output is going to a terminal or TTY with support
    • always: Enables colored output regardless of the detected environment
    • @@ -191,7 +191,7 @@ prek run [OPTIONS] [HOOK|PROJECT]...
      --all-files, -a

      Run on all files in the repo

      --cd, -C dir

      Change to directory before running

      --color color

      Whether to use color in output

      -

      May also be set with the PREK_COLOR environment variable.

      [default: auto]

      Possible values:

      +

      Possible values:

      • auto: Enables colored output only when the output is going to a terminal or TTY with support
      • always: Enables colored output regardless of the detected environment
      • @@ -277,7 +277,7 @@ prek list [OPTIONS] [HOOK|PROJECT]...
        --cd, -C dir

        Change to directory before running

        --color color

        Whether to use color in output

        -

        May also be set with the PREK_COLOR environment variable.

        [default: auto]

        Possible values:

        +

        Possible values:

        • auto: Enables colored output only when the output is going to a terminal or TTY with support
        • always: Enables colored output regardless of the detected environment
        • @@ -364,7 +364,7 @@ prek uninstall [OPTIONS]
          --cd, -C dir

          Change to directory before running

          --color color

          Whether to use color in output

          -

          May also be set with the PREK_COLOR environment variable.

          [default: auto]

          Possible values:

          +

          Possible values:

          • auto: Enables colored output only when the output is going to a terminal or TTY with support
          • always: Enables colored output regardless of the detected environment
          • @@ -400,7 +400,7 @@ prek validate-config [OPTIONS] [CONFIG]...
            --cd, -C dir

            Change to directory before running

            --color color

            Whether to use color in output

            -

            May also be set with the PREK_COLOR environment variable.

            [default: auto]

            Possible values:

            +

            Possible values:

            • auto: Enables colored output only when the output is going to a terminal or TTY with support
            • always: Enables colored output regardless of the detected environment
            • @@ -436,7 +436,7 @@ prek validate-manifest [OPTIONS] [MANIFEST]...
              --cd, -C dir

              Change to directory before running

              --color color

              Whether to use color in output

              -

              May also be set with the PREK_COLOR environment variable.

              [default: auto]

              Possible values:

              +

              Possible values:

              • auto: Enables colored output only when the output is going to a terminal or TTY with support
              • always: Enables colored output regardless of the detected environment
              • @@ -467,7 +467,7 @@ prek sample-config [OPTIONS]
                --cd, -C dir

                Change to directory before running

                --color color

                Whether to use color in output

                -

                May also be set with the PREK_COLOR environment variable.

                [default: auto]

                Possible values:

                +

                Possible values:

                • auto: Enables colored output only when the output is going to a terminal or TTY with support
                • always: Enables colored output regardless of the detected environment
                • @@ -500,7 +500,7 @@ prek auto-update [OPTIONS]
                  --bleeding-edge

                  Update to the bleeding edge of the default branch instead of the latest tagged version

                  --cd, -C dir

                  Change to directory before running

                  --color color

                  Whether to use color in output

                  -

                  May also be set with the PREK_COLOR environment variable.

                  [default: auto]

                  Possible values:

                  +

                  Possible values:

                  • auto: Enables colored output only when the output is going to a terminal or TTY with support
                  • always: Enables colored output regardless of the detected environment
                  • @@ -553,7 +553,7 @@ prek cache dir [OPTIONS]
                    --cd, -C dir

                    Change to directory before running

                    --color color

                    Whether to use color in output

                    -

                    May also be set with the PREK_COLOR environment variable.

                    [default: auto]

                    Possible values:

                    +

                    Possible values:

                    • auto: Enables colored output only when the output is going to a terminal or TTY with support
                    • always: Enables colored output regardless of the detected environment
                    • @@ -584,7 +584,7 @@ prek cache gc [OPTIONS]
                      --cd, -C dir

                      Change to directory before running

                      --color color

                      Whether to use color in output

                      -

                      May also be set with the PREK_COLOR environment variable.

                      [default: auto]

                      Possible values:

                      +

                      Possible values:

                      • auto: Enables colored output only when the output is going to a terminal or TTY with support
                      • always: Enables colored output regardless of the detected environment
                      • @@ -615,7 +615,7 @@ prek cache clean [OPTIONS]
                        --cd, -C dir

                        Change to directory before running

                        --color color

                        Whether to use color in output

                        -

                        May also be set with the PREK_COLOR environment variable.

                        [default: auto]

                        Possible values:

                        +

                        Possible values:

                        • auto: Enables colored output only when the output is going to a terminal or TTY with support
                        • always: Enables colored output regardless of the detected environment
                        • @@ -683,7 +683,7 @@ prek init-template-dir [OPTIONS]
                          --cd, -C dir

                          Change to directory before running

                          --color color

                          Whether to use color in output

                          -

                          May also be set with the PREK_COLOR environment variable.

                          [default: auto]

                          Possible values:

                          +

                          Possible values:

                          • auto: Enables colored output only when the output is going to a terminal or TTY with support
                          • always: Enables colored output regardless of the detected environment
                          • @@ -748,7 +748,7 @@ prek try-repo [OPTIONS] [HOOK|PROJECT]...
                            --all-files, -a

                            Run on all files in the repo

                            --cd, -C dir

                            Change to directory before running

                            --color color

                            Whether to use color in output

                            -

                            May also be set with the PREK_COLOR environment variable.

                            [default: auto]

                            Possible values:

                            +

                            Possible values:

                            • auto: Enables colored output only when the output is going to a terminal or TTY with support
                            • always: Enables colored output regardless of the detected environment
                            • @@ -837,7 +837,7 @@ prek self update [OPTIONS] [TARGET_VERSION]
                              --cd, -C dir

                              Change to directory before running

                              --color color

                              Whether to use color in output

                              -

                              May also be set with the PREK_COLOR environment variable.

                              [default: auto]

                              Possible values:

                              +

                              Possible values:

                              • auto: Enables colored output only when the output is going to a terminal or TTY with support
                              • always: Enables colored output regardless of the detected environment
                              • diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 96a39112e..8b2c1695b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -9,9 +9,9 @@ use clap_complete::engine::ArgValueCompleter; use serde::{Deserialize, Serialize}; use prek_consts::CONFIG_FILE; -use prek_consts::env_vars::EnvVars; use crate::config::{HookType, Language, Stage}; +use crate::settings::{CliOverrides, ColorChoice}; mod auto_update; mod cache_clean; @@ -72,28 +72,6 @@ impl From for ExitCode { } } -#[derive(Debug, Copy, Clone, clap::ValueEnum)] -pub enum ColorChoice { - /// Enables colored output only when the output is going to a terminal or TTY with support. - Auto, - - /// Enables colored output regardless of the detected environment. - Always, - - /// Disables colored output. - Never, -} - -impl From for anstream::ColorChoice { - fn from(value: ColorChoice) -> Self { - match value { - ColorChoice::Auto => Self::Auto, - ColorChoice::Always => Self::Always, - ColorChoice::Never => Self::Never, - } - } -} - const STYLES: Styles = Styles::styled() .header(AnsiColor::Green.on_default().effects(Effects::BOLD)) .usage(AnsiColor::Green.on_default().effects(Effects::BOLD)) @@ -144,14 +122,8 @@ pub(crate) struct GlobalArgs { pub(crate) cd: Option, /// Whether to use color in output. - #[arg( - global = true, - long, - value_enum, - env = EnvVars::PREK_COLOR, - default_value_t = ColorChoice::Auto, - )] - pub(crate) color: ColorChoice, + #[arg(global = true, long, value_enum)] + pub(crate) color: Option, /// Refresh all cached data. #[arg(global = true, long)] @@ -198,6 +170,13 @@ pub(crate) struct GlobalArgs { pub show_settings: bool, } +impl GlobalArgs { + /// Build CLI overrides from the parsed arguments. + pub fn cli_overrides(&self) -> CliOverrides { + CliOverrides::new().color(self.color) + } +} + #[derive(Debug, Subcommand)] pub(crate) enum Command { /// Install the prek git hook. diff --git a/src/main.rs b/src/main.rs index 7f3be6990..3061e75bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ use crate::cli::{CacheCommand, CacheNamespace, Cli, Command, ExitStatus}; use crate::cli::{SelfCommand, SelfNamespace, SelfUpdateArgs}; use crate::printer::Printer; use crate::run::USE_COLOR; +use crate::settings::Settings; use crate::store::Store; mod archive; @@ -40,6 +41,7 @@ mod process; #[cfg(all(unix, feature = "profiler"))] mod profiler; mod run; +pub mod settings; mod store; mod version; mod warnings; @@ -141,7 +143,21 @@ async fn run(mut cli: Cli) -> Result { // Enabled ANSI colors on Windows. let _ = anstyle_query::windows::enable_ansi_colors(); - ColorChoice::write_global(cli.globals.color.into()); + // Determine working directory early (--cd flag or current dir) + let working_dir = cli + .globals + .cd + .clone() + .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); + + // Initialize settings from all sources (env vars, pyproject.toml, CLI overrides) + Settings::init_with_cli(&working_dir, cli.globals.cli_overrides()) + .context("Failed to load configuration")?; + + let settings = Settings::get(); + + // Set color from resolved settings (CLI > pyproject.toml > env var > default) + ColorChoice::write_global(settings.resolved_color().into()); let store = Store::from_settings()?; let log_file = LogFile::from_args(cli.globals.log_file.clone(), cli.globals.no_log_file); diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 000000000..b89548965 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,440 @@ +//! Unified configuration settings for prek. +//! +//! Settings are resolved from multiple sources with the following precedence (highest to lowest): +//! 1. CLI flags (handled by clap, merged separately) +//! 2. `pyproject.toml` `[tool.prek]` section +//! 3. Environment variables (`PREK_*`) +//! 4. Built-in defaults +#![allow(clippy::result_large_err)] + +use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, RwLock}; + +use figment::providers::{Env, Serialized}; +use figment::{Figment, Profile, Provider, value::Map}; +use serde::{Deserialize, Serialize}; + +/// Global settings instance, initialized lazily. +/// +/// Call `Settings::init()` early in main to set the working directory, +/// or it will default to the current directory. +static SETTINGS: LazyLock> = LazyLock::new(|| RwLock::new(Settings::default())); + +/// Check if settings have been initialized +static INITIALIZED: LazyLock> = LazyLock::new(|| RwLock::new(false)); + +/// Prek configuration settings. +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +#[allow(clippy::struct_excessive_bools)] +pub struct Settings { + /// Hook IDs to skip (equivalent to `PREK_SKIP`). + #[serde(default)] + pub skip: Vec, + + /// Override the prek data directory (equivalent to `PREK_HOME`). + pub home: Option, + + /// Control colored output: "auto", "always", or "never" (equivalent to `PREK_COLOR`). + pub color: Option, + + /// Allow running without a `.pre-commit-config.yaml` (equivalent to `PREK_ALLOW_NO_CONFIG`). + #[serde(default)] + pub allow_no_config: bool, + + /// Disable parallelism for installs and runs (equivalent to `PREK_NO_CONCURRENCY`). + #[serde(default)] + pub no_concurrency: bool, + + /// Disable Rust-native built-in hooks (equivalent to `PREK_NO_FAST_PATH`). + #[serde(default)] + pub no_fast_path: bool, + + /// Control how uv is installed (equivalent to `PREK_UV_SOURCE`). + pub uv_source: Option, + + /// Use system's trusted store instead of bundled roots (equivalent to `PREK_NATIVE_TLS`). + #[serde(default)] + pub native_tls: bool, + + /// Container runtime to use: "auto", "docker", or "podman" (equivalent to `PREK_CONTAINER_RUNTIME`). + pub container_runtime: Option, +} + +/// Color output choice. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] +#[serde(rename_all = "lowercase")] +pub enum ColorChoice { + /// Enables colored output only when the output is going to a terminal or TTY with support. + #[default] + Auto, + /// Enables colored output regardless of the detected environment. + Always, + /// Disables colored output. + Never, +} + +impl From for anstream::ColorChoice { + fn from(value: ColorChoice) -> Self { + match value { + ColorChoice::Auto => Self::Auto, + ColorChoice::Always => Self::Always, + ColorChoice::Never => Self::Never, + } + } +} + +impl std::fmt::Display for ColorChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Auto => write!(f, "auto"), + Self::Always => write!(f, "always"), + Self::Never => write!(f, "never"), + } + } +} + +/// Container runtime choice. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] +#[serde(rename_all = "lowercase")] +pub enum ContainerRuntime { + /// Auto-detect available runtime. + #[default] + Auto, + /// Use Docker. + Docker, + /// Use Podman. + Podman, +} + +impl std::fmt::Display for ContainerRuntime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Auto => write!(f, "auto"), + Self::Docker => write!(f, "docker"), + Self::Podman => write!(f, "podman"), + } + } +} + +/// Wrapper to extract `[tool.prek]` from pyproject.toml +#[derive(Debug, Deserialize)] +struct PyProjectToml { + tool: Option, +} + +#[derive(Debug, Deserialize)] +struct PyProjectTool { + prek: Option, +} + +/// Custom provider that extracts `[tool.prek]` from a TOML file +struct PyProjectProvider { + path: PathBuf, +} + +impl PyProjectProvider { + fn new(path: impl Into) -> Self { + Self { path: path.into() } + } +} + +impl Provider for PyProjectProvider { + fn metadata(&self) -> figment::Metadata { + figment::Metadata::named("pyproject.toml [tool.prek]") + .source(self.path.display().to_string()) + } + + fn data(&self) -> Result, figment::Error> { + let Ok(content) = std::fs::read_to_string(&self.path) else { + return Ok(Map::new()); + }; + + let pyproject: PyProjectToml = match toml::from_str(&content) { + Ok(p) => p, + Err(_) => return Ok(Map::new()), + }; + + match pyproject.tool.and_then(|t| t.prek) { + Some(settings) => Serialized::defaults(settings).data(), + None => Ok(Map::new()), + } + } +} + +impl Settings { + /// Initialize settings from the given working directory. + /// + /// This should be called early in `main()` before any settings are accessed. + /// If not called, settings will use the current directory when first accessed. + pub fn init(working_dir: &Path) -> Result<(), figment::Error> { + let settings = Self::discover(working_dir)?; + + let mut guard = SETTINGS.write().expect("settings lock poisoned"); + *guard = settings; + + let mut init_guard = INITIALIZED.write().expect("initialized lock poisoned"); + *init_guard = true; + + Ok(()) + } + + /// Initialize settings with CLI overrides. + /// + /// CLI flags take highest precedence over all other sources. + pub fn init_with_cli( + working_dir: &Path, + cli_overrides: CliOverrides, + ) -> Result<(), figment::Error> { + let figment = Self::build_figment(working_dir).merge(Serialized::defaults(cli_overrides)); + + let settings: Settings = figment.extract()?; + + let mut guard = SETTINGS.write().expect("settings lock poisoned"); + *guard = settings; + + let mut init_guard = INITIALIZED.write().expect("initialized lock poisoned"); + *init_guard = true; + + Ok(()) + } + + /// Get the global settings instance. + /// + /// If settings haven't been initialized, this will initialize them + /// using the current directory. + pub fn get() -> Settings { + // Check if we need to initialize + { + let init_guard = INITIALIZED.read().expect("initialized lock poisoned"); + if !*init_guard { + drop(init_guard); + // Try to initialize with current directory + let cwd = std::env::current_dir().unwrap_or_default(); + let _ = Self::init(&cwd); + } + } + + let guard = SETTINGS.read().expect("settings lock poisoned"); + guard.clone() + } + + /// Build the figment for the given working directory. + fn build_figment(working_dir: &Path) -> Figment { + let mut figment = Figment::new() + // Lowest precedence: built-in defaults + .merge(Serialized::defaults(Settings::default())) + // Next: environment variables + .merge( + Env::prefixed("PREK_") + .map(|key| { + // Convert PREK_FOO_BAR to foo-bar for serde + key.as_str().to_lowercase().replace('_', "-").into() + }) + .split(","), // Allow comma-separated lists for `skip` + ); + + // Walk up to find pyproject.toml + let mut current = Some(working_dir); + while let Some(dir) = current { + let pyproject_path = dir.join("pyproject.toml"); + if pyproject_path.exists() { + // Higher precedence: pyproject.toml [tool.prek] + figment = figment.merge(PyProjectProvider::new(pyproject_path)); + break; + } + current = dir.parent(); + } + + figment + } + + /// Discover and resolve settings from the given directory. + fn discover(start_dir: &Path) -> Result { + Self::build_figment(start_dir).extract() + } + + /// Check if a hook should be skipped. + pub fn should_skip(&self, hook_id: &str) -> bool { + self.skip.iter().any(|s| s == hook_id) + } + + /// Get the resolved color choice. + pub fn resolved_color(&self) -> ColorChoice { + self.color.unwrap_or_default() + } + + /// Get the resolved container runtime. + pub fn resolved_container_runtime(&self) -> ContainerRuntime { + self.container_runtime.unwrap_or_default() + } +} + +/// CLI overrides that take highest precedence. +/// +/// This struct contains only the fields that can be set via CLI flags. +/// Fields are all optional - `None` means "use value from other sources". +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct CliOverrides { + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub no_concurrency: Option, + // Add other CLI-settable options here as needed +} + +impl CliOverrides { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn color(mut self, color: Option) -> Self { + self.color = color; + self + } + + #[must_use] + pub fn no_concurrency(mut self, value: bool) -> Self { + if value { + self.no_concurrency = Some(true); + } + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + #[test] + fn test_default_settings() { + let settings = Settings::default(); + assert!(settings.skip.is_empty()); + assert!(settings.home.is_none()); + assert!(settings.color.is_none()); + assert!(!settings.allow_no_config); + assert!(!settings.no_concurrency); + } + + #[test] + fn test_pyproject_loading() { + let dir = TempDir::new().unwrap(); + let pyproject = dir.path().join("pyproject.toml"); + + let mut file = std::fs::File::create(&pyproject).unwrap(); + write!( + file, + r#" +[tool.prek] +skip = ["black", "ruff"] +no-concurrency = true +color = "always" +"# + ) + .unwrap(); + + let settings = Settings::discover(dir.path()).unwrap(); + + assert_eq!(settings.skip, vec!["black", "ruff"]); + assert!(settings.no_concurrency); + assert_eq!(settings.color, Some(ColorChoice::Always)); + } + + #[test] + fn test_env_var_loading() { + figment::Jail::expect_with(|jail| { + jail.set_env("PREK_SKIP", "hook1,hook2"); + jail.set_env("PREK_NO_CONCURRENCY", "true"); + jail.set_env("PREK_COLOR", "never"); + + let settings = Settings::discover(jail.directory())?; + + assert_eq!(settings.skip, vec!["hook1", "hook2"]); + assert!(settings.no_concurrency); + assert_eq!(settings.color, Some(ColorChoice::Never)); + + Ok(()) + }); + } + + #[test] + fn test_pyproject_overrides_env() { + figment::Jail::expect_with(|jail| { + // Set env var + jail.set_env("PREK_COLOR", "never"); + + // Create pyproject.toml with different value + jail.create_file( + "pyproject.toml", + r#" +[tool.prek] +color = "always" +"#, + )?; + + let settings = Settings::discover(jail.directory())?; + + // pyproject.toml should win + assert_eq!(settings.color, Some(ColorChoice::Always)); + + Ok(()) + }); + } + + #[test] + fn test_cli_overrides_all() { + figment::Jail::expect_with(|jail| { + jail.set_env("PREK_COLOR", "never"); + jail.create_file( + "pyproject.toml", + r#" +[tool.prek] +color = "auto" +"#, + )?; + + let figment = Settings::build_figment(jail.directory()).merge(Serialized::defaults( + CliOverrides { + color: Some(ColorChoice::Always), + ..Default::default() + }, + )); + + let settings: Settings = figment.extract()?; + + // CLI should win + assert_eq!(settings.color, Some(ColorChoice::Always)); + + Ok(()) + }); + } + + #[test] + fn test_walks_up_directory_tree() { + figment::Jail::expect_with(|jail| { + // Create pyproject.toml in parent + jail.create_file( + "pyproject.toml", + r#" +[tool.prek] +skip = ["parent-hook"] +"#, + )?; + + // Create subdirectory + std::fs::create_dir_all(jail.directory().join("subdir/nested")) + .map_err(|e| e.to_string())?; // Convert io::Error to String, which impls Into + + let settings = Settings::discover(&jail.directory().join("subdir/nested"))?; + + assert_eq!(settings.skip, vec!["parent-hook"]); + + Ok(()) + }); + } +} diff --git a/src/store.rs b/src/store.rs index 9ba56352a..39b3a2761 100644 --- a/src/store.rs +++ b/src/store.rs @@ -9,13 +9,12 @@ use futures::StreamExt; use thiserror::Error; use tracing::{debug, warn}; -use prek_consts::env_vars::EnvVars; - use crate::config::RemoteRepo; use crate::fs::LockedFile; use crate::git::clone_repo; use crate::hook::InstallInfo; use crate::run::CONCURRENCY; +use crate::settings::Settings; use crate::workspace::HookInitReporter; #[derive(Debug, Error)] @@ -41,13 +40,15 @@ impl Store { Self { path: path.into() } } - /// Create a store from environment variables or default paths. + /// Create a store from settings or default paths. pub(crate) fn from_settings() -> Result { - let path = if let Some(path) = EnvVars::var_os(EnvVars::PREK_HOME) { - Some(path.into()) + let settings = Settings::get(); + + let path = if let Some(home) = settings.home { + Some(home) } else { etcetera::choose_base_strategy() - .map(|path| path.cache_dir().join("prek")) + .map(|strategy| strategy.cache_dir().join("prek")) .ok() }; From cf9919296ad0b65d0fc4a49ae403ee0515e7155a Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Wed, 3 Dec 2025 10:44:50 +0000 Subject: [PATCH 3/5] fix: dedicated env var provider --- docs/cli.md | 2 +- src/settings.rs | 108 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 538597700..18b38eec2 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -646,7 +646,7 @@ prek cache size [OPTIONS]
                                --cd, -C dir

                                Change to directory before running

                                --color color

                                Whether to use color in output

                                -

                                May also be set with the PREK_COLOR environment variable.

                                [default: auto]

                                Possible values:

                                +

                                Possible values:

                                • auto: Enables colored output only when the output is going to a terminal or TTY with support
                                • always: Enables colored output regardless of the detected environment
                                • diff --git a/src/settings.rs b/src/settings.rs index b89548965..12d5e38c0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,8 +10,9 @@ use std::path::{Path, PathBuf}; use std::sync::{LazyLock, RwLock}; -use figment::providers::{Env, Serialized}; +use figment::providers::Serialized; use figment::{Figment, Profile, Provider, value::Map}; +use prek_consts::env_vars::EnvVars; use serde::{Deserialize, Serialize}; /// Global settings instance, initialized lazily. @@ -162,6 +163,75 @@ impl Provider for PyProjectProvider { } } +/// Custom provider for PREK_* environment variables that handles +/// special cases like comma-separated lists and boolean values. +struct PrekEnvProvider; + +impl Provider for PrekEnvProvider { + fn metadata(&self) -> figment::Metadata { + figment::Metadata::named("PREK_* environment variables") + } + + fn data(&self) -> Result, figment::Error> { + use figment::value::{Dict, Value}; + + let mut dict = Dict::new(); + + // PREK_SKIP or SKIP - comma-separated list + if let Some(val) = EnvVars::var(EnvVars::PREK_SKIP) + .ok() + .or_else(|| EnvVars::var(EnvVars::SKIP).ok()) + { + let items: Vec = val + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .map(Value::from) + .collect(); + dict.insert("skip".into(), Value::from(items)); + } + + // PREK_HOME or PRE_COMMIT_HOME (EnvVars::var handles the fallback) + if let Ok(val) = EnvVars::var(EnvVars::PREK_HOME) { + dict.insert("home".into(), Value::from(val)); + } + + if let Ok(val) = EnvVars::var(EnvVars::PREK_COLOR) { + dict.insert("color".into(), Value::from(val)); + } + + // PREK_ALLOW_NO_CONFIG - boolish values (EnvVars::var handles PRE_COMMIT fallback) + if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_ALLOW_NO_CONFIG) { + dict.insert("allow-no-config".into(), Value::from(b)); + } + + // PREK_NO_CONCURRENCY - boolish values (EnvVars::var handles PRE_COMMIT fallback) + if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NO_CONCURRENCY) { + dict.insert("no-concurrency".into(), Value::from(b)); + } + + if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NO_FAST_PATH) { + dict.insert("no-fast-path".into(), Value::from(b)); + } + + if let Ok(val) = EnvVars::var(EnvVars::PREK_UV_SOURCE) { + dict.insert("uv-source".into(), Value::from(val)); + } + + if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NATIVE_TLS) { + dict.insert("native-tls".into(), Value::from(b)); + } + + if let Ok(val) = EnvVars::var(EnvVars::PREK_CONTAINER_RUNTIME) { + dict.insert("container-runtime".into(), Value::from(val)); + } + + let mut map = Map::new(); + map.insert(Profile::Default, dict); + Ok(map) + } +} + impl Settings { /// Initialize settings from the given working directory. /// @@ -169,13 +239,8 @@ impl Settings { /// If not called, settings will use the current directory when first accessed. pub fn init(working_dir: &Path) -> Result<(), figment::Error> { let settings = Self::discover(working_dir)?; - - let mut guard = SETTINGS.write().expect("settings lock poisoned"); - *guard = settings; - - let mut init_guard = INITIALIZED.write().expect("initialized lock poisoned"); - *init_guard = true; - + *SETTINGS.write().expect("settings lock poisoned") = settings; + *INITIALIZED.write().expect("initialized lock poisoned") = true; Ok(()) } @@ -187,15 +252,9 @@ impl Settings { cli_overrides: CliOverrides, ) -> Result<(), figment::Error> { let figment = Self::build_figment(working_dir).merge(Serialized::defaults(cli_overrides)); - let settings: Settings = figment.extract()?; - - let mut guard = SETTINGS.write().expect("settings lock poisoned"); - *guard = settings; - - let mut init_guard = INITIALIZED.write().expect("initialized lock poisoned"); - *init_guard = true; - + *SETTINGS.write().expect("settings lock poisoned") = settings; + *INITIALIZED.write().expect("initialized lock poisoned") = true; Ok(()) } @@ -214,9 +273,7 @@ impl Settings { let _ = Self::init(&cwd); } } - - let guard = SETTINGS.read().expect("settings lock poisoned"); - guard.clone() + SETTINGS.read().expect("settings lock poisoned").clone() } /// Build the figment for the given working directory. @@ -224,15 +281,8 @@ impl Settings { let mut figment = Figment::new() // Lowest precedence: built-in defaults .merge(Serialized::defaults(Settings::default())) - // Next: environment variables - .merge( - Env::prefixed("PREK_") - .map(|key| { - // Convert PREK_FOO_BAR to foo-bar for serde - key.as_str().to_lowercase().replace('_', "-").into() - }) - .split(","), // Allow comma-separated lists for `skip` - ); + // Next: environment variables (custom provider for special handling) + .merge(PrekEnvProvider); // Walk up to find pyproject.toml let mut current = Some(working_dir); @@ -349,7 +399,7 @@ color = "always" fn test_env_var_loading() { figment::Jail::expect_with(|jail| { jail.set_env("PREK_SKIP", "hook1,hook2"); - jail.set_env("PREK_NO_CONCURRENCY", "true"); + jail.set_env("PREK_NO_CONCURRENCY", "1"); jail.set_env("PREK_COLOR", "never"); let settings = Settings::discover(jail.directory())?; From 571ceb22129f012272c2e23130b26f3289dcb59a Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Wed, 3 Dec 2025 12:39:40 +0000 Subject: [PATCH 4/5] chore: switch to figment2 fork See discussion in https://github.com/SergioBenitez/Figment/issues/148 --- Cargo.lock | 107 +++++++---------------------------------------------- Cargo.toml | 4 +- 2 files changed, 16 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04e26e3e4..42052cb38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,17 +711,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "figment" -version = "0.10.19" +name = "figment2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +checksum = "5856dd39567e93bf4d63221875c25f620747163d1e8073b4b54d066051d2093d" dependencies = [ "atomic", "parking_lot", - "pear", "serde", "tempfile", - "toml 0.8.23", + "toml_edit", "uncased", "version_check", ] @@ -1303,12 +1302,6 @@ dependencies = [ "str_stack", ] -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "insta" version = "1.44.3" @@ -1756,29 +1749,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1919,7 +1889,7 @@ dependencies = [ "dunce", "etcetera", "fancy-regex", - "figment", + "figment2", "fs-err", "futures", "hex", @@ -1963,7 +1933,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", - "toml 0.9.8", + "toml", "tracing", "tracing-subscriber", "unicode-width 0.2.2", @@ -2005,19 +1975,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", - "yansi", -] - [[package]] name = "psm" version = "0.1.28" @@ -2473,15 +2430,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.3" @@ -2873,18 +2821,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - [[package]] name = "toml" version = "0.9.8" @@ -2894,22 +2830,13 @@ dependencies = [ "foldhash", "indexmap", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned", + "toml_datetime", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.3" @@ -2921,15 +2848,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", "winnow", ] @@ -2942,12 +2869,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 6d8d2fd43..174ed1262 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } unicode-width = { version = "0.2.0" } walkdir = { version = "2.5.0" } which = { version = "8.0.0" } -figment = { version = "0.10.19", features = ["env", "toml"] } +figment = { package = "figment2", version = "0.11.0", features = ["env", "toml"] } [target.'cfg(unix)'.dependencies] prek-pty = { workspace = true } @@ -112,7 +112,7 @@ fs-err = { version = "3.1.0" } assert_cmd = { version = "2.0.16", features = ["color"] } assert_fs = { version = "1.1.2" } etcetera = { version = "0.11.0" } -figment = { version = "0.10.19", features = ["test"] } +figment = { package = "figment2", version = "0.11.0", features = ["test"] } insta = { version = "1.40.0", features = ["filters"] } insta-cmd = { version = "0.6.0" } markdown = { version = "1.0.0" } From 0e1361bcfae6449bdeb3c8e8a678982f01e0afd9 Mon Sep 17 00:00:00 2001 From: Louis Maddox Date: Wed, 3 Dec 2025 14:31:55 +0000 Subject: [PATCH 5/5] revert: providers without figment --- Cargo.lock | 74 -------- Cargo.toml | 2 - src/main.rs | 3 +- src/settings.rs | 464 ++++++++++++++++++------------------------------ 4 files changed, 169 insertions(+), 374 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42052cb38..0b692ebf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,15 +195,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -710,21 +701,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "figment2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5856dd39567e93bf4d63221875c25f620747163d1e8073b4b54d066051d2093d" -dependencies = [ - "atomic", - "parking_lot", - "serde", - "tempfile", - "toml_edit", - "uncased", - "version_check", -] - [[package]] name = "filetime" version = "0.2.26" @@ -1720,29 +1696,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - [[package]] name = "path-clean" version = "1.0.1" @@ -1889,7 +1842,6 @@ dependencies = [ "dunce", "etcetera", "fancy-regex", - "figment2", "fs-err", "futures", "hex", @@ -2846,20 +2798,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - [[package]] name = "toml_parser" version = "1.0.4" @@ -2987,15 +2925,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-id" version = "0.3.6" @@ -3568,9 +3497,6 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] [[package]] name = "winsafe" diff --git a/Cargo.toml b/Cargo.toml index 174ed1262..171617226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,6 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } unicode-width = { version = "0.2.0" } walkdir = { version = "2.5.0" } which = { version = "8.0.0" } -figment = { package = "figment2", version = "0.11.0", features = ["env", "toml"] } [target.'cfg(unix)'.dependencies] prek-pty = { workspace = true } @@ -112,7 +111,6 @@ fs-err = { version = "3.1.0" } assert_cmd = { version = "2.0.16", features = ["color"] } assert_fs = { version = "1.1.2" } etcetera = { version = "0.11.0" } -figment = { package = "figment2", version = "0.11.0", features = ["test"] } insta = { version = "1.40.0", features = ["filters"] } insta-cmd = { version = "0.6.0" } markdown = { version = "1.0.0" } diff --git a/src/main.rs b/src/main.rs index 3061e75bc..6cda7ab34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,8 +151,7 @@ async fn run(mut cli: Cli) -> Result { .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); // Initialize settings from all sources (env vars, pyproject.toml, CLI overrides) - Settings::init_with_cli(&working_dir, cli.globals.cli_overrides()) - .context("Failed to load configuration")?; + Settings::init_with_cli(&working_dir, cli.globals.cli_overrides()); let settings = Settings::get(); diff --git a/src/settings.rs b/src/settings.rs index 12d5e38c0..3e02bd5b2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,23 +1,20 @@ //! Unified configuration settings for prek. //! //! Settings are resolved from multiple sources with the following precedence (highest to lowest): -//! 1. CLI flags (handled by clap, merged separately) +//! 1. CLI flags (merged via `CliOverrides`) //! 2. `pyproject.toml` `[tool.prek]` section //! 3. Environment variables (`PREK_*`) //! 4. Built-in defaults -#![allow(clippy::result_large_err)] use std::path::{Path, PathBuf}; use std::sync::{LazyLock, RwLock}; -use figment::providers::Serialized; -use figment::{Figment, Profile, Provider, value::Map}; use prek_consts::env_vars::EnvVars; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; /// Global settings instance, initialized lazily. /// -/// Call `Settings::init()` early in main to set the working directory, +/// Call `Settings::init_with_cli()` early in main to set the working directory, /// or it will default to the current directory. static SETTINGS: LazyLock> = LazyLock::new(|| RwLock::new(Settings::default())); @@ -25,45 +22,58 @@ static SETTINGS: LazyLock> = LazyLock::new(|| RwLock::new(Setti static INITIALIZED: LazyLock> = LazyLock::new(|| RwLock::new(false)); /// Prek configuration settings. -#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Clone, Default, PartialEq, Eq)] #[allow(clippy::struct_excessive_bools)] pub struct Settings { /// Hook IDs to skip (equivalent to `PREK_SKIP`). - #[serde(default)] pub skip: Vec, - /// Override the prek data directory (equivalent to `PREK_HOME`). pub home: Option, - /// Control colored output: "auto", "always", or "never" (equivalent to `PREK_COLOR`). pub color: Option, - /// Allow running without a `.pre-commit-config.yaml` (equivalent to `PREK_ALLOW_NO_CONFIG`). - #[serde(default)] pub allow_no_config: bool, - /// Disable parallelism for installs and runs (equivalent to `PREK_NO_CONCURRENCY`). - #[serde(default)] pub no_concurrency: bool, - /// Disable Rust-native built-in hooks (equivalent to `PREK_NO_FAST_PATH`). - #[serde(default)] pub no_fast_path: bool, - /// Control how uv is installed (equivalent to `PREK_UV_SOURCE`). pub uv_source: Option, - /// Use system's trusted store instead of bundled roots (equivalent to `PREK_NATIVE_TLS`). - #[serde(default)] pub native_tls: bool, - /// Container runtime to use: "auto", "docker", or "podman" (equivalent to `PREK_CONTAINER_RUNTIME`). pub container_runtime: Option, } +/// Settings as parsed from pyproject.toml `[tool.prek]` +#[derive(Debug, Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +#[allow(clippy::struct_excessive_bools)] +struct PyProjectSettings { + skip: Vec, + home: Option, + color: Option, + allow_no_config: bool, + no_concurrency: bool, + no_fast_path: bool, + uv_source: Option, + native_tls: bool, + container_runtime: Option, +} + +/// Wrapper to extract `[tool.prek]` from pyproject.toml +#[derive(Debug, Deserialize)] +struct PyProjectToml { + tool: Option, +} + +#[derive(Debug, Deserialize)] +struct PyProjectTool { + prek: Option, +} + /// Color output choice. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, clap::ValueEnum)] #[serde(rename_all = "lowercase")] pub enum ColorChoice { /// Enables colored output only when the output is going to a terminal or TTY with support. @@ -75,6 +85,16 @@ pub enum ColorChoice { Never, } +impl std::fmt::Display for ColorChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Auto => write!(f, "auto"), + Self::Always => write!(f, "always"), + Self::Never => write!(f, "never"), + } + } +} + impl From for anstream::ColorChoice { fn from(value: ColorChoice) -> Self { match value { @@ -85,18 +105,20 @@ impl From for anstream::ColorChoice { } } -impl std::fmt::Display for ColorChoice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Auto => write!(f, "auto"), - Self::Always => write!(f, "always"), - Self::Never => write!(f, "never"), +impl std::str::FromStr for ColorChoice { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "auto" => Ok(Self::Auto), + "always" => Ok(Self::Always), + "never" => Ok(Self::Never), + _ => Err(format!("invalid color choice: {s}")), } } } /// Container runtime choice. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, clap::ValueEnum)] #[serde(rename_all = "lowercase")] pub enum ContainerRuntime { /// Auto-detect available runtime. @@ -108,6 +130,18 @@ pub enum ContainerRuntime { Podman, } +impl std::str::FromStr for ContainerRuntime { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "auto" => Ok(Self::Auto), + "docker" => Ok(Self::Docker), + "podman" => Ok(Self::Podman), + _ => Err(format!("invalid container runtime: {s}")), + } + } +} + impl std::fmt::Display for ContainerRuntime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -118,144 +152,35 @@ impl std::fmt::Display for ContainerRuntime { } } -/// Wrapper to extract `[tool.prek]` from pyproject.toml -#[derive(Debug, Deserialize)] -struct PyProjectToml { - tool: Option, -} - -#[derive(Debug, Deserialize)] -struct PyProjectTool { - prek: Option, -} - -/// Custom provider that extracts `[tool.prek]` from a TOML file -struct PyProjectProvider { - path: PathBuf, -} - -impl PyProjectProvider { - fn new(path: impl Into) -> Self { - Self { path: path.into() } - } -} - -impl Provider for PyProjectProvider { - fn metadata(&self) -> figment::Metadata { - figment::Metadata::named("pyproject.toml [tool.prek]") - .source(self.path.display().to_string()) - } - - fn data(&self) -> Result, figment::Error> { - let Ok(content) = std::fs::read_to_string(&self.path) else { - return Ok(Map::new()); - }; - - let pyproject: PyProjectToml = match toml::from_str(&content) { - Ok(p) => p, - Err(_) => return Ok(Map::new()), - }; - - match pyproject.tool.and_then(|t| t.prek) { - Some(settings) => Serialized::defaults(settings).data(), - None => Ok(Map::new()), - } - } +/// CLI overrides that take highest precedence. +/// +/// This struct contains only the fields that can be set via CLI flags. +/// Fields are all optional - `None` means "use value from other sources". +#[derive(Copy, Debug, Clone, Default)] +pub struct CliOverrides { + pub color: Option, } -/// Custom provider for PREK_* environment variables that handles -/// special cases like comma-separated lists and boolean values. -struct PrekEnvProvider; - -impl Provider for PrekEnvProvider { - fn metadata(&self) -> figment::Metadata { - figment::Metadata::named("PREK_* environment variables") +impl CliOverrides { + pub fn new() -> Self { + Self::default() } - fn data(&self) -> Result, figment::Error> { - use figment::value::{Dict, Value}; - - let mut dict = Dict::new(); - - // PREK_SKIP or SKIP - comma-separated list - if let Some(val) = EnvVars::var(EnvVars::PREK_SKIP) - .ok() - .or_else(|| EnvVars::var(EnvVars::SKIP).ok()) - { - let items: Vec = val - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .map(Value::from) - .collect(); - dict.insert("skip".into(), Value::from(items)); - } - - // PREK_HOME or PRE_COMMIT_HOME (EnvVars::var handles the fallback) - if let Ok(val) = EnvVars::var(EnvVars::PREK_HOME) { - dict.insert("home".into(), Value::from(val)); - } - - if let Ok(val) = EnvVars::var(EnvVars::PREK_COLOR) { - dict.insert("color".into(), Value::from(val)); - } - - // PREK_ALLOW_NO_CONFIG - boolish values (EnvVars::var handles PRE_COMMIT fallback) - if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_ALLOW_NO_CONFIG) { - dict.insert("allow-no-config".into(), Value::from(b)); - } - - // PREK_NO_CONCURRENCY - boolish values (EnvVars::var handles PRE_COMMIT fallback) - if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NO_CONCURRENCY) { - dict.insert("no-concurrency".into(), Value::from(b)); - } - - if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NO_FAST_PATH) { - dict.insert("no-fast-path".into(), Value::from(b)); - } - - if let Ok(val) = EnvVars::var(EnvVars::PREK_UV_SOURCE) { - dict.insert("uv-source".into(), Value::from(val)); - } - - if let Some(b) = EnvVars::var_as_bool(EnvVars::PREK_NATIVE_TLS) { - dict.insert("native-tls".into(), Value::from(b)); - } - - if let Ok(val) = EnvVars::var(EnvVars::PREK_CONTAINER_RUNTIME) { - dict.insert("container-runtime".into(), Value::from(val)); - } - - let mut map = Map::new(); - map.insert(Profile::Default, dict); - Ok(map) + #[must_use] + pub fn color(mut self, color: Option) -> Self { + self.color = color; + self } } impl Settings { - /// Initialize settings from the given working directory. - /// - /// This should be called early in `main()` before any settings are accessed. - /// If not called, settings will use the current directory when first accessed. - pub fn init(working_dir: &Path) -> Result<(), figment::Error> { - let settings = Self::discover(working_dir)?; - *SETTINGS.write().expect("settings lock poisoned") = settings; - *INITIALIZED.write().expect("initialized lock poisoned") = true; - Ok(()) - } - /// Initialize settings with CLI overrides. /// - /// CLI flags take highest precedence over all other sources. - pub fn init_with_cli( - working_dir: &Path, - cli_overrides: CliOverrides, - ) -> Result<(), figment::Error> { - let figment = Self::build_figment(working_dir).merge(Serialized::defaults(cli_overrides)); - let settings: Settings = figment.extract()?; + /// This should be called early in `main()` before any settings are accessed. + pub fn init_with_cli(working_dir: &Path, cli: CliOverrides) { + let settings = Self::load(working_dir, cli); *SETTINGS.write().expect("settings lock poisoned") = settings; *INITIALIZED.write().expect("initialized lock poisoned") = true; - Ok(()) } /// Get the global settings instance. @@ -263,45 +188,54 @@ impl Settings { /// If settings haven't been initialized, this will initialize them /// using the current directory. pub fn get() -> Settings { - // Check if we need to initialize { - let init_guard = INITIALIZED.read().expect("initialized lock poisoned"); - if !*init_guard { - drop(init_guard); - // Try to initialize with current directory + let initialized = *INITIALIZED.read().expect("initialized lock poisoned"); + if !initialized { let cwd = std::env::current_dir().unwrap_or_default(); - let _ = Self::init(&cwd); + Self::init_with_cli(&cwd, CliOverrides::default()); } } SETTINGS.read().expect("settings lock poisoned").clone() } - /// Build the figment for the given working directory. - fn build_figment(working_dir: &Path) -> Figment { - let mut figment = Figment::new() - // Lowest precedence: built-in defaults - .merge(Serialized::defaults(Settings::default())) - // Next: environment variables (custom provider for special handling) - .merge(PrekEnvProvider); - - // Walk up to find pyproject.toml - let mut current = Some(working_dir); - while let Some(dir) = current { - let pyproject_path = dir.join("pyproject.toml"); - if pyproject_path.exists() { - // Higher precedence: pyproject.toml [tool.prek] - figment = figment.merge(PyProjectProvider::new(pyproject_path)); - break; - } - current = dir.parent(); + /// Load and merge settings from all sources. + fn load(start_dir: &Path, cli: CliOverrides) -> Self { + // Start with pyproject.toml settings (or defaults) + let pyproject = Self::load_pyproject(start_dir).unwrap_or_default(); + + // Layer env vars on top, then CLI + Settings { + skip: env_list("PREK_SKIP") + .or_else(|| env_list("SKIP")) + .unwrap_or(pyproject.skip), + home: env_path("PREK_HOME").or(pyproject.home), + color: cli + .color + .or_else(|| env_parse::("PREK_COLOR")) + .or(pyproject.color), + allow_no_config: env_bool("PREK_ALLOW_NO_CONFIG").unwrap_or(pyproject.allow_no_config), + no_concurrency: env_bool("PREK_NO_CONCURRENCY").unwrap_or(pyproject.no_concurrency), + no_fast_path: env_bool("PREK_NO_FAST_PATH").unwrap_or(pyproject.no_fast_path), + uv_source: env_string("PREK_UV_SOURCE").or(pyproject.uv_source), + native_tls: env_bool("PREK_NATIVE_TLS").unwrap_or(pyproject.native_tls), + container_runtime: env_parse("PREK_CONTAINER_RUNTIME").or(pyproject.container_runtime), } - - figment } - /// Discover and resolve settings from the given directory. - fn discover(start_dir: &Path) -> Result { - Self::build_figment(start_dir).extract() + /// Walk up the directory tree to find and parse pyproject.toml + fn load_pyproject(start_dir: &Path) -> Option { + let mut dir = Some(start_dir); + while let Some(d) = dir { + if let Ok(content) = std::fs::read_to_string(d.join("pyproject.toml")) { + if let Ok(parsed) = toml::from_str::(&content) { + if let Some(settings) = parsed.tool.and_then(|t| t.prek) { + return Some(settings); + } + } + } + dir = d.parent(); + } + None } /// Check if a hook should be skipped. @@ -320,39 +254,30 @@ impl Settings { } } -/// CLI overrides that take highest precedence. -/// -/// This struct contains only the fields that can be set via CLI flags. -/// Fields are all optional - `None` means "use value from other sources". -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct CliOverrides { - #[serde(skip_serializing_if = "Option::is_none")] - pub color: Option, +// Simple env var helpers +fn env_string(key: &str) -> Option { + EnvVars::var(key).ok() +} - #[serde(skip_serializing_if = "Option::is_none")] - pub no_concurrency: Option, - // Add other CLI-settable options here as needed +fn env_path(key: &str) -> Option { + env_string(key).map(PathBuf::from) } -impl CliOverrides { - pub fn new() -> Self { - Self::default() - } +fn env_bool(key: &str) -> Option { + EnvVars::var_as_bool(key) +} - #[must_use] - pub fn color(mut self, color: Option) -> Self { - self.color = color; - self - } +fn env_parse(key: &str) -> Option { + env_string(key).and_then(|s| s.parse().ok()) +} - #[must_use] - pub fn no_concurrency(mut self, value: bool) -> Self { - if value { - self.no_concurrency = Some(true); - } - self - } +fn env_list(key: &str) -> Option> { + env_string(key).map(|s| { + s.split(',') + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect() + }) } #[cfg(test)] @@ -366,8 +291,6 @@ mod tests { let settings = Settings::default(); assert!(settings.skip.is_empty()); assert!(settings.home.is_none()); - assert!(settings.color.is_none()); - assert!(!settings.allow_no_config); assert!(!settings.no_concurrency); } @@ -388,103 +311,52 @@ color = "always" ) .unwrap(); - let settings = Settings::discover(dir.path()).unwrap(); - + let settings = Settings::load(dir.path(), CliOverrides::default()); assert_eq!(settings.skip, vec!["black", "ruff"]); assert!(settings.no_concurrency); assert_eq!(settings.color, Some(ColorChoice::Always)); } #[test] - fn test_env_var_loading() { - figment::Jail::expect_with(|jail| { - jail.set_env("PREK_SKIP", "hook1,hook2"); - jail.set_env("PREK_NO_CONCURRENCY", "1"); - jail.set_env("PREK_COLOR", "never"); - - let settings = Settings::discover(jail.directory())?; - - assert_eq!(settings.skip, vec!["hook1", "hook2"]); - assert!(settings.no_concurrency); - assert_eq!(settings.color, Some(ColorChoice::Never)); - - Ok(()) - }); - } + fn test_walks_up_directory_tree() { + let dir = TempDir::new().unwrap(); + let pyproject = dir.path().join("pyproject.toml"); - #[test] - fn test_pyproject_overrides_env() { - figment::Jail::expect_with(|jail| { - // Set env var - jail.set_env("PREK_COLOR", "never"); - - // Create pyproject.toml with different value - jail.create_file( - "pyproject.toml", - r#" + let mut file = std::fs::File::create(&pyproject).unwrap(); + write!( + file, + r#" [tool.prek] -color = "always" -"#, - )?; - - let settings = Settings::discover(jail.directory())?; +skip = ["parent-hook"] +"# + ) + .unwrap(); - // pyproject.toml should win - assert_eq!(settings.color, Some(ColorChoice::Always)); + std::fs::create_dir_all(dir.path().join("subdir/nested")).unwrap(); - Ok(()) - }); + let settings = Settings::load(&dir.path().join("subdir/nested"), CliOverrides::default()); + assert_eq!(settings.skip, vec!["parent-hook"]); } #[test] - fn test_cli_overrides_all() { - figment::Jail::expect_with(|jail| { - jail.set_env("PREK_COLOR", "never"); - jail.create_file( - "pyproject.toml", - r#" -[tool.prek] -color = "auto" -"#, - )?; - - let figment = Settings::build_figment(jail.directory()).merge(Serialized::defaults( - CliOverrides { - color: Some(ColorChoice::Always), - ..Default::default() - }, - )); - - let settings: Settings = figment.extract()?; - - // CLI should win - assert_eq!(settings.color, Some(ColorChoice::Always)); - - Ok(()) - }); - } + fn test_cli_overrides_pyproject() { + let dir = TempDir::new().unwrap(); + let pyproject = dir.path().join("pyproject.toml"); - #[test] - fn test_walks_up_directory_tree() { - figment::Jail::expect_with(|jail| { - // Create pyproject.toml in parent - jail.create_file( - "pyproject.toml", - r#" + let mut file = std::fs::File::create(&pyproject).unwrap(); + write!( + file, + r#" [tool.prek] -skip = ["parent-hook"] -"#, - )?; - - // Create subdirectory - std::fs::create_dir_all(jail.directory().join("subdir/nested")) - .map_err(|e| e.to_string())?; // Convert io::Error to String, which impls Into - - let settings = Settings::discover(&jail.directory().join("subdir/nested"))?; - - assert_eq!(settings.skip, vec!["parent-hook"]); +color = "auto" +"# + ) + .unwrap(); - Ok(()) - }); + let settings = Settings::load( + dir.path(), + CliOverrides::new().color(Some(ColorChoice::Always)), + ); + assert_eq!(settings.color, Some(ColorChoice::Always)); } }