Skip to content

Commit df07b39

Browse files
authored
fix(config): non-mergeable list from cli should take priority (#16220)
### What does this PR try to resolve? Before, config value from environment variable won over those from `--config` CLI for non-mergeable list This was an oversight when implementing non-mergeable list. Fixes #16208 ### How to test and review this PR? Test passes
2 parents 92a8401 + aa9d3e4 commit df07b39

File tree

3 files changed

+78
-19
lines changed

3 files changed

+78
-19
lines changed

src/cargo/util/context/mod.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ pub use schema::*;
132132

133133
use super::auth::RegistryConfig;
134134

135+
/// List of which configuration lists cannot be merged.
136+
///
137+
/// Instead of merging,
138+
/// the higher priority list should replaces the lower priority list.
139+
///
140+
/// ## What kind of config is non-mergeable
141+
///
142+
/// The rule of thumb is that if a config is a path of a program,
143+
/// it should be added to this list.
144+
const NON_MERGEABLE_LISTS: &[&str] = &[
145+
"credential-alias.*",
146+
"doc.browser",
147+
"host.runner",
148+
"registries.*.credential-provider",
149+
"registry.credential-provider",
150+
"target.*.runner",
151+
];
152+
135153
/// Helper macro for creating typed access methods.
136154
macro_rules! get_value_typed {
137155
($name:ident, $ty:ty, $variant:ident, $expected:expr) => {
@@ -1007,15 +1025,33 @@ impl GlobalContext {
10071025
return Ok(());
10081026
};
10091027

1010-
if is_nonmergable_list(&key) {
1011-
output.clear();
1028+
let env_def = Definition::Environment(key.as_env_key().to_string());
1029+
1030+
if is_nonmergeable_list(&key) {
1031+
assert!(
1032+
output
1033+
.windows(2)
1034+
.all(|cvs| cvs[0].definition() == cvs[1].definition()),
1035+
"non-mergeable list must have only one definition: {output:?}",
1036+
);
1037+
1038+
// Keep existing config if higher priority than env (e.g., --config CLI),
1039+
// otherwise clear for env
1040+
if output
1041+
.first()
1042+
.map(|o| o.definition() > &env_def)
1043+
.unwrap_or_default()
1044+
{
1045+
return Ok(());
1046+
} else {
1047+
output.clear();
1048+
}
10121049
}
10131050

1014-
let def = Definition::Environment(key.as_env_key().to_string());
10151051
if self.cli_unstable().advanced_env && env_val.starts_with('[') && env_val.ends_with(']') {
10161052
// Parse an environment string as a TOML array.
10171053
let toml_v = env_val.parse::<toml::Value>().map_err(|e| {
1018-
ConfigError::new(format!("could not parse TOML list: {}", e), def.clone())
1054+
ConfigError::new(format!("could not parse TOML list: {}", e), env_def.clone())
10191055
})?;
10201056
let values = toml_v.as_array().expect("env var was not array");
10211057
for value in values {
@@ -1024,16 +1060,16 @@ impl GlobalContext {
10241060
let s = value.as_str().ok_or_else(|| {
10251061
ConfigError::new(
10261062
format!("expected string, found {}", value.type_str()),
1027-
def.clone(),
1063+
env_def.clone(),
10281064
)
10291065
})?;
1030-
output.push(CV::String(s.to_string(), def.clone()))
1066+
output.push(CV::String(s.to_string(), env_def.clone()))
10311067
}
10321068
} else {
10331069
output.extend(
10341070
env_val
10351071
.split_whitespace()
1036-
.map(|s| CV::String(s.to_string(), def.clone())),
1072+
.map(|s| CV::String(s.to_string(), env_def.clone())),
10371073
);
10381074
}
10391075
output.sort_by(|a, b| a.definition().cmp(b.definition()));
@@ -2194,7 +2230,7 @@ impl ConfigValue {
21942230
let is_higher_priority = from.definition().is_higher_priority(self.definition());
21952231
match (self, from) {
21962232
(&mut CV::List(ref mut old, _), CV::List(ref mut new, _)) => {
2197-
if is_nonmergable_list(&parts) {
2233+
if is_nonmergeable_list(&parts) {
21982234
// Use whichever list is higher priority.
21992235
if force || is_higher_priority {
22002236
mem::swap(new, old);
@@ -2342,15 +2378,8 @@ impl ConfigValue {
23422378
}
23432379
}
23442380

2345-
/// List of which configuration lists cannot be merged.
2346-
/// Instead of merging, these the higher priority list replaces the lower priority list.
2347-
fn is_nonmergable_list(key: &ConfigKey) -> bool {
2348-
key.matches("registry.credential-provider")
2349-
|| key.matches("registries.*.credential-provider")
2350-
|| key.matches("target.*.runner")
2351-
|| key.matches("host.runner")
2352-
|| key.matches("credential-alias.*")
2353-
|| key.matches("doc.browser")
2381+
fn is_nonmergeable_list(key: &ConfigKey) -> bool {
2382+
NON_MERGEABLE_LISTS.iter().any(|l| key.matches(l))
23542383
}
23552384

23562385
pub fn homedir(cwd: &Path) -> Option<PathBuf> {

src/cargo/util/context/path.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl ConfigRelativePath {
9898
/// to get the actual program.
9999
///
100100
/// **Note**: Any usage of this type in config needs to be listed in
101-
/// the `util::context::is_nonmergable_list` check to prevent list merging
101+
/// the [`super::NON_MERGEABLE_LISTS`] check to prevent list merging
102102
/// from multiple config files.
103103
#[derive(Debug, Clone, PartialEq)]
104104
pub struct PathAndArgs {

tests/testsuite/config.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2181,7 +2181,7 @@ gitoxide = \"fetch\"
21812181
}
21822182

21832183
#[cargo_test]
2184-
fn nonmergable_lists() {
2184+
fn nonmergeable_lists() {
21852185
let root_path = paths::root().join(".cargo/config.toml");
21862186
write_config_at(
21872187
&root_path,
@@ -2224,6 +2224,36 @@ credential-provider = ['c', 'd']
22242224
// expect: no merge happens; config CLI takes precedence
22252225
assert_eq!(provider.path.raw_value(), "cli");
22262226
assert_eq!(provider.args, ["cli-arg"]);
2227+
2228+
let env = "CARGO_REGISTRIES_EXAMPLE_CREDENTIAL_PROVIDER";
2229+
let gctx = GlobalContextBuilder::new()
2230+
.env(env, "env env-arg")
2231+
.cwd("foo")
2232+
.build();
2233+
let provider = gctx
2234+
.get::<Option<RegistryConfig>>(&format!("registries.example"))
2235+
.unwrap()
2236+
.unwrap()
2237+
.credential_provider
2238+
.unwrap();
2239+
// expect: no merge happens; env takes precedence over files
2240+
assert_eq!(provider.path.raw_value(), "env");
2241+
assert_eq!(provider.args, ["env-arg"]);
2242+
2243+
let gctx = GlobalContextBuilder::new()
2244+
.env(env, "env env-arg")
2245+
.config_arg(cli_arg)
2246+
.cwd("foo")
2247+
.build();
2248+
let provider = gctx
2249+
.get::<Option<RegistryConfig>>(&format!("registries.example"))
2250+
.unwrap()
2251+
.unwrap()
2252+
.credential_provider
2253+
.unwrap();
2254+
// expect: no merge happens; cli takes precedence over files and env
2255+
assert_eq!(provider.path.raw_value(), "cli");
2256+
assert_eq!(provider.args, ["cli-arg"]);
22272257
}
22282258

22292259
#[cargo_test]

0 commit comments

Comments
 (0)