Skip to content

Commit 4af39e2

Browse files
committed
cli: config list: mark values overridden by table or parent value
This fixes the output of "jj config list --include-overridden --include-defaults ui.pager" and ".. ui.pager.command". The default ui.pager.* should be marked as overridden by ui.pager if set in user configuration.
1 parent a001fe2 commit 4af39e2

File tree

2 files changed

+142
-25
lines changed

2 files changed

+142
-25
lines changed

cli/src/config.rs

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
// limitations under the License.
1414

1515
use std::borrow::Cow;
16+
use std::collections::BTreeSet;
1617
use std::collections::HashMap;
17-
use std::collections::HashSet;
1818
use std::env;
1919
use std::fmt;
2020
use std::path::Path;
@@ -103,28 +103,40 @@ pub fn resolved_config_values(
103103
stacked_config: &StackedConfig,
104104
filter_prefix: &ConfigNamePathBuf,
105105
) -> Vec<AnnotatedValue> {
106-
// Collect annotated values from each config.
106+
// Collect annotated values in reverse order and mark each value shadowed by
107+
// value or table in upper layers.
107108
let mut config_vals = vec![];
108-
for layer in stacked_config.layers() {
109-
// TODO: Err(item) means all descendant paths are overridden by the
110-
// current layer. For example, the default ui.pager.<field> should be
111-
// marked as overridden if user had ui.pager = [...] set.
112-
let Ok(Some(top_item)) = layer.look_up_item(filter_prefix) else {
113-
continue;
109+
let mut upper_value_names = BTreeSet::new();
110+
for layer in stacked_config.layers().iter().rev() {
111+
let top_item = match layer.look_up_item(filter_prefix) {
112+
Ok(Some(item)) => item,
113+
Ok(None) => continue, // parent is a table, but no value found
114+
Err(_) => {
115+
// parent is not a table, shadows lower layers
116+
upper_value_names.insert(filter_prefix.clone());
117+
continue;
118+
}
114119
};
115-
let mut config_stack = vec![(filter_prefix.clone(), top_item)];
116-
while let Some((name, item)) = config_stack.pop() {
120+
let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
121+
while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
117122
if let Some(table) = item.as_table() {
118-
// table.iter() does not implement DoubleEndedIterator as of
119-
// toml_edit 0.22.22.
120-
let frame = config_stack.len();
123+
// current table and children may be shadowed by value in upper layer
124+
let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
121125
for (k, v) in table {
122126
let mut sub_name = name.clone();
123127
sub_name.push(k);
124-
config_stack.push((sub_name, v));
128+
config_stack.push((sub_name, v, is_overridden)); // in reverse order
125129
}
126-
config_stack[frame..].reverse();
127130
} else {
131+
// current value may be shadowed by value or table in upper layer
132+
let maybe_child = upper_value_names
133+
.range(&name..)
134+
.next()
135+
.filter(|next| next.starts_with(&name));
136+
let is_overridden = is_parent_overridden || maybe_child.is_some();
137+
if maybe_child != Some(&name) {
138+
upper_value_names.insert(name.clone());
139+
}
128140
let value = item
129141
.clone()
130142
.into_value()
@@ -133,20 +145,12 @@ pub fn resolved_config_values(
133145
name,
134146
value,
135147
source: layer.source,
136-
// Note: Value updated below.
137-
is_overridden: false,
148+
is_overridden,
138149
});
139150
}
140151
}
141152
}
142-
143-
// Walk through config values in reverse order and mark each overridden value as
144-
// overridden.
145-
let mut names_found = HashSet::new();
146-
for val in config_vals.iter_mut().rev() {
147-
val.is_overridden = !names_found.insert(&val.name);
148-
}
149-
153+
config_vals.reverse();
150154
config_vals
151155
}
152156

@@ -702,6 +706,8 @@ impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
702706

703707
#[cfg(test)]
704708
mod tests {
709+
use std::fmt::Write as _;
710+
705711
use anyhow::anyhow;
706712
use assert_matches::assert_matches;
707713
use indoc::indoc;
@@ -1040,6 +1046,112 @@ mod tests {
10401046
);
10411047
}
10421048

1049+
#[test]
1050+
fn test_resolved_config_values_overridden() {
1051+
let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1052+
let mut config = StackedConfig::empty();
1053+
config.extend_layers(layers.iter().copied().cloned());
1054+
let prefix = if prefix.is_empty() {
1055+
ConfigNamePathBuf::root()
1056+
} else {
1057+
prefix.parse().unwrap()
1058+
};
1059+
let mut output = String::new();
1060+
for annotated in resolved_config_values(&config, &prefix) {
1061+
let AnnotatedValue { name, value, .. } = &annotated;
1062+
let sigil = if annotated.is_overridden { '!' } else { ' ' };
1063+
writeln!(output, "{sigil}{name} = {value}").unwrap();
1064+
}
1065+
output
1066+
};
1067+
1068+
let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1069+
layer0.set_value("a.b.e", "0.0").unwrap();
1070+
layer0.set_value("a.b.c.f", "0.1").unwrap();
1071+
layer0.set_value("a.b.d", "0.2").unwrap();
1072+
let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1073+
layer1.set_value("a.b", "1.0").unwrap();
1074+
layer1.set_value("a.c", "1.1").unwrap();
1075+
let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1076+
layer2.set_value("a.b.g", "2.0").unwrap();
1077+
layer2.set_value("a.b.d", "2.1").unwrap();
1078+
1079+
// a.b.* is shadowed by a.b
1080+
let layers = [&layer0, &layer1];
1081+
insta::assert_snapshot!(list(&layers, ""), @r#"
1082+
!a.b.e = "0.0"
1083+
!a.b.c.f = "0.1"
1084+
!a.b.d = "0.2"
1085+
a.b = "1.0"
1086+
a.c = "1.1"
1087+
"#);
1088+
insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1089+
!a.b.e = "0.0"
1090+
!a.b.c.f = "0.1"
1091+
!a.b.d = "0.2"
1092+
a.b = "1.0"
1093+
"#);
1094+
insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1095+
insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1096+
1097+
// a.b is shadowed by a.b.*
1098+
let layers = [&layer1, &layer2];
1099+
insta::assert_snapshot!(list(&layers, ""), @r#"
1100+
!a.b = "1.0"
1101+
a.c = "1.1"
1102+
a.b.g = "2.0"
1103+
a.b.d = "2.1"
1104+
"#);
1105+
insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1106+
!a.b = "1.0"
1107+
a.b.g = "2.0"
1108+
a.b.d = "2.1"
1109+
"#);
1110+
1111+
// a.b.d is shadowed by a.b.d
1112+
let layers = [&layer0, &layer2];
1113+
insta::assert_snapshot!(list(&layers, ""), @r#"
1114+
a.b.e = "0.0"
1115+
a.b.c.f = "0.1"
1116+
!a.b.d = "0.2"
1117+
a.b.g = "2.0"
1118+
a.b.d = "2.1"
1119+
"#);
1120+
insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1121+
a.b.e = "0.0"
1122+
a.b.c.f = "0.1"
1123+
!a.b.d = "0.2"
1124+
a.b.g = "2.0"
1125+
a.b.d = "2.1"
1126+
"#);
1127+
insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1128+
insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1129+
!a.b.d = "0.2"
1130+
a.b.d = "2.1"
1131+
"#);
1132+
1133+
// a.b.* is shadowed by a.b, which is shadowed by a.b.*
1134+
let layers = [&layer0, &layer1, &layer2];
1135+
insta::assert_snapshot!(list(&layers, ""), @r#"
1136+
!a.b.e = "0.0"
1137+
!a.b.c.f = "0.1"
1138+
!a.b.d = "0.2"
1139+
!a.b = "1.0"
1140+
a.c = "1.1"
1141+
a.b.g = "2.0"
1142+
a.b.d = "2.1"
1143+
"#);
1144+
insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1145+
!a.b.e = "0.0"
1146+
!a.b.c.f = "0.1"
1147+
!a.b.d = "0.2"
1148+
!a.b = "1.0"
1149+
a.b.g = "2.0"
1150+
a.b.d = "2.1"
1151+
"#);
1152+
insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1153+
}
1154+
10431155
#[test]
10441156
fn test_config_path_home_dir_existing() -> anyhow::Result<()> {
10451157
TestCase {

lib/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ impl ConfigNamePathBuf {
146146
self.0.is_empty()
147147
}
148148

149+
/// Returns true if the `base` is a prefix of this path.
150+
pub fn starts_with(&self, base: impl AsRef<[toml_edit::Key]>) -> bool {
151+
self.0.starts_with(base.as_ref())
152+
}
153+
149154
/// Returns iterator of path components (or keys.)
150155
pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> {
151156
self.0.iter()

0 commit comments

Comments
 (0)