just playing with tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at tmp-tutorial 960 lines 31 kB view raw
1// Copyright 2022 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::borrow::Cow; 16use std::collections::{HashMap, HashSet}; 17use std::path::{Path, PathBuf}; 18use std::process::Command; 19use std::{env, fmt}; 20 21use config::Source; 22use itertools::Itertools; 23use jj_lib::settings::ConfigResultExt as _; 24use thiserror::Error; 25use tracing::instrument; 26 27#[derive(Error, Debug)] 28pub enum ConfigError { 29 #[error(transparent)] 30 ConfigReadError(#[from] config::ConfigError), 31 #[error("Both {0} and {1} exist. Please consolidate your configs in one of them.")] 32 AmbiguousSource(PathBuf, PathBuf), 33 #[error(transparent)] 34 ConfigCreateError(#[from] std::io::Error), 35} 36 37#[derive(Clone, Debug, PartialEq, Eq)] 38pub enum ConfigSource { 39 Default, 40 Env, 41 // TODO: Track explicit file paths, especially for when user config is a dir. 42 User, 43 Repo, 44 CommandArg, 45} 46 47#[derive(Clone, Debug, PartialEq)] 48pub struct AnnotatedValue { 49 pub path: Vec<String>, 50 pub value: config::Value, 51 pub source: ConfigSource, 52 pub is_overridden: bool, 53} 54 55/// Set of configs which can be merged as needed. 56/// 57/// Sources from the lowest precedence: 58/// 1. Default 59/// 2. Base environment variables 60/// 3. User config `~/.jjconfig.toml` or `$JJ_CONFIG` 61/// 4. Repo config `.jj/repo/config.toml` 62/// 5. TODO: Workspace config `.jj/config.toml` 63/// 6. Override environment variables 64/// 7. Command-line arguments `--config-toml` 65#[derive(Clone, Debug)] 66pub struct LayeredConfigs { 67 default: config::Config, 68 env_base: config::Config, 69 user: Option<config::Config>, 70 repo: Option<config::Config>, 71 env_overrides: config::Config, 72 arg_overrides: Option<config::Config>, 73} 74 75impl LayeredConfigs { 76 /// Initializes configs with infallible sources. 77 pub fn from_environment() -> Self { 78 LayeredConfigs { 79 default: default_config(), 80 env_base: env_base(), 81 user: None, 82 repo: None, 83 env_overrides: env_overrides(), 84 arg_overrides: None, 85 } 86 } 87 88 #[instrument] 89 pub fn read_user_config(&mut self) -> Result<(), ConfigError> { 90 self.user = existing_config_path()? 91 .map(|path| read_config_path(&path)) 92 .transpose()?; 93 Ok(()) 94 } 95 96 #[instrument] 97 pub fn read_repo_config(&mut self, repo_path: &Path) -> Result<(), ConfigError> { 98 self.repo = Some(read_config_file(&repo_path.join("config.toml"))?); 99 Ok(()) 100 } 101 102 pub fn parse_config_args(&mut self, toml_strs: &[String]) -> Result<(), ConfigError> { 103 let config = toml_strs 104 .iter() 105 .fold(config::Config::builder(), |builder, s| { 106 builder.add_source(config::File::from_str(s, config::FileFormat::Toml)) 107 }) 108 .build()?; 109 self.arg_overrides = Some(config); 110 Ok(()) 111 } 112 113 /// Creates new merged config. 114 pub fn merge(&self) -> config::Config { 115 self.sources() 116 .into_iter() 117 .map(|(_, config)| config) 118 .fold(config::Config::builder(), |builder, source| { 119 builder.add_source(source.clone()) 120 }) 121 .build() 122 .expect("loaded configs should be merged without error") 123 } 124 125 pub fn sources(&self) -> Vec<(ConfigSource, &config::Config)> { 126 let config_sources = [ 127 (ConfigSource::Default, Some(&self.default)), 128 (ConfigSource::Env, Some(&self.env_base)), 129 (ConfigSource::User, self.user.as_ref()), 130 (ConfigSource::Repo, self.repo.as_ref()), 131 (ConfigSource::Env, Some(&self.env_overrides)), 132 (ConfigSource::CommandArg, self.arg_overrides.as_ref()), 133 ]; 134 config_sources 135 .into_iter() 136 .filter_map(|(source, config)| config.map(|c| (source, c))) 137 .collect_vec() 138 } 139 140 pub fn resolved_config_values( 141 &self, 142 filter_prefix: &[&str], 143 ) -> Result<Vec<AnnotatedValue>, ConfigError> { 144 // Collect annotated values from each config. 145 let mut config_vals = vec![]; 146 147 let prefix_key = match filter_prefix { 148 &[] => None, 149 _ => Some(filter_prefix.join(".")), 150 }; 151 for (source, config) in self.sources() { 152 let top_value = match prefix_key { 153 Some(ref key) => { 154 if let Some(val) = config.get(key).optional()? { 155 val 156 } else { 157 continue; 158 } 159 } 160 None => config.collect()?.into(), 161 }; 162 let mut config_stack: Vec<(Vec<&str>, &config::Value)> = 163 vec![(filter_prefix.to_vec(), &top_value)]; 164 while let Some((path, value)) = config_stack.pop() { 165 match &value.kind { 166 config::ValueKind::Table(table) => { 167 // TODO: Remove sorting when config crate maintains deterministic ordering. 168 for (k, v) in table.iter().sorted_by_key(|(k, _)| *k).rev() { 169 let mut key_path = path.to_vec(); 170 key_path.push(k); 171 config_stack.push((key_path, v)); 172 } 173 } 174 _ => { 175 config_vals.push(AnnotatedValue { 176 path: path.iter().map(|&s| s.to_owned()).collect_vec(), 177 value: value.to_owned(), 178 source: source.to_owned(), 179 // Note: Value updated below. 180 is_overridden: false, 181 }); 182 } 183 } 184 } 185 } 186 187 // Walk through config values in reverse order and mark each overridden value as 188 // overridden. 189 let mut keys_found = HashSet::new(); 190 for val in config_vals.iter_mut().rev() { 191 val.is_overridden = !keys_found.insert(&val.path); 192 } 193 194 Ok(config_vals) 195 } 196} 197 198enum ConfigPath { 199 /// Existing config file path. 200 Existing(PathBuf), 201 /// Could not find any config file, but a new file can be created at the 202 /// specified location. 203 New(PathBuf), 204 /// Could not find any config file. 205 Unavailable, 206} 207 208impl ConfigPath { 209 fn new(path: Option<PathBuf>) -> Self { 210 match path { 211 Some(path) if path.exists() => ConfigPath::Existing(path), 212 Some(path) => ConfigPath::New(path), 213 None => ConfigPath::Unavailable, 214 } 215 } 216} 217 218/// Like std::fs::create_dir_all but creates new directories to be accessible to 219/// the user only on Unix (chmod 700). 220fn create_dir_all(path: &Path) -> std::io::Result<()> { 221 let mut dir = std::fs::DirBuilder::new(); 222 dir.recursive(true); 223 #[cfg(unix)] 224 { 225 use std::os::unix::fs::DirBuilderExt; 226 dir.mode(0o700); 227 } 228 dir.create(path) 229} 230 231fn create_config_file(path: &Path) -> std::io::Result<std::fs::File> { 232 if let Some(parent) = path.parent() { 233 create_dir_all(parent)?; 234 } 235 // TODO: Use File::create_new once stabilized. 236 std::fs::OpenOptions::new() 237 .read(true) 238 .write(true) 239 .create_new(true) 240 .open(path) 241} 242 243// The struct exists so that we can mock certain global values in unit tests. 244#[derive(Clone, Default, Debug)] 245struct ConfigEnv { 246 config_dir: Option<PathBuf>, 247 home_dir: Option<PathBuf>, 248 jj_config: Option<String>, 249} 250 251impl ConfigEnv { 252 fn new() -> Self { 253 ConfigEnv { 254 config_dir: dirs::config_dir(), 255 home_dir: dirs::home_dir(), 256 jj_config: env::var("JJ_CONFIG").ok(), 257 } 258 } 259 260 fn config_path(self) -> Result<ConfigPath, ConfigError> { 261 if let Some(path) = self.jj_config { 262 // TODO: We should probably support colon-separated (std::env::split_paths) 263 return Ok(ConfigPath::new(Some(PathBuf::from(path)))); 264 } 265 // TODO: Should we drop the final `/config.toml` and read all files in the 266 // directory? 267 let platform_config_path = ConfigPath::new(self.config_dir.map(|mut config_dir| { 268 config_dir.push("jj"); 269 config_dir.push("config.toml"); 270 config_dir 271 })); 272 let home_config_path = ConfigPath::new(self.home_dir.map(|mut home_dir| { 273 home_dir.push(".jjconfig.toml"); 274 home_dir 275 })); 276 use ConfigPath::*; 277 match (platform_config_path, home_config_path) { 278 (Existing(platform_config_path), Existing(home_config_path)) => Err( 279 ConfigError::AmbiguousSource(platform_config_path, home_config_path), 280 ), 281 (Existing(path), _) | (_, Existing(path)) => Ok(Existing(path)), 282 (New(path), _) | (_, New(path)) => Ok(New(path)), 283 (Unavailable, Unavailable) => Ok(Unavailable), 284 } 285 } 286 287 fn existing_config_path(self) -> Result<Option<PathBuf>, ConfigError> { 288 match self.config_path()? { 289 ConfigPath::Existing(path) => Ok(Some(path)), 290 _ => Ok(None), 291 } 292 } 293 294 fn new_config_path(self) -> Result<Option<PathBuf>, ConfigError> { 295 match self.config_path()? { 296 ConfigPath::Existing(path) => Ok(Some(path)), 297 ConfigPath::New(path) => { 298 create_config_file(&path)?; 299 Ok(Some(path)) 300 } 301 ConfigPath::Unavailable => Ok(None), 302 } 303 } 304} 305 306pub fn existing_config_path() -> Result<Option<PathBuf>, ConfigError> { 307 ConfigEnv::new().existing_config_path() 308} 309 310/// Returns a path to the user-specific config file. If no config file is found, 311/// tries to guess a reasonable new location for it. If a path to a new config 312/// file is returned, the parent directory may be created as a result of this 313/// call. 314pub fn new_config_path() -> Result<Option<PathBuf>, ConfigError> { 315 ConfigEnv::new().new_config_path() 316} 317 318/// Environment variables that should be overridden by config values 319fn env_base() -> config::Config { 320 let mut builder = config::Config::builder(); 321 if env::var("NO_COLOR").is_ok() { 322 // "User-level configuration files and per-instance command-line arguments 323 // should override $NO_COLOR." https://no-color.org/ 324 builder = builder.set_override("ui.color", "never").unwrap(); 325 } 326 if let Ok(value) = env::var("PAGER") { 327 builder = builder.set_override("ui.pager", value).unwrap(); 328 } 329 if let Ok(value) = env::var("VISUAL") { 330 builder = builder.set_override("ui.editor", value).unwrap(); 331 } else if let Ok(value) = env::var("EDITOR") { 332 builder = builder.set_override("ui.editor", value).unwrap(); 333 } 334 335 builder.build().unwrap() 336} 337 338pub fn default_config() -> config::Config { 339 // Syntax error in default config isn't a user error. That's why defaults are 340 // loaded by separate builder. 341 macro_rules! from_toml { 342 ($file:literal) => { 343 config::File::from_str(include_str!($file), config::FileFormat::Toml) 344 }; 345 } 346 config::Config::builder() 347 .add_source(from_toml!("config/colors.toml")) 348 .add_source(from_toml!("config/merge_tools.toml")) 349 .add_source(from_toml!("config/misc.toml")) 350 .add_source(from_toml!("config/templates.toml")) 351 .build() 352 .unwrap() 353} 354 355/// Environment variables that override config values 356fn env_overrides() -> config::Config { 357 let mut builder = config::Config::builder(); 358 if let Ok(value) = env::var("JJ_USER") { 359 builder = builder.set_override("user.name", value).unwrap(); 360 } 361 if let Ok(value) = env::var("JJ_EMAIL") { 362 builder = builder.set_override("user.email", value).unwrap(); 363 } 364 if let Ok(value) = env::var("JJ_TIMESTAMP") { 365 builder = builder 366 .set_override("debug.commit-timestamp", value) 367 .unwrap(); 368 } 369 if let Ok(value) = env::var("JJ_RANDOMNESS_SEED") { 370 builder = builder 371 .set_override("debug.randomness-seed", value) 372 .unwrap(); 373 } 374 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") { 375 builder = builder 376 .set_override("debug.operation-timestamp", value) 377 .unwrap(); 378 } 379 if let Ok(value) = env::var("JJ_OP_HOSTNAME") { 380 builder = builder.set_override("operation.hostname", value).unwrap(); 381 } 382 if let Ok(value) = env::var("JJ_OP_USERNAME") { 383 builder = builder.set_override("operation.username", value).unwrap(); 384 } 385 if let Ok(value) = env::var("JJ_EDITOR") { 386 builder = builder.set_override("ui.editor", value).unwrap(); 387 } 388 builder.build().unwrap() 389} 390 391fn read_config_file(path: &Path) -> Result<config::Config, config::ConfigError> { 392 config::Config::builder() 393 .add_source( 394 config::File::from(path) 395 .required(false) 396 .format(config::FileFormat::Toml), 397 ) 398 .build() 399} 400 401fn read_config_path(config_path: &Path) -> Result<config::Config, config::ConfigError> { 402 let mut files = vec![]; 403 if config_path.is_dir() { 404 if let Ok(read_dir) = config_path.read_dir() { 405 // TODO: Walk the directory recursively? 406 for dir_entry in read_dir.flatten() { 407 let path = dir_entry.path(); 408 if path.is_file() { 409 files.push(path); 410 } 411 } 412 } 413 files.sort(); 414 } else { 415 files.push(config_path.to_owned()); 416 } 417 418 files 419 .iter() 420 .fold(config::Config::builder(), |builder, path| { 421 // TODO: Accept other formats and/or accept only certain file extensions? 422 builder.add_source( 423 config::File::from(path.as_ref()) 424 .required(false) 425 .format(config::FileFormat::Toml), 426 ) 427 }) 428 .build() 429} 430 431/// Command name and arguments specified by config. 432#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] 433#[serde(untagged)] 434pub enum CommandNameAndArgs { 435 String(String), 436 Vec(NonEmptyCommandArgsVec), 437 Structured { 438 env: HashMap<String, String>, 439 command: NonEmptyCommandArgsVec, 440 }, 441} 442 443impl CommandNameAndArgs { 444 /// Returns command name and arguments. 445 /// 446 /// The command name may be an empty string (as well as each argument.) 447 pub fn split_name_and_args(&self) -> (Cow<str>, Cow<[String]>) { 448 match self { 449 CommandNameAndArgs::String(s) => { 450 // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes) 451 let mut args = s.split(' ').map(|s| s.to_owned()); 452 (args.next().unwrap().into(), args.collect()) 453 } 454 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(a)) => { 455 (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])) 456 } 457 CommandNameAndArgs::Structured { 458 env: _, 459 command: cmd, 460 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])), 461 } 462 } 463 464 /// Returns process builder configured with this. 465 pub fn to_command(&self) -> Command { 466 let (name, args) = self.split_name_and_args(); 467 let mut cmd = Command::new(name.as_ref()); 468 if let CommandNameAndArgs::Structured { env, .. } = self { 469 cmd.envs(env); 470 } 471 cmd.args(args.as_ref()); 472 cmd 473 } 474} 475 476impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs { 477 fn from(s: &T) -> Self { 478 CommandNameAndArgs::String(s.as_ref().to_owned()) 479 } 480} 481 482impl fmt::Display for CommandNameAndArgs { 483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 484 match self { 485 CommandNameAndArgs::String(s) => write!(f, "{s}"), 486 // TODO: format with shell escapes 487 CommandNameAndArgs::Vec(a) => write!(f, "{}", a.0.join(" ")), 488 CommandNameAndArgs::Structured { env, command } => { 489 for (k, v) in env.iter() { 490 write!(f, "{k}={v} ")?; 491 } 492 write!(f, "{}", command.0.join(" ")) 493 } 494 } 495 } 496} 497 498/// Wrapper to reject an array without command name. 499// Based on https://github.com/serde-rs/serde/issues/939 500#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)] 501#[serde(try_from = "Vec<String>")] 502pub struct NonEmptyCommandArgsVec(Vec<String>); 503 504impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec { 505 type Error = &'static str; 506 507 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> { 508 if args.is_empty() { 509 Err("command arguments should not be empty") 510 } else { 511 Ok(NonEmptyCommandArgsVec(args)) 512 } 513 } 514} 515 516#[cfg(test)] 517mod tests { 518 use maplit::hashmap; 519 520 use super::*; 521 522 #[test] 523 fn test_command_args() { 524 let config = config::Config::builder() 525 .set_override("empty_array", Vec::<String>::new()) 526 .unwrap() 527 .set_override("empty_string", "") 528 .unwrap() 529 .set_override("array", vec!["emacs", "-nw"]) 530 .unwrap() 531 .set_override("string", "emacs -nw") 532 .unwrap() 533 .set_override("structured.env.KEY1", "value1") 534 .unwrap() 535 .set_override("structured.env.KEY2", "value2") 536 .unwrap() 537 .set_override("structured.command", vec!["emacs", "-nw"]) 538 .unwrap() 539 .build() 540 .unwrap(); 541 542 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err()); 543 544 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap(); 545 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned())); 546 let (name, args) = command_args.split_name_and_args(); 547 assert_eq!(name, ""); 548 assert!(args.is_empty()); 549 550 let command_args: CommandNameAndArgs = config.get("array").unwrap(); 551 assert_eq!( 552 command_args, 553 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec( 554 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec() 555 )) 556 ); 557 let (name, args) = command_args.split_name_and_args(); 558 assert_eq!(name, "emacs"); 559 assert_eq!(args, ["-nw"].as_ref()); 560 561 let command_args: CommandNameAndArgs = config.get("string").unwrap(); 562 assert_eq!( 563 command_args, 564 CommandNameAndArgs::String("emacs -nw".to_owned()) 565 ); 566 let (name, args) = command_args.split_name_and_args(); 567 assert_eq!(name, "emacs"); 568 assert_eq!(args, ["-nw"].as_ref()); 569 570 let command_args: CommandNameAndArgs = config.get("structured").unwrap(); 571 assert_eq!( 572 command_args, 573 CommandNameAndArgs::Structured { 574 env: hashmap! { 575 "KEY1".to_string() => "value1".to_string(), 576 "KEY2".to_string() => "value2".to_string(), 577 }, 578 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec()) 579 } 580 ); 581 let (name, args) = command_args.split_name_and_args(); 582 assert_eq!(name, "emacs"); 583 assert_eq!(args, ["-nw"].as_ref()); 584 } 585 586 #[test] 587 fn test_layered_configs_resolved_config_values_empty() { 588 let empty_config = config::Config::default(); 589 let layered_configs = LayeredConfigs { 590 default: empty_config.to_owned(), 591 env_base: empty_config.to_owned(), 592 user: None, 593 repo: None, 594 env_overrides: empty_config, 595 arg_overrides: None, 596 }; 597 assert_eq!(layered_configs.resolved_config_values(&[]).unwrap(), []); 598 } 599 600 #[test] 601 fn test_layered_configs_resolved_config_values_single_key() { 602 let empty_config = config::Config::default(); 603 let env_base_config = config::Config::builder() 604 .set_override("user.name", "base-user-name") 605 .unwrap() 606 .set_override("user.email", "base@user.email") 607 .unwrap() 608 .build() 609 .unwrap(); 610 let repo_config = config::Config::builder() 611 .set_override("user.email", "repo@user.email") 612 .unwrap() 613 .build() 614 .unwrap(); 615 let layered_configs = LayeredConfigs { 616 default: empty_config.to_owned(), 617 env_base: env_base_config, 618 user: None, 619 repo: Some(repo_config), 620 env_overrides: empty_config, 621 arg_overrides: None, 622 }; 623 // Note: "email" is alphabetized, before "name" from same layer. 624 insta::assert_debug_snapshot!( 625 layered_configs.resolved_config_values(&[]).unwrap(), 626 @r###" 627 [ 628 AnnotatedValue { 629 path: [ 630 "user", 631 "email", 632 ], 633 value: Value { 634 origin: None, 635 kind: String( 636 "base@user.email", 637 ), 638 }, 639 source: Env, 640 is_overridden: true, 641 }, 642 AnnotatedValue { 643 path: [ 644 "user", 645 "name", 646 ], 647 value: Value { 648 origin: None, 649 kind: String( 650 "base-user-name", 651 ), 652 }, 653 source: Env, 654 is_overridden: false, 655 }, 656 AnnotatedValue { 657 path: [ 658 "user", 659 "email", 660 ], 661 value: Value { 662 origin: None, 663 kind: String( 664 "repo@user.email", 665 ), 666 }, 667 source: Repo, 668 is_overridden: false, 669 }, 670 ] 671 "### 672 ); 673 } 674 675 #[test] 676 fn test_layered_configs_resolved_config_values_filter_path() { 677 let empty_config = config::Config::default(); 678 let user_config = config::Config::builder() 679 .set_override("test-table1.foo", "user-FOO") 680 .unwrap() 681 .set_override("test-table2.bar", "user-BAR") 682 .unwrap() 683 .build() 684 .unwrap(); 685 let repo_config = config::Config::builder() 686 .set_override("test-table1.bar", "repo-BAR") 687 .unwrap() 688 .build() 689 .unwrap(); 690 let layered_configs = LayeredConfigs { 691 default: empty_config.to_owned(), 692 env_base: empty_config.to_owned(), 693 user: Some(user_config), 694 repo: Some(repo_config), 695 env_overrides: empty_config, 696 arg_overrides: None, 697 }; 698 insta::assert_debug_snapshot!( 699 layered_configs 700 .resolved_config_values(&["test-table1"]) 701 .unwrap(), 702 @r###" 703 [ 704 AnnotatedValue { 705 path: [ 706 "test-table1", 707 "foo", 708 ], 709 value: Value { 710 origin: None, 711 kind: String( 712 "user-FOO", 713 ), 714 }, 715 source: User, 716 is_overridden: false, 717 }, 718 AnnotatedValue { 719 path: [ 720 "test-table1", 721 "bar", 722 ], 723 value: Value { 724 origin: None, 725 kind: String( 726 "repo-BAR", 727 ), 728 }, 729 source: Repo, 730 is_overridden: false, 731 }, 732 ] 733 "### 734 ); 735 } 736 737 #[test] 738 fn test_config_path_home_dir_existing() -> anyhow::Result<()> { 739 TestCase { 740 files: vec!["home/.jjconfig.toml"], 741 cfg: ConfigEnv { 742 home_dir: Some("home".into()), 743 ..Default::default() 744 }, 745 want: Want::ExistingAndNew("home/.jjconfig.toml"), 746 } 747 .run() 748 } 749 750 #[test] 751 fn test_config_path_home_dir_new() -> anyhow::Result<()> { 752 TestCase { 753 files: vec![], 754 cfg: ConfigEnv { 755 home_dir: Some("home".into()), 756 ..Default::default() 757 }, 758 want: Want::New("home/.jjconfig.toml"), 759 } 760 .run() 761 } 762 763 #[test] 764 fn test_config_path_config_dir_existing() -> anyhow::Result<()> { 765 TestCase { 766 files: vec!["config/jj/config.toml"], 767 cfg: ConfigEnv { 768 config_dir: Some("config".into()), 769 ..Default::default() 770 }, 771 want: Want::ExistingAndNew("config/jj/config.toml"), 772 } 773 .run() 774 } 775 776 #[test] 777 fn test_config_path_config_dir_new() -> anyhow::Result<()> { 778 TestCase { 779 files: vec![], 780 cfg: ConfigEnv { 781 config_dir: Some("config".into()), 782 ..Default::default() 783 }, 784 want: Want::New("config/jj/config.toml"), 785 } 786 .run() 787 } 788 789 #[test] 790 fn test_config_path_new_prefer_config_dir() -> anyhow::Result<()> { 791 TestCase { 792 files: vec![], 793 cfg: ConfigEnv { 794 config_dir: Some("config".into()), 795 home_dir: Some("home".into()), 796 ..Default::default() 797 }, 798 want: Want::New("config/jj/config.toml"), 799 } 800 .run() 801 } 802 803 #[test] 804 fn test_config_path_jj_config_existing() -> anyhow::Result<()> { 805 TestCase { 806 files: vec!["custom.toml"], 807 cfg: ConfigEnv { 808 jj_config: Some("custom.toml".into()), 809 ..Default::default() 810 }, 811 want: Want::ExistingAndNew("custom.toml"), 812 } 813 .run() 814 } 815 816 #[test] 817 fn test_config_path_jj_config_new() -> anyhow::Result<()> { 818 TestCase { 819 files: vec![], 820 cfg: ConfigEnv { 821 jj_config: Some("custom.toml".into()), 822 ..Default::default() 823 }, 824 want: Want::New("custom.toml"), 825 } 826 .run() 827 } 828 829 #[test] 830 fn test_config_path_config_pick_config_dir() -> anyhow::Result<()> { 831 TestCase { 832 files: vec!["config/jj/config.toml"], 833 cfg: ConfigEnv { 834 home_dir: Some("home".into()), 835 config_dir: Some("config".into()), 836 ..Default::default() 837 }, 838 want: Want::ExistingAndNew("config/jj/config.toml"), 839 } 840 .run() 841 } 842 843 #[test] 844 fn test_config_path_config_pick_home_dir() -> anyhow::Result<()> { 845 TestCase { 846 files: vec!["home/.jjconfig.toml"], 847 cfg: ConfigEnv { 848 home_dir: Some("home".into()), 849 config_dir: Some("config".into()), 850 ..Default::default() 851 }, 852 want: Want::ExistingAndNew("home/.jjconfig.toml"), 853 } 854 .run() 855 } 856 857 #[test] 858 fn test_config_path_none() -> anyhow::Result<()> { 859 TestCase { 860 files: vec![], 861 cfg: Default::default(), 862 want: Want::None, 863 } 864 .run() 865 } 866 867 #[test] 868 fn test_config_path_ambiguous() -> anyhow::Result<()> { 869 let tmp = setup_config_fs(&vec!["home/.jjconfig.toml", "config/jj/config.toml"])?; 870 let cfg = ConfigEnv { 871 home_dir: Some(tmp.path().join("home")), 872 config_dir: Some(tmp.path().join("config")), 873 ..Default::default() 874 }; 875 use assert_matches::assert_matches; 876 assert_matches!( 877 cfg.clone().existing_config_path(), 878 Err(ConfigError::AmbiguousSource(_, _)) 879 ); 880 assert_matches!( 881 cfg.clone().new_config_path(), 882 Err(ConfigError::AmbiguousSource(_, _)) 883 ); 884 Ok(()) 885 } 886 887 fn setup_config_fs(files: &Vec<&'static str>) -> anyhow::Result<tempfile::TempDir> { 888 let tmp = testutils::new_temp_dir(); 889 for file in files { 890 let path = tmp.path().join(file); 891 if let Some(parent) = path.parent() { 892 std::fs::create_dir_all(parent)?; 893 } 894 std::fs::File::create(path)?; 895 } 896 Ok(tmp) 897 } 898 899 enum Want { 900 None, 901 New(&'static str), 902 ExistingAndNew(&'static str), 903 } 904 905 struct TestCase { 906 files: Vec<&'static str>, 907 cfg: ConfigEnv, 908 want: Want, 909 } 910 911 impl TestCase { 912 fn config(&self, root: &Path) -> ConfigEnv { 913 ConfigEnv { 914 config_dir: self.cfg.config_dir.as_ref().map(|p| root.join(p)), 915 home_dir: self.cfg.home_dir.as_ref().map(|p| root.join(p)), 916 jj_config: self 917 .cfg 918 .jj_config 919 .as_ref() 920 .map(|p| root.join(p).to_str().unwrap().to_string()), 921 } 922 } 923 924 fn run(self) -> anyhow::Result<()> { 925 use anyhow::anyhow; 926 let tmp = setup_config_fs(&self.files)?; 927 928 let check = |name, f: fn(ConfigEnv) -> Result<_, _>, want: Option<_>| { 929 let got = f(self.config(tmp.path())).map_err(|e| anyhow!("{name}: {e}"))?; 930 let want = want.map(|p| tmp.path().join(p)); 931 if got != want { 932 Err(anyhow!("{name}: got {got:?}, want {want:?}")) 933 } else { 934 Ok(got) 935 } 936 }; 937 938 let (want_existing, want_new) = match self.want { 939 Want::None => (None, None), 940 Want::New(want) => (None, Some(want)), 941 Want::ExistingAndNew(want) => (Some(want.clone()), Some(want)), 942 }; 943 944 check( 945 "existing_config_path", 946 ConfigEnv::existing_config_path, 947 want_existing, 948 )?; 949 let got = check("new_config_path", ConfigEnv::new_config_path, want_new)?; 950 if let Some(path) = got { 951 if !Path::new(&path).is_file() { 952 return Err(anyhow!( 953 "new_config_path returned {path:?} which is not a file" 954 )); 955 } 956 } 957 Ok(()) 958 } 959 } 960}