##// END OF EJS Templates
rust-config: add more ways of reading the config...
Raphaël Gomès -
r52744:0dbf6a5c default
parent child Browse files
Show More
@@ -1,725 +1,785
1 1 //! Code for parsing default Mercurial config items.
2 2 use itertools::Itertools;
3 3 use serde::Deserialize;
4 4
5 5 use crate::{errors::HgError, exit_codes, FastHashMap};
6 6
7 7 /// Corresponds to the structure of `mercurial/configitems.toml`.
8 8 #[derive(Debug, Deserialize)]
9 9 pub struct ConfigItems {
10 10 items: Vec<DefaultConfigItem>,
11 11 templates: FastHashMap<String, Vec<TemplateItem>>,
12 12 #[serde(rename = "template-applications")]
13 13 template_applications: Vec<TemplateApplication>,
14 14 }
15 15
16 16 /// Corresponds to a config item declaration in `mercurial/configitems.toml`.
17 17 #[derive(Clone, Debug, PartialEq, Deserialize)]
18 18 #[serde(try_from = "RawDefaultConfigItem")]
19 19 pub struct DefaultConfigItem {
20 20 /// Section of the config the item is in (e.g. `[merge-tools]`)
21 21 section: String,
22 22 /// Name of the item (e.g. `meld.gui`)
23 23 name: String,
24 24 /// Default value (can be dynamic, see [`DefaultConfigItemType`])
25 25 default: Option<DefaultConfigItemType>,
26 26 /// If the config option is generic (e.g. `merge-tools.*`), defines
27 27 /// the priority of this item relative to other generic items.
28 28 /// If we're looking for <pattern>, then all generic items within the same
29 29 /// section will be sorted by order of priority, and the first regex match
30 30 /// against `name` is returned.
31 31 #[serde(default)]
32 32 priority: Option<isize>,
33 33 /// Aliases, if any. Each alias is a tuple of `(section, name)` for each
34 34 /// option that is aliased to this one.
35 35 #[serde(default)]
36 36 alias: Vec<(String, String)>,
37 37 /// Whether the config item is marked as experimental
38 38 #[serde(default)]
39 39 experimental: bool,
40 40 /// The (possibly empty) docstring for the item
41 41 #[serde(default)]
42 42 documentation: String,
43 43 /// Whether the item is part of an in-core extension. This allows us to
44 44 /// hide them if the extension is not enabled, to preserve legacy
45 45 /// behavior.
46 46 #[serde(default)]
47 47 in_core_extension: Option<String>,
48 48 }
49 49
50 50 /// Corresponds to the raw (i.e. on disk) structure of config items. Used as
51 51 /// an intermediate step in deserialization.
52 52 #[derive(Clone, Debug, Deserialize)]
53 53 struct RawDefaultConfigItem {
54 54 section: String,
55 55 name: String,
56 56 default: Option<toml::Value>,
57 57 #[serde(rename = "default-type")]
58 58 default_type: Option<String>,
59 59 #[serde(default)]
60 60 priority: isize,
61 61 #[serde(default)]
62 62 generic: bool,
63 63 #[serde(default)]
64 64 alias: Vec<(String, String)>,
65 65 #[serde(default)]
66 66 experimental: bool,
67 67 #[serde(default)]
68 68 documentation: String,
69 69 #[serde(default)]
70 70 in_core_extension: Option<String>,
71 71 }
72 72
73 73 impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem {
74 74 type Error = HgError;
75 75
76 76 fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> {
77 77 Ok(Self {
78 78 section: value.section,
79 79 name: value.name,
80 80 default: raw_default_to_concrete(
81 81 value.default_type,
82 82 value.default,
83 83 )?,
84 84 priority: if value.generic {
85 85 Some(value.priority)
86 86 } else {
87 87 None
88 88 },
89 89 alias: value.alias,
90 90 experimental: value.experimental,
91 91 documentation: value.documentation,
92 92 in_core_extension: value.in_core_extension,
93 93 })
94 94 }
95 95 }
96 96
97 97 impl DefaultConfigItem {
98 98 fn is_generic(&self) -> bool {
99 99 self.priority.is_some()
100 100 }
101 101
102 102 pub fn in_core_extension(&self) -> Option<&str> {
103 103 self.in_core_extension.as_deref()
104 104 }
105 105
106 106 pub fn section(&self) -> &str {
107 107 self.section.as_ref()
108 108 }
109 109 }
110 110
111 111 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> {
112 112 type Error = HgError;
113 113
114 114 fn try_from(
115 115 value: &'a DefaultConfigItem,
116 116 ) -> Result<Option<&'a str>, Self::Error> {
117 117 match &value.default {
118 118 Some(default) => {
119 119 let err = HgError::abort(
120 120 format!(
121 121 "programming error: wrong query on config item '{}.{}'",
122 122 value.section,
123 123 value.name
124 124 ),
125 125 exit_codes::ABORT,
126 126 Some(format!(
127 127 "asked for '&str', type of default is '{}'",
128 128 default.type_str()
129 129 )),
130 130 );
131 131 match default {
132 132 DefaultConfigItemType::Primitive(toml::Value::String(
133 133 s,
134 134 )) => Ok(Some(s)),
135 135 _ => Err(err),
136 136 }
137 137 }
138 138 None => Ok(None),
139 139 }
140 140 }
141 141 }
142 142
143 143 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a [u8]> {
144 144 type Error = HgError;
145 145
146 146 fn try_from(
147 147 value: &'a DefaultConfigItem,
148 148 ) -> Result<Option<&'a [u8]>, Self::Error> {
149 149 match &value.default {
150 150 Some(default) => {
151 151 let err = HgError::abort(
152 152 format!(
153 153 "programming error: wrong query on config item '{}.{}'",
154 154 value.section,
155 155 value.name
156 156 ),
157 157 exit_codes::ABORT,
158 158 Some(format!(
159 159 "asked for bytes, type of default is '{}', \
160 160 which cannot be interpreted as bytes",
161 161 default.type_str()
162 162 )),
163 163 );
164 164 match default {
165 165 DefaultConfigItemType::Primitive(p) => {
166 166 Ok(p.as_str().map(str::as_bytes))
167 167 }
168 168 _ => Err(err),
169 169 }
170 170 }
171 171 None => Ok(None),
172 172 }
173 173 }
174 174 }
175 175
176 176 impl TryFrom<&DefaultConfigItem> for Option<bool> {
177 177 type Error = HgError;
178 178
179 179 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
180 180 match &value.default {
181 181 Some(default) => {
182 182 let err = HgError::abort(
183 183 format!(
184 184 "programming error: wrong query on config item '{}.{}'",
185 185 value.section,
186 186 value.name
187 187 ),
188 188 exit_codes::ABORT,
189 189 Some(format!(
190 190 "asked for 'bool', type of default is '{}'",
191 191 default.type_str()
192 192 )),
193 193 );
194 194 match default {
195 195 DefaultConfigItemType::Primitive(
196 196 toml::Value::Boolean(b),
197 197 ) => Ok(Some(*b)),
198 198 _ => Err(err),
199 199 }
200 200 }
201 201 None => Ok(None),
202 202 }
203 203 }
204 204 }
205 205
206 206 impl TryFrom<&DefaultConfigItem> for Option<u32> {
207 207 type Error = HgError;
208 208
209 209 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
210 210 match &value.default {
211 211 Some(default) => {
212 212 let err = HgError::abort(
213 213 format!(
214 214 "programming error: wrong query on config item '{}.{}'",
215 215 value.section,
216 216 value.name
217 217 ),
218 218 exit_codes::ABORT,
219 219 Some(format!(
220 220 "asked for 'u32', type of default is '{}'",
221 221 default.type_str()
222 222 )),
223 223 );
224 224 match default {
225 225 DefaultConfigItemType::Primitive(
226 226 toml::Value::Integer(b),
227 227 ) => {
228 228 Ok(Some((*b).try_into().expect("TOML integer to u32")))
229 229 }
230 230 _ => Err(err),
231 231 }
232 232 }
233 233 None => Ok(None),
234 234 }
235 235 }
236 236 }
237 237
238 238 impl TryFrom<&DefaultConfigItem> for Option<u64> {
239 239 type Error = HgError;
240 240
241 241 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
242 242 match &value.default {
243 243 Some(default) => {
244 244 let err = HgError::abort(
245 245 format!(
246 246 "programming error: wrong query on config item '{}.{}'",
247 247 value.section,
248 248 value.name
249 249 ),
250 250 exit_codes::ABORT,
251 251 Some(format!(
252 252 "asked for 'u64', type of default is '{}'",
253 253 default.type_str()
254 254 )),
255 255 );
256 256 match default {
257 257 DefaultConfigItemType::Primitive(
258 258 toml::Value::Integer(b),
259 259 ) => {
260 260 Ok(Some((*b).try_into().expect("TOML integer to u64")))
261 261 }
262 262 _ => Err(err),
263 263 }
264 264 }
265 265 None => Ok(None),
266 266 }
267 267 }
268 268 }
269 269
270 impl TryFrom<&DefaultConfigItem> for Option<i64> {
271 type Error = HgError;
272
273 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
274 match &value.default {
275 Some(default) => {
276 let err = HgError::abort(
277 format!(
278 "programming error: wrong query on config item '{}.{}'",
279 value.section,
280 value.name
281 ),
282 exit_codes::ABORT,
283 Some(format!(
284 "asked for 'i64', type of default is '{}'",
285 default.type_str()
286 )),
287 );
288 match default {
289 DefaultConfigItemType::Primitive(
290 toml::Value::Integer(b),
291 ) => Ok(Some(*b)),
292 _ => Err(err),
293 }
294 }
295 None => Ok(None),
296 }
297 }
298 }
299
300 impl TryFrom<&DefaultConfigItem> for Option<f64> {
301 type Error = HgError;
302
303 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
304 match &value.default {
305 Some(default) => {
306 let err = HgError::abort(
307 format!(
308 "programming error: wrong query on config item '{}.{}'",
309 value.section,
310 value.name
311 ),
312 exit_codes::ABORT,
313 Some(format!(
314 "asked for 'f64', type of default is '{}'",
315 default.type_str()
316 )),
317 );
318 match default {
319 DefaultConfigItemType::Primitive(toml::Value::Float(
320 b,
321 )) => Ok(Some(*b)),
322 _ => Err(err),
323 }
324 }
325 None => Ok(None),
326 }
327 }
328 }
329
270 330 /// Allows abstracting over more complex default values than just primitives.
271 331 /// The former `configitems.py` contained some dynamic code that is encoded
272 332 /// in this enum.
273 333 #[derive(Debug, PartialEq, Clone, Deserialize)]
274 334 pub enum DefaultConfigItemType {
275 335 /// Some primitive type (string, integer, boolean)
276 336 Primitive(toml::Value),
277 337 /// A dynamic value that will be given by the code at runtime
278 338 Dynamic,
279 339 /// An lazily-returned array (possibly only relevant in the Python impl)
280 340 /// Example: `lambda: [b"zstd", b"zlib"]`
281 341 Lambda(Vec<String>),
282 342 /// For now, a special case for `web.encoding` that points to the
283 343 /// `encoding.encoding` module in the Python impl so that local encoding
284 344 /// is correctly resolved at runtime
285 345 LazyModule(String),
286 346 ListType,
287 347 }
288 348
289 349 impl DefaultConfigItemType {
290 350 pub fn type_str(&self) -> &str {
291 351 match self {
292 352 DefaultConfigItemType::Primitive(primitive) => {
293 353 primitive.type_str()
294 354 }
295 355 DefaultConfigItemType::Dynamic => "dynamic",
296 356 DefaultConfigItemType::Lambda(_) => "lambda",
297 357 DefaultConfigItemType::LazyModule(_) => "lazy_module",
298 358 DefaultConfigItemType::ListType => "list_type",
299 359 }
300 360 }
301 361 }
302 362
303 363 /// Most of the fields are shared with [`DefaultConfigItem`].
304 364 #[derive(Debug, Clone, Deserialize)]
305 365 #[serde(try_from = "RawTemplateItem")]
306 366 struct TemplateItem {
307 367 suffix: String,
308 368 default: Option<DefaultConfigItemType>,
309 369 priority: Option<isize>,
310 370 #[serde(default)]
311 371 alias: Vec<(String, String)>,
312 372 #[serde(default)]
313 373 experimental: bool,
314 374 #[serde(default)]
315 375 documentation: String,
316 376 }
317 377
318 378 /// Corresponds to the raw (i.e. on disk) representation of a template item.
319 379 /// Used as an intermediate step in deserialization.
320 380 #[derive(Clone, Debug, Deserialize)]
321 381 struct RawTemplateItem {
322 382 suffix: String,
323 383 default: Option<toml::Value>,
324 384 #[serde(rename = "default-type")]
325 385 default_type: Option<String>,
326 386 #[serde(default)]
327 387 priority: isize,
328 388 #[serde(default)]
329 389 generic: bool,
330 390 #[serde(default)]
331 391 alias: Vec<(String, String)>,
332 392 #[serde(default)]
333 393 experimental: bool,
334 394 #[serde(default)]
335 395 documentation: String,
336 396 }
337 397
338 398 impl TemplateItem {
339 399 fn into_default_item(
340 400 self,
341 401 application: TemplateApplication,
342 402 ) -> DefaultConfigItem {
343 403 DefaultConfigItem {
344 404 section: application.section,
345 405 name: application
346 406 .prefix
347 407 .map(|prefix| format!("{}.{}", prefix, self.suffix))
348 408 .unwrap_or(self.suffix),
349 409 default: self.default,
350 410 priority: self.priority,
351 411 alias: self.alias,
352 412 experimental: self.experimental,
353 413 documentation: self.documentation,
354 414 in_core_extension: None,
355 415 }
356 416 }
357 417 }
358 418
359 419 impl TryFrom<RawTemplateItem> for TemplateItem {
360 420 type Error = HgError;
361 421
362 422 fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> {
363 423 Ok(Self {
364 424 suffix: value.suffix,
365 425 default: raw_default_to_concrete(
366 426 value.default_type,
367 427 value.default,
368 428 )?,
369 429 priority: if value.generic {
370 430 Some(value.priority)
371 431 } else {
372 432 None
373 433 },
374 434 alias: value.alias,
375 435 experimental: value.experimental,
376 436 documentation: value.documentation,
377 437 })
378 438 }
379 439 }
380 440
381 441 /// Transforms the on-disk string-based representation of complex default types
382 442 /// to the concrete [`DefaultconfigItemType`].
383 443 fn raw_default_to_concrete(
384 444 default_type: Option<String>,
385 445 default: Option<toml::Value>,
386 446 ) -> Result<Option<DefaultConfigItemType>, HgError> {
387 447 Ok(match default_type.as_deref() {
388 448 None => default.as_ref().map(|default| {
389 449 DefaultConfigItemType::Primitive(default.to_owned())
390 450 }),
391 451 Some("dynamic") => Some(DefaultConfigItemType::Dynamic),
392 452 Some("list_type") => Some(DefaultConfigItemType::ListType),
393 453 Some("lambda") => match &default {
394 454 Some(default) => Some(DefaultConfigItemType::Lambda(
395 455 default.to_owned().try_into().map_err(|e| {
396 456 HgError::abort(
397 457 e.to_string(),
398 458 exit_codes::ABORT,
399 459 Some("Check 'mercurial/configitems.toml'".into()),
400 460 )
401 461 })?,
402 462 )),
403 463 None => {
404 464 return Err(HgError::abort(
405 465 "lambda defined with no return value".to_string(),
406 466 exit_codes::ABORT,
407 467 Some("Check 'mercurial/configitems.toml'".into()),
408 468 ))
409 469 }
410 470 },
411 471 Some("lazy_module") => match &default {
412 472 Some(default) => {
413 473 Some(DefaultConfigItemType::LazyModule(match default {
414 474 toml::Value::String(module) => module.to_owned(),
415 475 _ => {
416 476 return Err(HgError::abort(
417 477 "lazy_module module name should be a string"
418 478 .to_string(),
419 479 exit_codes::ABORT,
420 480 Some("Check 'mercurial/configitems.toml'".into()),
421 481 ))
422 482 }
423 483 }))
424 484 }
425 485 None => {
426 486 return Err(HgError::abort(
427 487 "lazy_module should have a default value".to_string(),
428 488 exit_codes::ABORT,
429 489 Some("Check 'mercurial/configitems.toml'".into()),
430 490 ))
431 491 }
432 492 },
433 493 Some(invalid) => {
434 494 return Err(HgError::abort(
435 495 format!("invalid default_type '{}'", invalid),
436 496 exit_codes::ABORT,
437 497 Some("Check 'mercurial/configitems.toml'".into()),
438 498 ))
439 499 }
440 500 })
441 501 }
442 502
443 503 #[derive(Debug, Clone, Deserialize)]
444 504 struct TemplateApplication {
445 505 template: String,
446 506 section: String,
447 507 #[serde(default)]
448 508 prefix: Option<String>,
449 509 }
450 510
451 511 /// Represents the (dynamic) set of default core Mercurial config items from
452 512 /// `mercurial/configitems.toml`.
453 513 #[derive(Clone, Debug, Default)]
454 514 pub struct DefaultConfig {
455 515 /// Mapping of section -> (mapping of name -> item)
456 516 items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>,
457 517 }
458 518
459 519 impl DefaultConfig {
460 520 pub fn empty() -> DefaultConfig {
461 521 Self {
462 522 items: Default::default(),
463 523 }
464 524 }
465 525
466 526 /// Returns `Self`, given the contents of `mercurial/configitems.toml`
467 527 #[logging_timer::time("trace")]
468 528 pub fn from_contents(contents: &str) -> Result<Self, HgError> {
469 529 let mut from_file: ConfigItems =
470 530 toml::from_str(contents).map_err(|e| {
471 531 HgError::abort(
472 532 e.to_string(),
473 533 exit_codes::ABORT,
474 534 Some("Check 'mercurial/configitems.toml'".into()),
475 535 )
476 536 })?;
477 537
478 538 let mut flat_items = from_file.items;
479 539
480 540 for application in from_file.template_applications.drain(..) {
481 541 match from_file.templates.get(&application.template) {
482 542 None => return Err(
483 543 HgError::abort(
484 544 format!(
485 545 "template application refers to undefined template '{}'",
486 546 application.template
487 547 ),
488 548 exit_codes::ABORT,
489 549 Some("Check 'mercurial/configitems.toml'".into())
490 550 )
491 551 ),
492 552 Some(template_items) => {
493 553 for template_item in template_items {
494 554 flat_items.push(
495 555 template_item
496 556 .clone()
497 557 .into_default_item(application.clone()),
498 558 )
499 559 }
500 560 }
501 561 };
502 562 }
503 563
504 564 let items = flat_items.into_iter().fold(
505 565 FastHashMap::default(),
506 566 |mut acc, item| {
507 567 acc.entry(item.section.to_owned())
508 568 .or_insert_with(|| {
509 569 let mut section = FastHashMap::default();
510 570 section.insert(item.name.to_owned(), item.to_owned());
511 571 section
512 572 })
513 573 .insert(item.name.to_owned(), item);
514 574 acc
515 575 },
516 576 );
517 577
518 578 Ok(Self { items })
519 579 }
520 580
521 581 /// Return the default config item that matches `section` and `item`.
522 582 pub fn get(
523 583 &self,
524 584 section: &[u8],
525 585 item: &[u8],
526 586 ) -> Option<&DefaultConfigItem> {
527 587 // Core items must be valid UTF-8
528 588 let section = String::from_utf8_lossy(section);
529 589 let section_map = self.items.get(section.as_ref())?;
530 590 let item_name_lossy = String::from_utf8_lossy(item);
531 591 match section_map.get(item_name_lossy.as_ref()) {
532 592 Some(item) => Some(item),
533 593 None => {
534 594 for generic_item in section_map
535 595 .values()
536 596 .filter(|item| item.is_generic())
537 597 .sorted_by_key(|item| match item.priority {
538 598 Some(priority) => (priority, &item.name),
539 599 _ => unreachable!(),
540 600 })
541 601 {
542 602 if regex::bytes::Regex::new(&generic_item.name)
543 603 .expect("invalid regex in configitems")
544 604 .is_match(item)
545 605 {
546 606 return Some(generic_item);
547 607 }
548 608 }
549 609 None
550 610 }
551 611 }
552 612 }
553 613 }
554 614
555 615 #[cfg(test)]
556 616 mod tests {
557 617 use crate::config::config_items::{
558 618 DefaultConfigItem, DefaultConfigItemType,
559 619 };
560 620
561 621 use super::DefaultConfig;
562 622
563 623 #[test]
564 624 fn test_config_read() {
565 625 let contents = r#"
566 626 [[items]]
567 627 section = "alias"
568 628 name = "abcd.*"
569 629 default = 3
570 630 generic = true
571 631 priority = -1
572 632
573 633 [[items]]
574 634 section = "alias"
575 635 name = ".*"
576 636 default-type = "dynamic"
577 637 generic = true
578 638
579 639 [[items]]
580 640 section = "cmdserver"
581 641 name = "track-log"
582 642 default-type = "lambda"
583 643 default = [ "chgserver", "cmdserver", "repocache",]
584 644
585 645 [[items]]
586 646 section = "chgserver"
587 647 name = "idletimeout"
588 648 default = 3600
589 649
590 650 [[items]]
591 651 section = "cmdserver"
592 652 name = "message-encodings"
593 653 default-type = "list_type"
594 654
595 655 [[items]]
596 656 section = "web"
597 657 name = "encoding"
598 658 default-type = "lazy_module"
599 659 default = "encoding.encoding"
600 660
601 661 [[items]]
602 662 section = "command-templates"
603 663 name = "graphnode"
604 664 alias = [["ui", "graphnodetemplate"]]
605 665 documentation = """This is a docstring.
606 666 This is another line \
607 667 but this is not."""
608 668
609 669 [[items]]
610 670 section = "censor"
611 671 name = "policy"
612 672 default = "abort"
613 673 experimental = true
614 674
615 675 [[template-applications]]
616 676 template = "diff-options"
617 677 section = "commands"
618 678 prefix = "revert.interactive"
619 679
620 680 [[template-applications]]
621 681 template = "diff-options"
622 682 section = "diff"
623 683
624 684 [templates]
625 685 [[templates.diff-options]]
626 686 suffix = "nodates"
627 687 default = false
628 688
629 689 [[templates.diff-options]]
630 690 suffix = "showfunc"
631 691 default = false
632 692
633 693 [[templates.diff-options]]
634 694 suffix = "unified"
635 695 "#;
636 696 let res = DefaultConfig::from_contents(contents);
637 697 let config = match res {
638 698 Ok(config) => config,
639 699 Err(e) => panic!("{}", e),
640 700 };
641 701 let expected = DefaultConfigItem {
642 702 section: "censor".into(),
643 703 name: "policy".into(),
644 704 default: Some(DefaultConfigItemType::Primitive("abort".into())),
645 705 priority: None,
646 706 alias: vec![],
647 707 experimental: true,
648 708 documentation: "".into(),
649 709 in_core_extension: None,
650 710 };
651 711 assert_eq!(config.get(b"censor", b"policy"), Some(&expected));
652 712
653 713 // Test generic priority. The `.*` pattern is wider than `abcd.*`, but
654 714 // `abcd.*` has priority, so it should match first.
655 715 let expected = DefaultConfigItem {
656 716 section: "alias".into(),
657 717 name: "abcd.*".into(),
658 718 default: Some(DefaultConfigItemType::Primitive(3.into())),
659 719 priority: Some(-1),
660 720 alias: vec![],
661 721 experimental: false,
662 722 documentation: "".into(),
663 723 in_core_extension: None,
664 724 };
665 725 assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected));
666 726
667 727 //... but if it doesn't, we should fallback to `.*`
668 728 let expected = DefaultConfigItem {
669 729 section: "alias".into(),
670 730 name: ".*".into(),
671 731 default: Some(DefaultConfigItemType::Dynamic),
672 732 priority: Some(0),
673 733 alias: vec![],
674 734 experimental: false,
675 735 documentation: "".into(),
676 736 in_core_extension: None,
677 737 };
678 738 assert_eq!(config.get(b"alias", b"something"), Some(&expected));
679 739
680 740 let expected = DefaultConfigItem {
681 741 section: "chgserver".into(),
682 742 name: "idletimeout".into(),
683 743 default: Some(DefaultConfigItemType::Primitive(3600.into())),
684 744 priority: None,
685 745 alias: vec![],
686 746 experimental: false,
687 747 documentation: "".into(),
688 748 in_core_extension: None,
689 749 };
690 750 assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected));
691 751
692 752 let expected = DefaultConfigItem {
693 753 section: "cmdserver".into(),
694 754 name: "track-log".into(),
695 755 default: Some(DefaultConfigItemType::Lambda(vec![
696 756 "chgserver".into(),
697 757 "cmdserver".into(),
698 758 "repocache".into(),
699 759 ])),
700 760 priority: None,
701 761 alias: vec![],
702 762 experimental: false,
703 763 documentation: "".into(),
704 764 in_core_extension: None,
705 765 };
706 766 assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected));
707 767
708 768 let expected = DefaultConfigItem {
709 769 section: "command-templates".into(),
710 770 name: "graphnode".into(),
711 771 default: None,
712 772 priority: None,
713 773 alias: vec![("ui".into(), "graphnodetemplate".into())],
714 774 experimental: false,
715 775 documentation:
716 776 "This is a docstring.\nThis is another line but this is not."
717 777 .into(),
718 778 in_core_extension: None,
719 779 };
720 780 assert_eq!(
721 781 config.get(b"command-templates", b"graphnode"),
722 782 Some(&expected)
723 783 );
724 784 }
725 785 }
@@ -1,810 +1,928
1 1 // config.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 //! Mercurial config parsing and interfaces.
11 11
12 12 pub mod config_items;
13 13 mod layer;
14 14 mod plain_info;
15 15 mod values;
16 16 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
17 17 use lazy_static::lazy_static;
18 18 pub use plain_info::PlainInfo;
19 19
20 20 use self::config_items::DefaultConfig;
21 21 use self::config_items::DefaultConfigItem;
22 22 use self::layer::ConfigLayer;
23 23 use self::layer::ConfigValue;
24 24 use crate::errors::HgError;
25 25 use crate::errors::{HgResultExt, IoResultExt};
26 26 use crate::utils::files::get_bytes_from_os_str;
27 27 use format_bytes::{write_bytes, DisplayBytes};
28 28 use std::collections::HashSet;
29 29 use std::env;
30 30 use std::fmt;
31 31 use std::path::{Path, PathBuf};
32 32 use std::str;
33 33
34 34 lazy_static! {
35 35 static ref DEFAULT_CONFIG: Result<DefaultConfig, HgError> = {
36 36 DefaultConfig::from_contents(include_str!(
37 37 "../../../../mercurial/configitems.toml"
38 38 ))
39 39 };
40 40 }
41 41
42 42 /// Holds the config values for the current repository
43 43 /// TODO update this docstring once we support more sources
44 44 #[derive(Clone)]
45 45 pub struct Config {
46 46 layers: Vec<layer::ConfigLayer>,
47 47 plain: PlainInfo,
48 48 }
49 49
50 50 impl DisplayBytes for Config {
51 51 fn display_bytes(
52 52 &self,
53 53 out: &mut dyn std::io::Write,
54 54 ) -> std::io::Result<()> {
55 55 for (index, layer) in self.layers.iter().rev().enumerate() {
56 56 write_bytes!(
57 57 out,
58 58 b"==== Layer {} (trusted: {}) ====\n{}",
59 59 index,
60 60 if layer.trusted {
61 61 &b"yes"[..]
62 62 } else {
63 63 &b"no"[..]
64 64 },
65 65 layer
66 66 )?;
67 67 }
68 68 Ok(())
69 69 }
70 70 }
71 71
72 72 pub enum ConfigSource {
73 73 /// Absolute path to a config file
74 74 AbsPath(PathBuf),
75 75 /// Already parsed (from the CLI, env, Python resources, etc.)
76 76 Parsed(layer::ConfigLayer),
77 77 }
78 78
79 79 #[derive(Debug)]
80 80 pub struct ConfigValueParseErrorDetails {
81 81 pub origin: ConfigOrigin,
82 82 pub line: Option<usize>,
83 83 pub section: Vec<u8>,
84 84 pub item: Vec<u8>,
85 85 pub value: Vec<u8>,
86 86 pub expected_type: &'static str,
87 87 }
88 88
89 89 // boxed to avoid very large Result types
90 90 pub type ConfigValueParseError = Box<ConfigValueParseErrorDetails>;
91 91
92 92 impl fmt::Display for ConfigValueParseError {
93 93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 94 // TODO: add origin and line number information, here and in
95 95 // corresponding python code
96 96 write!(
97 97 f,
98 98 "config error: {}.{} is not a {} ('{}')",
99 99 String::from_utf8_lossy(&self.section),
100 100 String::from_utf8_lossy(&self.item),
101 101 self.expected_type,
102 102 String::from_utf8_lossy(&self.value)
103 103 )
104 104 }
105 105 }
106 106
107 107 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
108 108 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
109 109 // duplication with [_applyconfig] in [ui.py],
110 110 if !plain.is_plain() {
111 111 return false;
112 112 }
113 113 if section == b"alias" {
114 114 return plain.plainalias();
115 115 }
116 116 if section == b"revsetalias" {
117 117 return plain.plainrevsetalias();
118 118 }
119 119 if section == b"templatealias" {
120 120 return plain.plaintemplatealias();
121 121 }
122 122 if section == b"ui" {
123 123 let to_delete: &[&[u8]] = &[
124 124 b"debug",
125 125 b"fallbackencoding",
126 126 b"quiet",
127 127 b"slash",
128 128 b"logtemplate",
129 129 b"message-output",
130 130 b"statuscopies",
131 131 b"style",
132 132 b"traceback",
133 133 b"verbose",
134 134 ];
135 135 return to_delete.contains(&item);
136 136 }
137 137 let sections_to_delete: &[&[u8]] =
138 138 &[b"defaults", b"commands", b"command-templates"];
139 139 sections_to_delete.contains(&section)
140 140 }
141 141
142 142 impl Config {
143 143 /// The configuration to use when printing configuration-loading errors
144 144 pub fn empty() -> Self {
145 145 Self {
146 146 layers: Vec::new(),
147 147 plain: PlainInfo::empty(),
148 148 }
149 149 }
150 150
151 151 /// Load system and user configuration from various files.
152 152 ///
153 153 /// This is also affected by some environment variables.
154 154 pub fn load_non_repo() -> Result<Self, ConfigError> {
155 155 let mut config = Self::empty();
156 156 let opt_rc_path = env::var_os("HGRCPATH");
157 157 // HGRCPATH replaces system config
158 158 if opt_rc_path.is_none() {
159 159 config.add_system_config()?
160 160 }
161 161
162 162 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
163 163 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
164 164 config.add_for_environment_variable("PAGER", b"pager", b"pager");
165 165
166 166 // These are set by `run-tests.py --rhg` to enable fallback for the
167 167 // entire test suite. Alternatives would be setting configuration
168 168 // through `$HGRCPATH` but some tests override that, or changing the
169 169 // `hg` shell alias to include `--config` but that disrupts tests that
170 170 // print command lines and check expected output.
171 171 config.add_for_environment_variable(
172 172 "RHG_ON_UNSUPPORTED",
173 173 b"rhg",
174 174 b"on-unsupported",
175 175 );
176 176 config.add_for_environment_variable(
177 177 "RHG_FALLBACK_EXECUTABLE",
178 178 b"rhg",
179 179 b"fallback-executable",
180 180 );
181 181
182 182 // HGRCPATH replaces user config
183 183 if opt_rc_path.is_none() {
184 184 config.add_user_config()?
185 185 }
186 186 if let Some(rc_path) = &opt_rc_path {
187 187 for path in env::split_paths(rc_path) {
188 188 if !path.as_os_str().is_empty() {
189 189 if path.is_dir() {
190 190 config.add_trusted_dir(&path)?
191 191 } else {
192 192 config.add_trusted_file(&path)?
193 193 }
194 194 }
195 195 }
196 196 }
197 197 Ok(config)
198 198 }
199 199
200 200 pub fn load_cli_args(
201 201 &mut self,
202 202 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
203 203 color_arg: Option<Vec<u8>>,
204 204 ) -> Result<(), ConfigError> {
205 205 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
206 206 self.layers.push(layer)
207 207 }
208 208 if let Some(arg) = color_arg {
209 209 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
210 210 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
211 211 self.layers.push(layer)
212 212 }
213 213 Ok(())
214 214 }
215 215
216 216 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
217 217 if let Some(entries) = std::fs::read_dir(path)
218 218 .when_reading_file(path)
219 219 .io_not_found_as_none()?
220 220 {
221 221 let mut file_paths = entries
222 222 .map(|result| {
223 223 result.when_reading_file(path).map(|entry| entry.path())
224 224 })
225 225 .collect::<Result<Vec<_>, _>>()?;
226 226 file_paths.sort();
227 227 for file_path in &file_paths {
228 228 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
229 229 self.add_trusted_file(file_path)?
230 230 }
231 231 }
232 232 }
233 233 Ok(())
234 234 }
235 235
236 236 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
237 237 if let Some(data) = std::fs::read(path)
238 238 .when_reading_file(path)
239 239 .io_not_found_as_none()?
240 240 {
241 241 self.layers.extend(ConfigLayer::parse(path, &data)?)
242 242 }
243 243 Ok(())
244 244 }
245 245
246 246 fn add_for_environment_variable(
247 247 &mut self,
248 248 var: &str,
249 249 section: &[u8],
250 250 key: &[u8],
251 251 ) {
252 252 if let Some(value) = env::var_os(var) {
253 253 let origin = layer::ConfigOrigin::Environment(var.into());
254 254 let mut layer = ConfigLayer::new(origin);
255 255 layer.add(
256 256 section.to_owned(),
257 257 key.to_owned(),
258 258 get_bytes_from_os_str(value),
259 259 None,
260 260 );
261 261 self.layers.push(layer)
262 262 }
263 263 }
264 264
265 265 #[cfg(unix)] // TODO: other platforms
266 266 fn add_system_config(&mut self) -> Result<(), ConfigError> {
267 267 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
268 268 let etc = prefix.join("etc").join("mercurial");
269 269 self.add_trusted_file(&etc.join("hgrc"))?;
270 270 self.add_trusted_dir(&etc.join("hgrc.d"))
271 271 };
272 272 let root = Path::new("/");
273 273 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
274 274 // instead? TODO: can this be a relative path?
275 275 let hg = crate::utils::current_exe()?;
276 276 // TODO: this order (per-installation then per-system) matches
277 277 // `systemrcpath()` in `mercurial/scmposix.py`, but
278 278 // `mercurial/helptext/config.txt` suggests it should be reversed
279 279 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
280 280 if installation_prefix != root {
281 281 add_for_prefix(installation_prefix)?
282 282 }
283 283 }
284 284 add_for_prefix(root)?;
285 285 Ok(())
286 286 }
287 287
288 288 #[cfg(unix)] // TODO: other plateforms
289 289 fn add_user_config(&mut self) -> Result<(), ConfigError> {
290 290 let opt_home = home::home_dir();
291 291 if let Some(home) = &opt_home {
292 292 self.add_trusted_file(&home.join(".hgrc"))?
293 293 }
294 294 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
295 295 if !darwin {
296 296 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
297 297 .map(PathBuf::from)
298 298 .or_else(|| opt_home.map(|home| home.join(".config")))
299 299 {
300 300 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
301 301 }
302 302 }
303 303 Ok(())
304 304 }
305 305
306 306 /// Loads in order, which means that the precedence is the same
307 307 /// as the order of `sources`.
308 308 pub fn load_from_explicit_sources(
309 309 sources: Vec<ConfigSource>,
310 310 ) -> Result<Self, ConfigError> {
311 311 let mut layers = vec![];
312 312
313 313 for source in sources.into_iter() {
314 314 match source {
315 315 ConfigSource::Parsed(c) => layers.push(c),
316 316 ConfigSource::AbsPath(c) => {
317 317 // TODO check if it should be trusted
318 318 // mercurial/ui.py:427
319 319 let data = match std::fs::read(&c) {
320 320 Err(_) => continue, // same as the python code
321 321 Ok(data) => data,
322 322 };
323 323 layers.extend(ConfigLayer::parse(&c, &data)?)
324 324 }
325 325 }
326 326 }
327 327
328 328 Ok(Config {
329 329 layers,
330 330 plain: PlainInfo::empty(),
331 331 })
332 332 }
333 333
334 334 /// Loads the per-repository config into a new `Config` which is combined
335 335 /// with `self`.
336 336 pub(crate) fn combine_with_repo(
337 337 &self,
338 338 repo_config_files: &[PathBuf],
339 339 ) -> Result<Self, ConfigError> {
340 340 let (cli_layers, other_layers) = self
341 341 .layers
342 342 .iter()
343 343 .cloned()
344 344 .partition(ConfigLayer::is_from_command_line);
345 345
346 346 let mut repo_config = Self {
347 347 layers: other_layers,
348 348 plain: PlainInfo::empty(),
349 349 };
350 350 for path in repo_config_files {
351 351 // TODO: check if this file should be trusted:
352 352 // `mercurial/ui.py:427`
353 353 repo_config.add_trusted_file(path)?;
354 354 }
355 355 repo_config.layers.extend(cli_layers);
356 356 Ok(repo_config)
357 357 }
358 358
359 359 pub fn apply_plain(&mut self, plain: PlainInfo) {
360 360 self.plain = plain;
361 361 }
362 362
363 363 /// Returns the default value for the given config item, if any.
364 364 pub fn get_default(
365 365 &self,
366 366 section: &[u8],
367 367 item: &[u8],
368 368 ) -> Result<Option<&DefaultConfigItem>, HgError> {
369 369 let default_config = DEFAULT_CONFIG.as_ref().map_err(|e| {
370 370 HgError::abort(
371 371 e.to_string(),
372 372 crate::exit_codes::ABORT,
373 373 Some("`mercurial/configitems.toml` is not valid".into()),
374 374 )
375 375 })?;
376 376 let default_opt = default_config.get(section, item);
377 377 Ok(default_opt.filter(|default| {
378 378 default
379 379 .in_core_extension()
380 380 .map(|extension| {
381 381 // Only return the default for an in-core extension item
382 382 // if said extension is enabled
383 383 self.is_extension_enabled(extension.as_bytes())
384 384 })
385 385 .unwrap_or(true)
386 386 }))
387 387 }
388 388
389 389 /// Return the config item that corresponds to a section + item, a function
390 390 /// to parse from the raw bytes to the expected type (which is passed as
391 391 /// a string only to make debugging easier).
392 392 /// Used by higher-level methods like `get_bool`.
393 393 ///
394 394 /// `fallback_to_default` controls whether the default value (if any) is
395 395 /// returned if nothing is found.
396 396 fn get_parse<'config, T: 'config>(
397 397 &'config self,
398 398 section: &[u8],
399 399 item: &[u8],
400 400 expected_type: &'static str,
401 401 parse: impl Fn(&'config [u8]) -> Option<T>,
402 402 fallback_to_default: bool,
403 403 ) -> Result<Option<T>, HgError>
404 404 where
405 405 Option<T>: TryFrom<&'config DefaultConfigItem, Error = HgError>,
406 406 {
407 407 match self.get_inner(section, item) {
408 408 Some((layer, v)) => match parse(&v.bytes) {
409 409 Some(b) => Ok(Some(b)),
410 410 None => Err(Box::new(ConfigValueParseErrorDetails {
411 411 origin: layer.origin.to_owned(),
412 412 line: v.line,
413 413 value: v.bytes.to_owned(),
414 414 section: section.to_owned(),
415 415 item: item.to_owned(),
416 416 expected_type,
417 417 })
418 418 .into()),
419 419 },
420 420 None => {
421 421 if !fallback_to_default {
422 422 return Ok(None);
423 423 }
424 424 match self.get_default(section, item)? {
425 425 Some(default) => {
426 426 // Defaults are TOML values, so they're not in the same
427 427 // shape as in the config files.
428 428 // First try to convert directly to the expected type
429 429 let as_t = default.try_into();
430 430 match as_t {
431 431 Ok(t) => Ok(t),
432 432 Err(e) => {
433 433 // If it fails, it means that...
434 434 let as_bytes: Result<Option<&[u8]>, _> =
435 435 default.try_into();
436 436 match as_bytes {
437 437 Ok(bytes_opt) => {
438 438 if let Some(bytes) = bytes_opt {
439 439 // ...we should be able to parse it
440 440 return Ok(parse(bytes));
441 441 }
442 442 Err(e)
443 443 }
444 444 Err(_) => Err(e),
445 445 }
446 446 }
447 447 }
448 448 }
449 449 None => {
450 450 self.print_devel_warning(section, item)?;
451 451 Ok(None)
452 452 }
453 453 }
454 454 }
455 455 }
456 456 }
457 457
458 458 fn print_devel_warning(
459 459 &self,
460 460 section: &[u8],
461 461 item: &[u8],
462 462 ) -> Result<(), HgError> {
463 463 let warn_all = self.get_bool(b"devel", b"all-warnings")?;
464 464 let warn_specific = self.get_bool(b"devel", b"warn-config-unknown")?;
465 465 if !warn_all || !warn_specific {
466 466 // We technically shouldn't print anything here since it's not
467 467 // the concern of `hg-core`.
468 468 //
469 469 // We're printing directly to stderr since development warnings
470 470 // are not on by default and surfacing this to consumer crates
471 471 // (like `rhg`) would be more difficult, probably requiring
472 472 // something à la `log` crate.
473 473 //
474 474 // TODO maybe figure out a way of exposing a "warnings" channel
475 475 // that consumer crates can hook into. It would be useful for
476 476 // all other warnings that `hg-core` could expose.
477 477 eprintln!(
478 478 "devel-warn: accessing unregistered config item: '{}.{}'",
479 479 String::from_utf8_lossy(section),
480 480 String::from_utf8_lossy(item),
481 481 );
482 482 }
483 483 Ok(())
484 484 }
485 485
486 486 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
487 487 /// Otherwise, returns an `Ok(value)` if found, or `None`.
488 488 pub fn get_str(
489 489 &self,
490 490 section: &[u8],
491 491 item: &[u8],
492 492 ) -> Result<Option<&str>, HgError> {
493 493 self.get_parse(
494 494 section,
495 495 item,
496 496 "ASCII or UTF-8 string",
497 497 |value| str::from_utf8(value).ok(),
498 498 true,
499 499 )
500 500 }
501 501
502 502 /// Same as `get_str`, but doesn't fall back to the default `configitem`
503 503 /// if not defined in the user config.
504 504 pub fn get_str_no_default(
505 505 &self,
506 506 section: &[u8],
507 507 item: &[u8],
508 508 ) -> Result<Option<&str>, HgError> {
509 509 self.get_parse(
510 510 section,
511 511 item,
512 512 "ASCII or UTF-8 string",
513 513 |value| str::from_utf8(value).ok(),
514 514 false,
515 515 )
516 516 }
517 517
518 518 /// Returns an `Err` if the first value found is not a valid unsigned
519 519 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
520 520 pub fn get_u32(
521 521 &self,
522 522 section: &[u8],
523 523 item: &[u8],
524 524 ) -> Result<Option<u32>, HgError> {
525 525 self.get_parse(
526 526 section,
527 527 item,
528 528 "valid integer",
529 529 |value| str::from_utf8(value).ok()?.parse().ok(),
530 530 true,
531 531 )
532 532 }
533 533
534 /// Returns an `Err` if the first value found is not a valid unsigned
535 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
536 pub fn get_i64(
537 &self,
538 section: &[u8],
539 item: &[u8],
540 ) -> Result<Option<i64>, HgError> {
541 self.get_parse(
542 section,
543 item,
544 "valid integer",
545 |value| str::from_utf8(value).ok()?.parse().ok(),
546 true,
547 )
548 }
549
550 /// Returns an `Err` if the first value found is not a valid unsigned
551 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
552 pub fn get_u64(
553 &self,
554 section: &[u8],
555 item: &[u8],
556 ) -> Result<Option<u64>, HgError> {
557 self.get_parse(
558 section,
559 item,
560 "valid integer",
561 |value| str::from_utf8(value).ok()?.parse().ok(),
562 true,
563 )
564 }
565
566 /// Returns an `Err` if the first value found is not a valid float
567 /// representation. Otherwise, returns an `Ok(value)` if found, or `None`.
568 pub fn get_f64(
569 &self,
570 section: &[u8],
571 item: &[u8],
572 ) -> Result<Option<f64>, HgError> {
573 self.get_parse(
574 section,
575 item,
576 "valid float",
577 |value| str::from_utf8(value).ok()?.parse().ok(),
578 true,
579 )
580 }
581
534 582 /// Returns an `Err` if the first value found is not a valid file size
535 583 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
536 584 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
537 585 pub fn get_byte_size(
538 586 &self,
539 587 section: &[u8],
540 588 item: &[u8],
541 589 ) -> Result<Option<u64>, HgError> {
542 590 self.get_parse(
543 591 section,
544 592 item,
545 593 "byte quantity",
546 594 values::parse_byte_size,
547 595 true,
548 596 )
549 597 }
550 598
599 /// Same as [`Self::get_byte_size`], but doesn't fall back to the default
600 /// `configitem` if not defined in the user config.
601 pub fn get_byte_size_no_default(
602 &self,
603 section: &[u8],
604 item: &[u8],
605 ) -> Result<Option<u64>, HgError> {
606 self.get_parse(
607 section,
608 item,
609 "byte quantity",
610 values::parse_byte_size,
611 false,
612 )
613 }
614
551 615 /// Returns an `Err` if the first value found is not a valid boolean.
552 616 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
553 617 /// found, or `None`.
554 618 pub fn get_option(
555 619 &self,
556 620 section: &[u8],
557 621 item: &[u8],
558 622 ) -> Result<Option<bool>, HgError> {
559 623 self.get_parse(section, item, "boolean", values::parse_bool, true)
560 624 }
561 625
562 626 /// Same as `get_option`, but doesn't fall back to the default `configitem`
563 627 /// if not defined in the user config.
564 628 pub fn get_option_no_default(
565 629 &self,
566 630 section: &[u8],
567 631 item: &[u8],
568 632 ) -> Result<Option<bool>, HgError> {
569 633 self.get_parse(section, item, "boolean", values::parse_bool, false)
570 634 }
571 635
572 636 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
573 637 /// if the value is not found, an `Err` if it's not a valid boolean.
574 638 pub fn get_bool(
575 639 &self,
576 640 section: &[u8],
577 641 item: &[u8],
578 642 ) -> Result<bool, HgError> {
579 643 Ok(self.get_option(section, item)?.unwrap_or(false))
580 644 }
581 645
582 646 /// Same as `get_bool`, but doesn't fall back to the default `configitem`
583 647 /// if not defined in the user config.
584 648 pub fn get_bool_no_default(
585 649 &self,
586 650 section: &[u8],
587 651 item: &[u8],
588 652 ) -> Result<bool, HgError> {
589 653 Ok(self.get_option_no_default(section, item)?.unwrap_or(false))
590 654 }
591 655
592 656 /// Returns `true` if the extension is enabled, `false` otherwise
593 657 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
594 658 let value = self.get(b"extensions", extension);
595 659 match value {
596 660 Some(c) => !c.starts_with(b"!"),
597 661 None => false,
598 662 }
599 663 }
600 664
601 665 /// If there is an `item` value in `section`, parse and return a list of
602 666 /// byte strings.
603 667 pub fn get_list(
604 668 &self,
605 669 section: &[u8],
606 670 item: &[u8],
607 671 ) -> Option<Vec<Vec<u8>>> {
608 672 self.get(section, item).map(values::parse_list)
609 673 }
610 674
611 675 /// Returns the raw value bytes of the first one found, or `None`.
612 676 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
613 677 self.get_inner(section, item)
614 678 .map(|(_, value)| value.bytes.as_ref())
615 679 }
616 680
617 681 /// Returns the raw value bytes of the first one found, or `None`.
618 682 pub fn get_with_origin(
619 683 &self,
620 684 section: &[u8],
621 685 item: &[u8],
622 686 ) -> Option<(&[u8], &ConfigOrigin)> {
623 687 self.get_inner(section, item)
624 688 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
625 689 }
626 690
627 691 /// Returns the layer and the value of the first one found, or `None`.
628 692 fn get_inner(
629 693 &self,
630 694 section: &[u8],
631 695 item: &[u8],
632 696 ) -> Option<(&ConfigLayer, &ConfigValue)> {
633 697 // Filter out the config items that are hidden by [PLAIN].
634 698 // This differs from python hg where we delete them from the config.
635 699 let should_ignore = should_ignore(&self.plain, section, item);
636 700 for layer in self.layers.iter().rev() {
637 701 if !layer.trusted {
638 702 continue;
639 703 }
640 704 //The [PLAIN] config should not affect the defaults.
641 705 //
642 706 // However, PLAIN should also affect the "tweaked" defaults (unless
643 707 // "tweakdefault" is part of "HGPLAINEXCEPT").
644 708 //
645 709 // In practice the tweak-default layer is only added when it is
646 710 // relevant, so we can safely always take it into
647 711 // account here.
648 712 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
649 713 {
650 714 continue;
651 715 }
652 716 if let Some(v) = layer.get(section, item) {
653 717 return Some((layer, v));
654 718 }
655 719 }
656 720 None
657 721 }
658 722
659 723 /// Return all keys defined for the given section
660 724 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
661 725 self.layers
662 726 .iter()
663 727 .flat_map(|layer| layer.iter_keys(section))
664 728 .collect()
665 729 }
666 730
667 731 /// Returns whether any key is defined in the given section
668 732 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
669 733 self.layers
670 734 .iter()
671 735 .any(|layer| layer.has_non_empty_section(section))
672 736 }
673 737
674 738 /// Yields (key, value) pairs for everything in the given section
675 739 pub fn iter_section<'a>(
676 740 &'a self,
677 741 section: &'a [u8],
678 742 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
679 743 // Deduplicate keys redefined in multiple layers
680 744 let mut keys_already_seen = HashSet::new();
681 745 let mut key_is_new =
682 746 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
683 747 keys_already_seen.insert(key)
684 748 };
685 749 // This is similar to `flat_map` + `filter_map`, except with a single
686 750 // closure that owns `key_is_new` (and therefore the
687 751 // `keys_already_seen` set):
688 752 let mut layer_iters = self
689 753 .layers
690 754 .iter()
691 755 .rev()
692 756 .map(move |layer| layer.iter_section(section))
693 757 .peekable();
694 758 std::iter::from_fn(move || loop {
695 759 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
696 760 return Some(pair);
697 761 } else {
698 762 layer_iters.next();
699 763 }
700 764 })
701 765 }
702 766
703 767 /// Get raw values bytes from all layers (even untrusted ones) in order
704 768 /// of precedence.
705 769 #[cfg(test)]
706 770 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
707 771 let mut res = vec![];
708 772 for layer in self.layers.iter().rev() {
709 773 if let Some(v) = layer.get(section, item) {
710 774 res.push(v.bytes.as_ref());
711 775 }
712 776 }
713 777 res
714 778 }
715 779
716 780 // a config layer that's introduced by ui.tweakdefaults
717 781 fn tweakdefaults_layer() -> ConfigLayer {
718 782 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
719 783
720 784 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
721 785 layer.add(
722 786 section[..].into(),
723 787 item[..].into(),
724 788 value[..].into(),
725 789 None,
726 790 );
727 791 };
728 792 // duplication of [tweakrc] from [ui.py]
729 793 add(b"ui", b"rollback", b"False");
730 794 add(b"ui", b"statuscopies", b"yes");
731 795 add(b"ui", b"interface", b"curses");
732 796 add(b"ui", b"relative-paths", b"yes");
733 797 add(b"commands", b"grep.all-files", b"True");
734 798 add(b"commands", b"update.check", b"noconflict");
735 799 add(b"commands", b"status.verbose", b"True");
736 800 add(b"commands", b"resolve.explicit-re-merge", b"True");
737 801 add(b"git", b"git", b"1");
738 802 add(b"git", b"showfunc", b"1");
739 803 add(b"git", b"word-diff", b"1");
740 804 layer
741 805 }
742 806
743 807 // introduce the tweaked defaults as implied by ui.tweakdefaults
744 808 pub fn tweakdefaults(&mut self) {
745 809 self.layers.insert(0, Config::tweakdefaults_layer());
746 810 }
811
812 /// Return the resource profile for a dimension (memory, cpu or disk).
813 ///
814 /// If no dimension is specified, the generic value is returned.
815 pub fn get_resource_profile(
816 &self,
817 dimension: Option<&str>,
818 ) -> ResourceProfile {
819 let mut value = self.resource_profile_from_item(b"usage", b"resource");
820 if let Some(dimension) = &dimension {
821 let sub_value = self.resource_profile_from_item(
822 b"usage",
823 format!("resources.{}", dimension).as_bytes(),
824 );
825 if sub_value != ResourceProfileValue::Default {
826 value = sub_value
827 }
828 }
829 ResourceProfile {
830 dimension: dimension.map(ToOwned::to_owned),
831 value,
832 }
833 }
834
835 fn resource_profile_from_item(
836 &self,
837 section: &[u8],
838 item: &[u8],
839 ) -> ResourceProfileValue {
840 match self.get(section, item).unwrap_or(b"default") {
841 b"default" => ResourceProfileValue::Default,
842 b"low" => ResourceProfileValue::Low,
843 b"medium" => ResourceProfileValue::Medium,
844 b"high" => ResourceProfileValue::High,
845 _ => ResourceProfileValue::Default,
846 }
847 }
848 }
849
850 /// Corresponds to `usage.resources[.<dimension>]`.
851 ///
852 /// See `hg help config.usage.resources`.
853 #[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
854 pub struct ResourceProfile {
855 pub dimension: Option<String>,
856 pub value: ResourceProfileValue,
857 }
858
859 #[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
860 pub enum ResourceProfileValue {
861 Default,
862 Low,
863 Medium,
864 High,
747 865 }
748 866
749 867 #[cfg(test)]
750 868 mod tests {
751 869 use super::*;
752 870 use pretty_assertions::assert_eq;
753 871 use std::fs::File;
754 872 use std::io::Write;
755 873
756 874 #[test]
757 875 fn test_include_layer_ordering() {
758 876 let tmpdir = tempfile::tempdir().unwrap();
759 877 let tmpdir_path = tmpdir.path();
760 878 let mut included_file =
761 879 File::create(tmpdir_path.join("included.rc")).unwrap();
762 880
763 881 included_file.write_all(b"[section]\nitem=value1").unwrap();
764 882 let base_config_path = tmpdir_path.join("base.rc");
765 883 let mut config_file = File::create(&base_config_path).unwrap();
766 884 let data =
767 885 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
768 886 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
769 887 config_file.write_all(data).unwrap();
770 888
771 889 let sources = vec![ConfigSource::AbsPath(base_config_path)];
772 890 let config = Config::load_from_explicit_sources(sources)
773 891 .expect("expected valid config");
774 892
775 893 let (_, value) = config.get_inner(b"section", b"item").unwrap();
776 894 assert_eq!(
777 895 value,
778 896 &ConfigValue {
779 897 bytes: b"value2".to_vec(),
780 898 line: Some(4)
781 899 }
782 900 );
783 901
784 902 let value = config.get(b"section", b"item").unwrap();
785 903 assert_eq!(value, b"value2",);
786 904 assert_eq!(
787 905 config.get_all(b"section", b"item"),
788 906 [b"value2", b"value1", b"value0"]
789 907 );
790 908
791 909 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
792 910 assert_eq!(
793 911 config.get_byte_size(b"section2", b"size").unwrap(),
794 912 Some(1024 + 512)
795 913 );
796 914 assert!(config.get_u32(b"section2", b"not-count").is_err());
797 915 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
798 916 }
799 917
800 918 #[test]
801 919 fn test_default_parse() {
802 920 let config = Config::load_from_explicit_sources(vec![])
803 921 .expect("expected valid config");
804 922 let ret = config.get_byte_size(b"cmdserver", b"max-log-size");
805 923 assert!(ret.is_ok(), "{:?}", ret);
806 924
807 925 let ret = config.get_byte_size(b"ui", b"formatted");
808 926 assert!(ret.unwrap().is_none());
809 927 }
810 928 }
General Comments 0
You need to be logged in to leave comments. Login now