Mercurial > public > mercurial-scm > hg-stable
comparison rust/hg-core/src/config/config_items.rs @ 50802:f8412da86d05
rust-config: add support for default config items
Now that configitems.toml exists, we can read from it the default values for
all core config items.
We will add the devel-warning for use of undeclared config items in a later
patch when we're done adding the missing entries for `rhg`.
author | Rapha?l Gom?s <rgomes@octobus.net> |
---|---|
date | Thu, 06 Jul 2023 14:32:07 +0200 |
parents | |
children | 7f8f6fe13fa9 |
comparison
equal
deleted
inserted
replaced
50801:c51b178b0b7e | 50802:f8412da86d05 |
---|---|
1 //! Code for parsing default Mercurial config items. | |
2 use itertools::Itertools; | |
3 use serde::Deserialize; | |
4 | |
5 use crate::{errors::HgError, exit_codes, FastHashMap}; | |
6 | |
7 /// Corresponds to the structure of `mercurial/configitems.toml`. | |
8 #[derive(Debug, Deserialize)] | |
9 pub struct ConfigItems { | |
10 items: Vec<DefaultConfigItem>, | |
11 templates: FastHashMap<String, Vec<TemplateItem>>, | |
12 #[serde(rename = "template-applications")] | |
13 template_applications: Vec<TemplateApplication>, | |
14 } | |
15 | |
16 /// Corresponds to a config item declaration in `mercurial/configitems.toml`. | |
17 #[derive(Clone, Debug, PartialEq, Deserialize)] | |
18 #[serde(try_from = "RawDefaultConfigItem")] | |
19 pub struct DefaultConfigItem { | |
20 /// Section of the config the item is in (e.g. `[merge-tools]`) | |
21 section: String, | |
22 /// Name of the item (e.g. `meld.gui`) | |
23 name: String, | |
24 /// Default value (can be dynamic, see [`DefaultConfigItemType`]) | |
25 default: Option<DefaultConfigItemType>, | |
26 /// If the config option is generic (e.g. `merge-tools.*`), defines | |
27 /// the priority of this item relative to other generic items. | |
28 /// If we're looking for <pattern>, then all generic items within the same | |
29 /// section will be sorted by order of priority, and the first regex match | |
30 /// against `name` is returned. | |
31 #[serde(default)] | |
32 priority: Option<isize>, | |
33 /// Aliases, if any. Each alias is a tuple of `(section, name)` for each | |
34 /// option that is aliased to this one. | |
35 #[serde(default)] | |
36 alias: Vec<(String, String)>, | |
37 /// Whether the config item is marked as experimental | |
38 #[serde(default)] | |
39 experimental: bool, | |
40 /// The (possibly empty) docstring for the item | |
41 #[serde(default)] | |
42 documentation: String, | |
43 } | |
44 | |
45 /// Corresponds to the raw (i.e. on disk) structure of config items. Used as | |
46 /// an intermediate step in deserialization. | |
47 #[derive(Clone, Debug, Deserialize)] | |
48 struct RawDefaultConfigItem { | |
49 section: String, | |
50 name: String, | |
51 default: Option<toml::Value>, | |
52 #[serde(rename = "default-type")] | |
53 default_type: Option<String>, | |
54 #[serde(default)] | |
55 priority: isize, | |
56 #[serde(default)] | |
57 generic: bool, | |
58 #[serde(default)] | |
59 alias: Vec<(String, String)>, | |
60 #[serde(default)] | |
61 experimental: bool, | |
62 #[serde(default)] | |
63 documentation: String, | |
64 } | |
65 | |
66 impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem { | |
67 type Error = HgError; | |
68 | |
69 fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> { | |
70 Ok(Self { | |
71 section: value.section, | |
72 name: value.name, | |
73 default: raw_default_to_concrete( | |
74 value.default_type, | |
75 value.default, | |
76 )?, | |
77 priority: if value.generic { | |
78 Some(value.priority) | |
79 } else { | |
80 None | |
81 }, | |
82 alias: value.alias, | |
83 experimental: value.experimental, | |
84 documentation: value.documentation, | |
85 }) | |
86 } | |
87 } | |
88 | |
89 impl DefaultConfigItem { | |
90 fn is_generic(&self) -> bool { | |
91 self.priority.is_some() | |
92 } | |
93 } | |
94 | |
95 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> { | |
96 type Error = HgError; | |
97 | |
98 fn try_from( | |
99 value: &'a DefaultConfigItem, | |
100 ) -> Result<Option<&'a str>, Self::Error> { | |
101 match &value.default { | |
102 Some(default) => { | |
103 let err = HgError::abort( | |
104 format!( | |
105 "programming error: wrong query on config item '{}.{}'", | |
106 value.section, | |
107 value.name | |
108 ), | |
109 exit_codes::ABORT, | |
110 Some(format!( | |
111 "asked for '&str', type of default is '{}'", | |
112 default.type_str() | |
113 )), | |
114 ); | |
115 match default { | |
116 DefaultConfigItemType::Primitive(toml::Value::String( | |
117 s, | |
118 )) => Ok(Some(s)), | |
119 _ => Err(err), | |
120 } | |
121 } | |
122 None => Ok(None), | |
123 } | |
124 } | |
125 } | |
126 | |
127 impl TryFrom<&DefaultConfigItem> for Option<bool> { | |
128 type Error = HgError; | |
129 | |
130 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | |
131 match &value.default { | |
132 Some(default) => { | |
133 let err = HgError::abort( | |
134 format!( | |
135 "programming error: wrong query on config item '{}.{}'", | |
136 value.section, | |
137 value.name | |
138 ), | |
139 exit_codes::ABORT, | |
140 Some(format!( | |
141 "asked for 'bool', type of default is '{}'", | |
142 default.type_str() | |
143 )), | |
144 ); | |
145 match default { | |
146 DefaultConfigItemType::Primitive( | |
147 toml::Value::Boolean(b), | |
148 ) => Ok(Some(*b)), | |
149 _ => Err(err), | |
150 } | |
151 } | |
152 None => Ok(Some(false)), | |
153 } | |
154 } | |
155 } | |
156 | |
157 impl TryFrom<&DefaultConfigItem> for Option<u32> { | |
158 type Error = HgError; | |
159 | |
160 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | |
161 match &value.default { | |
162 Some(default) => { | |
163 let err = HgError::abort( | |
164 format!( | |
165 "programming error: wrong query on config item '{}.{}'", | |
166 value.section, | |
167 value.name | |
168 ), | |
169 exit_codes::ABORT, | |
170 Some(format!( | |
171 "asked for 'u32', type of default is '{}'", | |
172 default.type_str() | |
173 )), | |
174 ); | |
175 match default { | |
176 DefaultConfigItemType::Primitive( | |
177 toml::Value::Integer(b), | |
178 ) => { | |
179 Ok(Some((*b).try_into().expect("TOML integer to u32"))) | |
180 } | |
181 _ => Err(err), | |
182 } | |
183 } | |
184 None => Ok(None), | |
185 } | |
186 } | |
187 } | |
188 | |
189 impl TryFrom<&DefaultConfigItem> for Option<u64> { | |
190 type Error = HgError; | |
191 | |
192 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> { | |
193 match &value.default { | |
194 Some(default) => { | |
195 let err = HgError::abort( | |
196 format!( | |
197 "programming error: wrong query on config item '{}.{}'", | |
198 value.section, | |
199 value.name | |
200 ), | |
201 exit_codes::ABORT, | |
202 Some(format!( | |
203 "asked for 'u64', type of default is '{}'", | |
204 default.type_str() | |
205 )), | |
206 ); | |
207 match default { | |
208 DefaultConfigItemType::Primitive( | |
209 toml::Value::Integer(b), | |
210 ) => { | |
211 Ok(Some((*b).try_into().expect("TOML integer to u64"))) | |
212 } | |
213 _ => Err(err), | |
214 } | |
215 } | |
216 None => Ok(None), | |
217 } | |
218 } | |
219 } | |
220 | |
221 /// Allows abstracting over more complex default values than just primitives. | |
222 /// The former `configitems.py` contained some dynamic code that is encoded | |
223 /// in this enum. | |
224 #[derive(Debug, PartialEq, Clone, Deserialize)] | |
225 pub enum DefaultConfigItemType { | |
226 /// Some primitive type (string, integer, boolean) | |
227 Primitive(toml::Value), | |
228 /// A dynamic value that will be given by the code at runtime | |
229 Dynamic, | |
230 /// An lazily-returned array (possibly only relevant in the Python impl) | |
231 /// Example: `lambda: [b"zstd", b"zlib"]` | |
232 Lambda(Vec<String>), | |
233 /// For now, a special case for `web.encoding` that points to the | |
234 /// `encoding.encoding` module in the Python impl so that local encoding | |
235 /// is correctly resolved at runtime | |
236 LazyModule(String), | |
237 ListType, | |
238 } | |
239 | |
240 impl DefaultConfigItemType { | |
241 pub fn type_str(&self) -> &str { | |
242 match self { | |
243 DefaultConfigItemType::Primitive(primitive) => { | |
244 primitive.type_str() | |
245 } | |
246 DefaultConfigItemType::Dynamic => "dynamic", | |
247 DefaultConfigItemType::Lambda(_) => "lambda", | |
248 DefaultConfigItemType::LazyModule(_) => "lazy_module", | |
249 DefaultConfigItemType::ListType => "list_type", | |
250 } | |
251 } | |
252 } | |
253 | |
254 /// Most of the fields are shared with [`DefaultConfigItem`]. | |
255 #[derive(Debug, Clone, Deserialize)] | |
256 #[serde(try_from = "RawTemplateItem")] | |
257 struct TemplateItem { | |
258 suffix: String, | |
259 default: Option<DefaultConfigItemType>, | |
260 priority: Option<isize>, | |
261 #[serde(default)] | |
262 alias: Vec<(String, String)>, | |
263 #[serde(default)] | |
264 experimental: bool, | |
265 #[serde(default)] | |
266 documentation: String, | |
267 } | |
268 | |
269 /// Corresponds to the raw (i.e. on disk) representation of a template item. | |
270 /// Used as an intermediate step in deserialization. | |
271 #[derive(Clone, Debug, Deserialize)] | |
272 struct RawTemplateItem { | |
273 suffix: String, | |
274 default: Option<toml::Value>, | |
275 #[serde(rename = "default-type")] | |
276 default_type: Option<String>, | |
277 #[serde(default)] | |
278 priority: isize, | |
279 #[serde(default)] | |
280 generic: bool, | |
281 #[serde(default)] | |
282 alias: Vec<(String, String)>, | |
283 #[serde(default)] | |
284 experimental: bool, | |
285 #[serde(default)] | |
286 documentation: String, | |
287 } | |
288 | |
289 impl TemplateItem { | |
290 fn into_default_item( | |
291 self, | |
292 application: TemplateApplication, | |
293 ) -> DefaultConfigItem { | |
294 DefaultConfigItem { | |
295 section: application.section, | |
296 name: application | |
297 .prefix | |
298 .map(|prefix| format!("{}.{}", prefix, self.suffix)) | |
299 .unwrap_or(self.suffix), | |
300 default: self.default, | |
301 priority: self.priority, | |
302 alias: self.alias, | |
303 experimental: self.experimental, | |
304 documentation: self.documentation, | |
305 } | |
306 } | |
307 } | |
308 | |
309 impl TryFrom<RawTemplateItem> for TemplateItem { | |
310 type Error = HgError; | |
311 | |
312 fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> { | |
313 Ok(Self { | |
314 suffix: value.suffix, | |
315 default: raw_default_to_concrete( | |
316 value.default_type, | |
317 value.default, | |
318 )?, | |
319 priority: if value.generic { | |
320 Some(value.priority) | |
321 } else { | |
322 None | |
323 }, | |
324 alias: value.alias, | |
325 experimental: value.experimental, | |
326 documentation: value.documentation, | |
327 }) | |
328 } | |
329 } | |
330 | |
331 /// Transforms the on-disk string-based representation of complex default types | |
332 /// to the concrete [`DefaultconfigItemType`]. | |
333 fn raw_default_to_concrete( | |
334 default_type: Option<String>, | |
335 default: Option<toml::Value>, | |
336 ) -> Result<Option<DefaultConfigItemType>, HgError> { | |
337 Ok(match default_type.as_deref() { | |
338 None => default.as_ref().map(|default| { | |
339 DefaultConfigItemType::Primitive(default.to_owned()) | |
340 }), | |
341 Some("dynamic") => Some(DefaultConfigItemType::Dynamic), | |
342 Some("list_type") => Some(DefaultConfigItemType::ListType), | |
343 Some("lambda") => match &default { | |
344 Some(default) => Some(DefaultConfigItemType::Lambda( | |
345 default.to_owned().try_into().map_err(|e| { | |
346 HgError::abort( | |
347 e.to_string(), | |
348 exit_codes::ABORT, | |
349 Some("Check 'mercurial/configitems.toml'".into()), | |
350 ) | |
351 })?, | |
352 )), | |
353 None => { | |
354 return Err(HgError::abort( | |
355 "lambda defined with no return value".to_string(), | |
356 exit_codes::ABORT, | |
357 Some("Check 'mercurial/configitems.toml'".into()), | |
358 )) | |
359 } | |
360 }, | |
361 Some("lazy_module") => match &default { | |
362 Some(default) => { | |
363 Some(DefaultConfigItemType::LazyModule(match default { | |
364 toml::Value::String(module) => module.to_owned(), | |
365 _ => { | |
366 return Err(HgError::abort( | |
367 "lazy_module module name should be a string" | |
368 .to_string(), | |
369 exit_codes::ABORT, | |
370 Some("Check 'mercurial/configitems.toml'".into()), | |
371 )) | |
372 } | |
373 })) | |
374 } | |
375 None => { | |
376 return Err(HgError::abort( | |
377 "lazy_module should have a default value".to_string(), | |
378 exit_codes::ABORT, | |
379 Some("Check 'mercurial/configitems.toml'".into()), | |
380 )) | |
381 } | |
382 }, | |
383 Some(invalid) => { | |
384 return Err(HgError::abort( | |
385 format!("invalid default_type '{}'", invalid), | |
386 exit_codes::ABORT, | |
387 Some("Check 'mercurial/configitems.toml'".into()), | |
388 )) | |
389 } | |
390 }) | |
391 } | |
392 | |
393 #[derive(Debug, Clone, Deserialize)] | |
394 struct TemplateApplication { | |
395 template: String, | |
396 section: String, | |
397 #[serde(default)] | |
398 prefix: Option<String>, | |
399 } | |
400 | |
401 /// Represents the (dynamic) set of default core Mercurial config items from | |
402 /// `mercurial/configitems.toml`. | |
403 #[derive(Clone, Debug, Default)] | |
404 pub struct DefaultConfig { | |
405 /// Mapping of section -> (mapping of name -> item) | |
406 items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>, | |
407 } | |
408 | |
409 impl DefaultConfig { | |
410 pub fn empty() -> DefaultConfig { | |
411 Self { | |
412 items: Default::default(), | |
413 } | |
414 } | |
415 | |
416 /// Returns `Self`, given the contents of `mercurial/configitems.toml` | |
417 #[logging_timer::time("trace")] | |
418 pub fn from_contents(contents: &str) -> Result<Self, HgError> { | |
419 let mut from_file: ConfigItems = | |
420 toml::from_str(contents).map_err(|e| { | |
421 HgError::abort( | |
422 e.to_string(), | |
423 exit_codes::ABORT, | |
424 Some("Check 'mercurial/configitems.toml'".into()), | |
425 ) | |
426 })?; | |
427 | |
428 let mut flat_items = from_file.items; | |
429 | |
430 for application in from_file.template_applications.drain(..) { | |
431 match from_file.templates.get(&application.template) { | |
432 None => return Err( | |
433 HgError::abort( | |
434 format!( | |
435 "template application refers to undefined template '{}'", | |
436 application.template | |
437 ), | |
438 exit_codes::ABORT, | |
439 Some("Check 'mercurial/configitems.toml'".into()) | |
440 ) | |
441 ), | |
442 Some(template_items) => { | |
443 for template_item in template_items { | |
444 flat_items.push( | |
445 template_item | |
446 .clone() | |
447 .into_default_item(application.clone()), | |
448 ) | |
449 } | |
450 } | |
451 }; | |
452 } | |
453 | |
454 let items = flat_items.into_iter().fold( | |
455 FastHashMap::default(), | |
456 |mut acc, item| { | |
457 acc.entry(item.section.to_owned()) | |
458 .or_insert_with(|| { | |
459 let mut section = FastHashMap::default(); | |
460 section.insert(item.name.to_owned(), item.to_owned()); | |
461 section | |
462 }) | |
463 .insert(item.name.to_owned(), item); | |
464 acc | |
465 }, | |
466 ); | |
467 | |
468 Ok(Self { items }) | |
469 } | |
470 | |
471 /// Return the default config item that matches `section` and `item`. | |
472 pub fn get( | |
473 &self, | |
474 section: &[u8], | |
475 item: &[u8], | |
476 ) -> Option<&DefaultConfigItem> { | |
477 // Core items must be valid UTF-8 | |
478 let section = String::from_utf8_lossy(section); | |
479 let section_map = self.items.get(section.as_ref())?; | |
480 let item_name_lossy = String::from_utf8_lossy(item); | |
481 match section_map.get(item_name_lossy.as_ref()) { | |
482 Some(item) => Some(item), | |
483 None => { | |
484 for generic_item in section_map | |
485 .values() | |
486 .filter(|item| item.is_generic()) | |
487 .sorted_by_key(|item| match item.priority { | |
488 Some(priority) => (priority, &item.name), | |
489 _ => unreachable!(), | |
490 }) | |
491 { | |
492 if regex::bytes::Regex::new(&generic_item.name) | |
493 .expect("invalid regex in configitems") | |
494 .is_match(item) | |
495 { | |
496 return Some(generic_item); | |
497 } | |
498 } | |
499 None | |
500 } | |
501 } | |
502 } | |
503 } | |
504 | |
505 #[cfg(test)] | |
506 mod tests { | |
507 use crate::config::config_items::{ | |
508 DefaultConfigItem, DefaultConfigItemType, | |
509 }; | |
510 | |
511 use super::DefaultConfig; | |
512 | |
513 #[test] | |
514 fn test_config_read() { | |
515 let contents = r#" | |
516 [[items]] | |
517 section = "alias" | |
518 name = "abcd.*" | |
519 default = 3 | |
520 generic = true | |
521 priority = -1 | |
522 | |
523 [[items]] | |
524 section = "alias" | |
525 name = ".*" | |
526 default-type = "dynamic" | |
527 generic = true | |
528 | |
529 [[items]] | |
530 section = "cmdserver" | |
531 name = "track-log" | |
532 default-type = "lambda" | |
533 default = [ "chgserver", "cmdserver", "repocache",] | |
534 | |
535 [[items]] | |
536 section = "chgserver" | |
537 name = "idletimeout" | |
538 default = 3600 | |
539 | |
540 [[items]] | |
541 section = "cmdserver" | |
542 name = "message-encodings" | |
543 default-type = "list_type" | |
544 | |
545 [[items]] | |
546 section = "web" | |
547 name = "encoding" | |
548 default-type = "lazy_module" | |
549 default = "encoding.encoding" | |
550 | |
551 [[items]] | |
552 section = "command-templates" | |
553 name = "graphnode" | |
554 alias = [["ui", "graphnodetemplate"]] | |
555 documentation = """This is a docstring. | |
556 This is another line \ | |
557 but this is not.""" | |
558 | |
559 [[items]] | |
560 section = "censor" | |
561 name = "policy" | |
562 default = "abort" | |
563 experimental = true | |
564 | |
565 [[template-applications]] | |
566 template = "diff-options" | |
567 section = "commands" | |
568 prefix = "revert.interactive" | |
569 | |
570 [[template-applications]] | |
571 template = "diff-options" | |
572 section = "diff" | |
573 | |
574 [templates] | |
575 [[templates.diff-options]] | |
576 suffix = "nodates" | |
577 default = false | |
578 | |
579 [[templates.diff-options]] | |
580 suffix = "showfunc" | |
581 default = false | |
582 | |
583 [[templates.diff-options]] | |
584 suffix = "unified" | |
585 "#; | |
586 let res = DefaultConfig::from_contents(contents); | |
587 let config = match res { | |
588 Ok(config) => config, | |
589 Err(e) => panic!("{}", e), | |
590 }; | |
591 let expected = DefaultConfigItem { | |
592 section: "censor".into(), | |
593 name: "policy".into(), | |
594 default: Some(DefaultConfigItemType::Primitive("abort".into())), | |
595 priority: None, | |
596 alias: vec![], | |
597 experimental: true, | |
598 documentation: "".into(), | |
599 }; | |
600 assert_eq!(config.get(b"censor", b"policy"), Some(&expected)); | |
601 | |
602 // Test generic priority. The `.*` pattern is wider than `abcd.*`, but | |
603 // `abcd.*` has priority, so it should match first. | |
604 let expected = DefaultConfigItem { | |
605 section: "alias".into(), | |
606 name: "abcd.*".into(), | |
607 default: Some(DefaultConfigItemType::Primitive(3.into())), | |
608 priority: Some(-1), | |
609 alias: vec![], | |
610 experimental: false, | |
611 documentation: "".into(), | |
612 }; | |
613 assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected)); | |
614 | |
615 //... but if it doesn't, we should fallback to `.*` | |
616 let expected = DefaultConfigItem { | |
617 section: "alias".into(), | |
618 name: ".*".into(), | |
619 default: Some(DefaultConfigItemType::Dynamic), | |
620 priority: Some(0), | |
621 alias: vec![], | |
622 experimental: false, | |
623 documentation: "".into(), | |
624 }; | |
625 assert_eq!(config.get(b"alias", b"something"), Some(&expected)); | |
626 | |
627 let expected = DefaultConfigItem { | |
628 section: "chgserver".into(), | |
629 name: "idletimeout".into(), | |
630 default: Some(DefaultConfigItemType::Primitive(3600.into())), | |
631 priority: None, | |
632 alias: vec![], | |
633 experimental: false, | |
634 documentation: "".into(), | |
635 }; | |
636 assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected)); | |
637 | |
638 let expected = DefaultConfigItem { | |
639 section: "cmdserver".into(), | |
640 name: "track-log".into(), | |
641 default: Some(DefaultConfigItemType::Lambda(vec![ | |
642 "chgserver".into(), | |
643 "cmdserver".into(), | |
644 "repocache".into(), | |
645 ])), | |
646 priority: None, | |
647 alias: vec![], | |
648 experimental: false, | |
649 documentation: "".into(), | |
650 }; | |
651 assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected)); | |
652 | |
653 let expected = DefaultConfigItem { | |
654 section: "command-templates".into(), | |
655 name: "graphnode".into(), | |
656 default: None, | |
657 priority: None, | |
658 alias: vec![("ui".into(), "graphnodetemplate".into())], | |
659 experimental: false, | |
660 documentation: | |
661 "This is a docstring.\nThis is another line but this is not." | |
662 .into(), | |
663 }; | |
664 assert_eq!( | |
665 config.get(b"command-templates", b"graphnode"), | |
666 Some(&expected) | |
667 ); | |
668 } | |
669 } |