diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 56a65ecef..f613bdf90 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -764,7 +764,9 @@ unstable_cli_options!( dual_proc_macros: bool = ("Build proc-macros for both the host and the target"), features: Option>, gc: bool = ("Track cache usage and \"garbage collect\" unused files"), + #[serde(deserialize_with = "deserialize_git_features")] git: Option = ("Enable support for shallow git fetch operations"), + #[serde(deserialize_with = "deserialize_gitoxide_features")] gitoxide: Option = ("Use gitoxide for the given git interactions, or all of them if no argument is given"), host_config: bool = ("Enable the `[host]` section in the .cargo/config.toml file"), minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum"), @@ -874,7 +876,8 @@ where )) } -#[derive(Debug, Copy, Clone, Default, Deserialize)] +#[derive(Debug, Copy, Clone, Default, Deserialize, Ord, PartialOrd, Eq, PartialEq)] +#[serde(default)] pub struct GitFeatures { /// When cloning the index, perform a shallow clone. Maintain shallowness upon subsequent fetches. pub shallow_index: bool, @@ -883,12 +886,71 @@ pub struct GitFeatures { } impl GitFeatures { - fn all() -> Self { + pub fn all() -> Self { GitFeatures { shallow_index: true, shallow_deps: true, } } + + fn expecting() -> String { + let fields = vec!["`shallow-index`", "`shallow-deps`"]; + format!( + "unstable 'git' only takes {} as valid inputs", + fields.join(" and ") + ) + } +} + +fn deserialize_git_features<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + struct GitFeaturesVisitor; + + impl<'de> serde::de::Visitor<'de> for GitFeaturesVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&GitFeatures::expecting()) + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + if v { + Ok(Some(GitFeatures::all())) + } else { + Ok(None) + } + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + Ok(parse_git(s.split(",")).map_err(serde::de::Error::custom)?) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let git = GitFeatures::deserialize(deserializer)?; + Ok(Some(git)) + } + + fn visit_map(self, map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mvd = serde::de::value::MapAccessDeserializer::new(map); + Ok(Some(GitFeatures::deserialize(mvd)?)) + } + } + + deserializer.deserialize_any(GitFeaturesVisitor) } fn parse_git(it: impl Iterator>) -> CargoResult> { @@ -903,16 +965,15 @@ fn parse_git(it: impl Iterator>) -> CargoResult *shallow_index = true, "shallow-deps" => *shallow_deps = true, _ => { - bail!( - "unstable 'git' only takes 'shallow-index' and 'shallow-deps' as valid inputs" - ) + bail!(GitFeatures::expecting()) } } } Ok(Some(out)) } -#[derive(Debug, Copy, Clone, Default, Deserialize)] +#[derive(Debug, Copy, Clone, Default, Deserialize, Ord, PartialOrd, Eq, PartialEq)] +#[serde(default)] pub struct GitoxideFeatures { /// All fetches are done with `gitoxide`, which includes git dependencies as well as the crates index. pub fetch: bool, @@ -926,7 +987,7 @@ pub struct GitoxideFeatures { } impl GitoxideFeatures { - fn all() -> Self { + pub fn all() -> Self { GitoxideFeatures { fetch: true, checkout: true, @@ -943,6 +1004,67 @@ impl GitoxideFeatures { internal_use_git2: false, } } + + fn expecting() -> String { + let fields = vec!["`fetch`", "`checkout`", "`internal-use-git2`"]; + format!( + "unstable 'gitoxide' only takes {} as valid inputs, for shallow fetches see `-Zgit=shallow-index,shallow-deps`", + fields.join(" and ") + ) + } +} + +fn deserialize_gitoxide_features<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + struct GitoxideFeaturesVisitor; + + impl<'de> serde::de::Visitor<'de> for GitoxideFeaturesVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&GitoxideFeatures::expecting()) + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + Ok(parse_gitoxide(s.split(",")).map_err(serde::de::Error::custom)?) + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + if v { + Ok(Some(GitoxideFeatures::all())) + } else { + Ok(None) + } + } + + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let gitoxide = GitoxideFeatures::deserialize(deserializer)?; + Ok(Some(gitoxide)) + } + + fn visit_map(self, map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mvd = serde::de::value::MapAccessDeserializer::new(map); + Ok(Some(GitoxideFeatures::deserialize(mvd)?)) + } + } + + deserializer.deserialize_any(GitoxideFeaturesVisitor) } fn parse_gitoxide( @@ -961,7 +1083,7 @@ fn parse_gitoxide( "checkout" => *checkout = true, "internal-use-git2" => *internal_use_git2 = true, _ => { - bail!("unstable 'gitoxide' only takes `fetch` and 'checkout' as valid input, for shallow fetches see `-Zgit=shallow-index,shallow-deps`") + bail!(GitoxideFeatures::expecting()) } } } diff --git a/src/cargo/util/context/de.rs b/src/cargo/util/context/de.rs index cbb5cf042..d35325a50 100644 --- a/src/cargo/util/context/de.rs +++ b/src/cargo/util/context/de.rs @@ -62,6 +62,12 @@ impl<'de, 'gctx> de::Deserializer<'de> for Deserializer<'gctx> { let (res, def) = res; return res.map_err(|e| e.with_key_context(&self.key, Some(def))); } + + // The effect here is the same as in `deserialize_option`. + if self.gctx.has_key(&self.key, self.env_prefix_ok)? { + return visitor.visit_some(self); + } + Err(ConfigError::missing(&self.key)) } diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 63bd6358a..7d9bf74d6 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -1,5 +1,6 @@ //! Tests for config settings. +use cargo::core::features::{GitFeatures, GitoxideFeatures}; use cargo::core::{PackageIdSpec, Shell}; use cargo::util::context::{ self, Definition, GlobalContext, JobsConfig, SslVersionConfig, StringList, @@ -1930,3 +1931,211 @@ Caused by: missing field `bax`", ); } + +#[cargo_test] +fn git_features() { + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT", "shallow-index") + .build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: true, + ..GitFeatures::default() + }), + )); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT", "shallow-index,abc") + .build(); + assert_error( + gctx.get::>("unstable") + .unwrap_err(), + "\ +error in environment variable `CARGO_UNSTABLE_GIT`: could not load config key `unstable.git` + +Caused by: +[..]unstable 'git' only takes [..] as valid inputs", + ); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT", "shallow-deps") + .build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: false, + shallow_deps: true, + }), + )); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT", "true") + .build(); + assert!(do_check(gctx, Some(GitFeatures::all()))); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT_SHALLOW_INDEX", "true") + .build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: true, + ..Default::default() + }), + )); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GIT_SHALLOW_INDEX", "true") + .env("CARGO_UNSTABLE_GIT_SHALLOW_DEPS", "true") + .build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: true, + shallow_deps: true, + ..Default::default() + }), + )); + + write_config_toml( + "\ +[unstable] +git = 'shallow-index' +", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: true, + shallow_deps: false, + }), + )); + + write_config_toml( + "\ + [unstable.git] + shallow_deps = false + shallow_index = true + ", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check( + gctx, + Some(GitFeatures { + shallow_index: true, + shallow_deps: false, + ..Default::default() + }), + )); + + write_config_toml( + "\ + [unstable.git] + ", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check(gctx, Some(Default::default()))); + + fn do_check(gctx: GlobalContext, expect: Option) -> bool { + let unstable_flags = gctx + .get::>("unstable") + .unwrap() + .unwrap(); + unstable_flags.git == expect + } +} + +#[cargo_test] +fn gitoxide_features() { + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GITOXIDE", "fetch") + .build(); + assert!(do_check( + gctx, + Some(GitoxideFeatures { + fetch: true, + ..GitoxideFeatures::default() + }), + )); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GITOXIDE", "fetch,abc") + .build(); + + assert_error( + gctx.get::>("unstable") + .unwrap_err(), + "\ +error in environment variable `CARGO_UNSTABLE_GITOXIDE`: could not load config key `unstable.gitoxide` + +Caused by: +[..]unstable 'gitoxide' only takes [..] as valid inputs, for shallow fetches see `-Zgit=shallow-index,shallow-deps`", +); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GITOXIDE", "true") + .build(); + assert!(do_check(gctx, Some(GitoxideFeatures::all()))); + + let gctx = GlobalContextBuilder::new() + .env("CARGO_UNSTABLE_GITOXIDE_FETCH", "true") + .build(); + assert!(do_check( + gctx, + Some(GitoxideFeatures { + fetch: true, + ..Default::default() + }), + )); + + write_config_toml( + "\ +[unstable] +gitoxide = \"fetch\" +", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check( + gctx, + Some(GitoxideFeatures { + fetch: true, + ..GitoxideFeatures::default() + }), + )); + + write_config_toml( + "\ + [unstable.gitoxide] + fetch = true + checkout = false + internal_use_git2 = false + ", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check( + gctx, + Some(GitoxideFeatures { + fetch: true, + checkout: false, + internal_use_git2: false, + }), + )); + + write_config_toml( + "\ + [unstable.gitoxide] + ", + ); + let gctx = GlobalContextBuilder::new().build(); + assert!(do_check(gctx, Some(Default::default()))); + + fn do_check(gctx: GlobalContext, expect: Option) -> bool { + let unstable_flags = gctx + .get::>("unstable") + .unwrap() + .unwrap(); + unstable_flags.gitoxide == expect + } +}