Auto merge of #13687 - linyihai:git-features-env, r=weihanglo

Allows the default git/gitoxide configuration to be obtained from the ENV and config

### What does this PR try to resolve?
The default git/gitoxide feautures config can be obtained througt `-Zgit` and `-Zgitoxide`.
However, it cannot be obtained from environment variables or configurations.
It's not very ergonomic.

### How should we test and review this PR?
The previous commit explained the staus quo, the next commit addressed the problem.

### Additional information
Inspired by https://github.com/rust-lang/cargo/issues/11813#issuecomment-1817517629
See also https://github.com/rust-lang/cargo/issues/13285#issuecomment-2016875459

### Change of usage

1. Mirror Zgit/Zgitoxide when they parsed as string

Specify the feature you like
```
CARGO_UNSABLE_GIT='shallow-deps' cargo fetch
cargo fetch --config "unstable.git='shallow-deps'"
cargo fetch -Zgit="shallow-deps"
```

2. Specify partial fields when parsed as table

```
CARGO_UNSTABLE_GITOXIDE_FETCH=true cargo fetch

```

The rest fields will use Rust default value. That said, checkout and internal_use_git2 will be false.

Besides, you can pass true to the whole feature to specify the pre-defined features.

```
CARGO_UNSTABLE_GITOXIDE=true cargo fetch

```
This commit is contained in:
bors 2024-06-03 13:27:22 +00:00
commit 4219b66f63
3 changed files with 345 additions and 8 deletions

View File

@ -764,7 +764,9 @@ unstable_cli_options!(
dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
features: Option<Vec<String>>,
gc: bool = ("Track cache usage and \"garbage collect\" unused files"),
#[serde(deserialize_with = "deserialize_git_features")]
git: Option<GitFeatures> = ("Enable support for shallow git fetch operations"),
#[serde(deserialize_with = "deserialize_gitoxide_features")]
gitoxide: Option<GitoxideFeatures> = ("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<Option<GitFeatures>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct GitFeaturesVisitor;
impl<'de> serde::de::Visitor<'de> for GitFeaturesVisitor {
type Value = Option<GitFeatures>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&GitFeatures::expecting())
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Ok(Some(GitFeatures::all()))
} else {
Ok(None)
}
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(parse_git(s.split(",")).map_err(serde::de::Error::custom)?)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let git = GitFeatures::deserialize(deserializer)?;
Ok(Some(git))
}
fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
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<Item = impl AsRef<str>>) -> CargoResult<Option<GitFeatures>> {
@ -903,16 +965,15 @@ fn parse_git(it: impl Iterator<Item = impl AsRef<str>>) -> CargoResult<Option<Gi
"shallow-index" => *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<Option<GitoxideFeatures>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct GitoxideFeaturesVisitor;
impl<'de> serde::de::Visitor<'de> for GitoxideFeaturesVisitor {
type Value = Option<GitoxideFeatures>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&GitoxideFeatures::expecting())
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(parse_gitoxide(s.split(",")).map_err(serde::de::Error::custom)?)
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Ok(Some(GitoxideFeatures::all()))
} else {
Ok(None)
}
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let gitoxide = GitoxideFeatures::deserialize(deserializer)?;
Ok(Some(gitoxide))
}
fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
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())
}
}
}

View File

@ -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))
}

View File

@ -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::<Option<cargo::core::CliUnstable>>("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<GitFeatures>) -> bool {
let unstable_flags = gctx
.get::<Option<cargo::core::CliUnstable>>("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::<Option<cargo::core::CliUnstable>>("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<GitoxideFeatures>) -> bool {
let unstable_flags = gctx
.get::<Option<cargo::core::CliUnstable>>("unstable")
.unwrap()
.unwrap();
unstable_flags.gitoxide == expect
}
}