just playing with tangled
at ig/vimdiffwarn 1567 lines 55 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::BTreeSet; 17use std::collections::HashMap; 18use std::env; 19use std::env::split_paths; 20use std::fmt; 21use std::path::Path; 22use std::path::PathBuf; 23use std::process::Command; 24 25use itertools::Itertools as _; 26use jj_lib::config::ConfigFile; 27use jj_lib::config::ConfigGetError; 28use jj_lib::config::ConfigLayer; 29use jj_lib::config::ConfigLoadError; 30use jj_lib::config::ConfigMigrationRule; 31use jj_lib::config::ConfigNamePathBuf; 32use jj_lib::config::ConfigResolutionContext; 33use jj_lib::config::ConfigSource; 34use jj_lib::config::ConfigValue; 35use jj_lib::config::StackedConfig; 36use regex::Captures; 37use regex::Regex; 38use tracing::instrument; 39 40use crate::command_error::config_error; 41use crate::command_error::config_error_with_message; 42use crate::command_error::CommandError; 43 44// TODO(#879): Consider generating entire schema dynamically vs. static file. 45pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json"); 46 47/// Parses a TOML value expression. Interprets the given value as string if it 48/// can't be parsed and doesn't look like a TOML expression. 49pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> { 50 match value_str.parse() { 51 Ok(value) => Ok(value), 52 Err(_) if is_bare_string(value_str) => Ok(value_str.into()), 53 Err(err) => Err(err), 54 } 55} 56 57fn is_bare_string(value_str: &str) -> bool { 58 // leading whitespace isn't ignored when parsing TOML value expression, but 59 // "\n[]" doesn't look like a bare string. 60 let trimmed = value_str.trim_ascii().as_bytes(); 61 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) { 62 // string, array, or table constructs? 63 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}') 64 } else { 65 true // empty or whitespace only 66 } 67} 68 69/// Configuration variable with its source information. 70#[derive(Clone, Debug)] 71pub struct AnnotatedValue { 72 /// Dotted name path to the configuration variable. 73 pub name: ConfigNamePathBuf, 74 /// Configuration value. 75 pub value: ConfigValue, 76 /// Source of the configuration value. 77 pub source: ConfigSource, 78 /// Path to the source file, if available. 79 pub path: Option<PathBuf>, 80 /// True if this value is overridden in higher precedence layers. 81 pub is_overridden: bool, 82} 83 84/// Collects values under the given `filter_prefix` name recursively, from all 85/// layers. 86pub fn resolved_config_values( 87 stacked_config: &StackedConfig, 88 filter_prefix: &ConfigNamePathBuf, 89) -> Vec<AnnotatedValue> { 90 // Collect annotated values in reverse order and mark each value shadowed by 91 // value or table in upper layers. 92 let mut config_vals = vec![]; 93 let mut upper_value_names = BTreeSet::new(); 94 for layer in stacked_config.layers().iter().rev() { 95 let top_item = match layer.look_up_item(filter_prefix) { 96 Ok(Some(item)) => item, 97 Ok(None) => continue, // parent is a table, but no value found 98 Err(_) => { 99 // parent is not a table, shadows lower layers 100 upper_value_names.insert(filter_prefix.clone()); 101 continue; 102 } 103 }; 104 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)]; 105 while let Some((name, item, is_parent_overridden)) = config_stack.pop() { 106 // Cannot retain inline table formatting because inner values may be 107 // overridden independently. 108 if let Some(table) = item.as_table_like() { 109 // current table and children may be shadowed by value in upper layer 110 let is_overridden = is_parent_overridden || upper_value_names.contains(&name); 111 for (k, v) in table.iter() { 112 let mut sub_name = name.clone(); 113 sub_name.push(k); 114 config_stack.push((sub_name, v, is_overridden)); // in reverse order 115 } 116 } else { 117 // current value may be shadowed by value or table in upper layer 118 let maybe_child = upper_value_names 119 .range(&name..) 120 .next() 121 .filter(|next| next.starts_with(&name)); 122 let is_overridden = is_parent_overridden || maybe_child.is_some(); 123 if maybe_child != Some(&name) { 124 upper_value_names.insert(name.clone()); 125 } 126 let value = item 127 .clone() 128 .into_value() 129 .expect("Item::None should not exist in table"); 130 config_vals.push(AnnotatedValue { 131 name, 132 value, 133 source: layer.source, 134 path: layer.path.clone(), 135 is_overridden, 136 }); 137 } 138 } 139 } 140 config_vals.reverse(); 141 config_vals 142} 143 144/// Newtype for unprocessed (or unresolved) [`StackedConfig`]. 145/// 146/// This doesn't provide any strict guarantee about the underlying config 147/// object. It just requires an explicit cast to access to the config object. 148#[derive(Clone, Debug)] 149pub struct RawConfig(StackedConfig); 150 151impl AsRef<StackedConfig> for RawConfig { 152 fn as_ref(&self) -> &StackedConfig { 153 &self.0 154 } 155} 156 157impl AsMut<StackedConfig> for RawConfig { 158 fn as_mut(&mut self) -> &mut StackedConfig { 159 &mut self.0 160 } 161} 162 163#[derive(Clone, Debug)] 164enum ConfigPath { 165 /// Existing config file path. 166 Existing(PathBuf), 167 /// Could not find any config file, but a new file can be created at the 168 /// specified location. 169 New(PathBuf), 170} 171 172impl ConfigPath { 173 fn new(path: PathBuf) -> Self { 174 if path.exists() { 175 ConfigPath::Existing(path) 176 } else { 177 ConfigPath::New(path) 178 } 179 } 180 181 fn as_path(&self) -> &Path { 182 match self { 183 ConfigPath::Existing(path) | ConfigPath::New(path) => path, 184 } 185 } 186} 187 188/// Like std::fs::create_dir_all but creates new directories to be accessible to 189/// the user only on Unix (chmod 700). 190fn create_dir_all(path: &Path) -> std::io::Result<()> { 191 let mut dir = std::fs::DirBuilder::new(); 192 dir.recursive(true); 193 #[cfg(unix)] 194 { 195 use std::os::unix::fs::DirBuilderExt as _; 196 dir.mode(0o700); 197 } 198 dir.create(path) 199} 200 201// The struct exists so that we can mock certain global values in unit tests. 202#[derive(Clone, Default, Debug)] 203struct UnresolvedConfigEnv { 204 config_dir: Option<PathBuf>, 205 home_dir: Option<PathBuf>, 206 jj_config: Option<String>, 207} 208 209impl UnresolvedConfigEnv { 210 fn resolve(self) -> Vec<ConfigPath> { 211 if let Some(paths) = self.jj_config { 212 return split_paths(&paths) 213 .filter(|path| !path.as_os_str().is_empty()) 214 .map(ConfigPath::new) 215 .collect(); 216 } 217 let mut paths = vec![]; 218 let home_config_path = self.home_dir.map(|mut home_dir| { 219 home_dir.push(".jjconfig.toml"); 220 ConfigPath::new(home_dir) 221 }); 222 let platform_config_path = self.config_dir.clone().map(|mut config_dir| { 223 config_dir.push("jj"); 224 config_dir.push("config.toml"); 225 ConfigPath::new(config_dir) 226 }); 227 let platform_config_dir = self.config_dir.map(|mut config_dir| { 228 config_dir.push("jj"); 229 config_dir.push("conf.d"); 230 ConfigPath::new(config_dir) 231 }); 232 use ConfigPath::*; 233 if let Some(path @ Existing(_)) = home_config_path { 234 paths.push(path); 235 } else if let (Some(path @ New(_)), None) = (home_config_path, &platform_config_path) { 236 paths.push(path); 237 } 238 // This should be the default config created if there's 239 // no user config and `jj config edit` is executed. 240 if let Some(path) = platform_config_path { 241 paths.push(path); 242 } 243 if let Some(path @ Existing(_)) = platform_config_dir { 244 paths.push(path); 245 } 246 paths 247 } 248} 249 250#[derive(Clone, Debug)] 251pub struct ConfigEnv { 252 home_dir: Option<PathBuf>, 253 repo_path: Option<PathBuf>, 254 user_config_paths: Vec<ConfigPath>, 255 repo_config_path: Option<ConfigPath>, 256 command: Option<String>, 257} 258 259impl ConfigEnv { 260 /// Initializes configuration loader based on environment variables. 261 pub fn from_environment() -> Self { 262 // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might 263 // point to symlink. 264 let home_dir = dirs::home_dir().map(|path| dunce::canonicalize(&path).unwrap_or(path)); 265 let env = UnresolvedConfigEnv { 266 config_dir: dirs::config_dir(), 267 home_dir: home_dir.clone(), 268 jj_config: env::var("JJ_CONFIG").ok(), 269 }; 270 ConfigEnv { 271 home_dir, 272 repo_path: None, 273 user_config_paths: env.resolve(), 274 repo_config_path: None, 275 command: None, 276 } 277 } 278 279 pub fn set_command_name(&mut self, command: String) { 280 self.command = Some(command); 281 } 282 283 /// Returns the paths to the user-specific config files or directories. 284 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> { 285 self.user_config_paths.iter().map(|p| p.as_path()) 286 } 287 288 /// Returns the paths to the existing user-specific config files or 289 /// directories. 290 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> { 291 self.user_config_paths.iter().filter_map(|p| match p { 292 ConfigPath::Existing(path) => Some(path.as_path()), 293 _ => None, 294 }) 295 } 296 297 /// Returns user configuration files for modification. Instantiates one if 298 /// `config` has no user configuration layers. 299 /// 300 /// The parent directory for the new file may be created by this function. 301 /// If the user configuration path is unknown, this function returns an 302 /// empty `Vec`. 303 pub fn user_config_files( 304 &self, 305 config: &RawConfig, 306 ) -> Result<Vec<ConfigFile>, ConfigLoadError> { 307 config_files_for(config, ConfigSource::User, || self.new_user_config_file()) 308 } 309 310 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> { 311 self.user_config_paths() 312 .next() 313 .map(|path| { 314 // No need to propagate io::Error here. If the directory 315 // couldn't be created, file.save() would fail later. 316 if let Some(dir) = path.parent() { 317 create_dir_all(dir).ok(); 318 } 319 // The path doesn't usually exist, but we shouldn't overwrite it 320 // with an empty config if it did exist. 321 ConfigFile::load_or_empty(ConfigSource::User, path) 322 }) 323 .transpose() 324 } 325 326 /// Loads user-specific config files into the given `config`. The old 327 /// user-config layers will be replaced if any. 328 #[instrument] 329 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> { 330 config.as_mut().remove_layers(ConfigSource::User); 331 for path in self.existing_user_config_paths() { 332 if path.is_dir() { 333 config.as_mut().load_dir(ConfigSource::User, path)?; 334 } else { 335 config.as_mut().load_file(ConfigSource::User, path)?; 336 } 337 } 338 Ok(()) 339 } 340 341 /// Sets the directory where repo-specific config file is stored. The path 342 /// is usually `.jj/repo`. 343 pub fn reset_repo_path(&mut self, path: &Path) { 344 self.repo_path = Some(path.to_owned()); 345 self.repo_config_path = Some(ConfigPath::new(path.join("config.toml"))); 346 } 347 348 /// Returns a path to the repo-specific config file. 349 pub fn repo_config_path(&self) -> Option<&Path> { 350 self.repo_config_path.as_ref().map(ConfigPath::as_path) 351 } 352 353 /// Returns a path to the existing repo-specific config file. 354 fn existing_repo_config_path(&self) -> Option<&Path> { 355 match &self.repo_config_path { 356 Some(ConfigPath::Existing(path)) => Some(path), 357 _ => None, 358 } 359 } 360 361 /// Returns repo configuration files for modification. Instantiates one if 362 /// `config` has no repo configuration layers. 363 /// 364 /// If the repo path is unknown, this function returns an empty `Vec`. Since 365 /// the repo config path cannot be a directory, the returned `Vec` should 366 /// have at most one config file. 367 pub fn repo_config_files( 368 &self, 369 config: &RawConfig, 370 ) -> Result<Vec<ConfigFile>, ConfigLoadError> { 371 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file()) 372 } 373 374 fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> { 375 self.repo_config_path() 376 // The path doesn't usually exist, but we shouldn't overwrite it 377 // with an empty config if it did exist. 378 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path)) 379 .transpose() 380 } 381 382 /// Loads repo-specific config file into the given `config`. The old 383 /// repo-config layer will be replaced if any. 384 #[instrument] 385 pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> { 386 config.as_mut().remove_layers(ConfigSource::Repo); 387 if let Some(path) = self.existing_repo_config_path() { 388 config.as_mut().load_file(ConfigSource::Repo, path)?; 389 } 390 Ok(()) 391 } 392 393 /// Resolves conditional scopes within the current environment. Returns new 394 /// resolved config. 395 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> { 396 let context = ConfigResolutionContext { 397 home_dir: self.home_dir.as_deref(), 398 repo_path: self.repo_path.as_deref(), 399 command: self.command.as_deref(), 400 }; 401 jj_lib::config::resolve(config.as_ref(), &context) 402 } 403} 404 405fn config_files_for( 406 config: &RawConfig, 407 source: ConfigSource, 408 new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>, 409) -> Result<Vec<ConfigFile>, ConfigLoadError> { 410 let mut files = config 411 .as_ref() 412 .layers_for(source) 413 .iter() 414 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok()) 415 .collect_vec(); 416 if files.is_empty() { 417 files.extend(new_file()?); 418 } 419 Ok(files) 420} 421 422/// Initializes stacked config with the given `default_layers` and infallible 423/// sources. 424/// 425/// Sources from the lowest precedence: 426/// 1. Default 427/// 2. Base environment variables 428/// 3. [User configs](https://jj-vcs.github.io/jj/latest/config/) 429/// 4. Repo config `.jj/repo/config.toml` 430/// 5. TODO: Workspace config `.jj/config.toml` 431/// 6. Override environment variables 432/// 7. Command-line arguments `--config`, `--config-toml`, `--config-file` 433/// 434/// This function sets up 1, 2, and 6. 435pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig { 436 let mut config = StackedConfig::with_defaults(); 437 config.extend_layers(default_layers); 438 config.add_layer(env_base_layer()); 439 config.add_layer(env_overrides_layer()); 440 RawConfig(config) 441} 442 443const OP_HOSTNAME: &str = "operation.hostname"; 444const OP_USERNAME: &str = "operation.username"; 445 446/// Environment variables that should be overridden by config values 447fn env_base_layer() -> ConfigLayer { 448 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase); 449 if let Ok(value) = whoami::fallible::hostname() 450 .inspect_err(|err| tracing::warn!(?err, "failed to get hostname")) 451 { 452 layer.set_value(OP_HOSTNAME, value).unwrap(); 453 } 454 if let Ok(value) = whoami::fallible::username() 455 .inspect_err(|err| tracing::warn!(?err, "failed to get username")) 456 { 457 layer.set_value(OP_USERNAME, value).unwrap(); 458 } else if let Ok(value) = env::var("USER") { 459 // On Unix, $USER is set by login(1). Use it as a fallback because 460 // getpwuid() of musl libc appears not (fully?) supporting nsswitch. 461 layer.set_value(OP_USERNAME, value).unwrap(); 462 } 463 if !env::var("NO_COLOR").unwrap_or_default().is_empty() { 464 // "User-level configuration files and per-instance command-line arguments 465 // should override $NO_COLOR." https://no-color.org/ 466 layer.set_value("ui.color", "never").unwrap(); 467 } 468 if let Ok(value) = env::var("PAGER") { 469 layer.set_value("ui.pager", value).unwrap(); 470 } 471 if let Ok(value) = env::var("VISUAL") { 472 layer.set_value("ui.editor", value).unwrap(); 473 } else if let Ok(value) = env::var("EDITOR") { 474 layer.set_value("ui.editor", value).unwrap(); 475 } 476 layer 477} 478 479pub fn default_config_layers() -> Vec<ConfigLayer> { 480 // Syntax error in default config isn't a user error. That's why defaults are 481 // loaded by separate builder. 482 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap(); 483 let mut layers = vec![ 484 parse(include_str!("config/colors.toml")), 485 parse(include_str!("config/hints.toml")), 486 parse(include_str!("config/merge_tools.toml")), 487 parse(include_str!("config/misc.toml")), 488 parse(include_str!("config/revsets.toml")), 489 parse(include_str!("config/templates.toml")), 490 ]; 491 if cfg!(unix) { 492 layers.push(parse(include_str!("config/unix.toml"))); 493 } 494 if cfg!(windows) { 495 layers.push(parse(include_str!("config/windows.toml"))); 496 } 497 layers 498} 499 500/// Environment variables that override config values 501fn env_overrides_layer() -> ConfigLayer { 502 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides); 503 if let Ok(value) = env::var("JJ_USER") { 504 layer.set_value("user.name", value).unwrap(); 505 } 506 if let Ok(value) = env::var("JJ_EMAIL") { 507 layer.set_value("user.email", value).unwrap(); 508 } 509 if let Ok(value) = env::var("JJ_TIMESTAMP") { 510 layer.set_value("debug.commit-timestamp", value).unwrap(); 511 } 512 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) { 513 layer.set_value("debug.randomness-seed", value).unwrap(); 514 } 515 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") { 516 layer.set_value("debug.operation-timestamp", value).unwrap(); 517 } 518 if let Ok(value) = env::var("JJ_OP_HOSTNAME") { 519 layer.set_value(OP_HOSTNAME, value).unwrap(); 520 } 521 if let Ok(value) = env::var("JJ_OP_USERNAME") { 522 layer.set_value(OP_USERNAME, value).unwrap(); 523 } 524 if let Ok(value) = env::var("JJ_EDITOR") { 525 layer.set_value("ui.editor", value).unwrap(); 526 } 527 layer 528} 529 530/// Configuration source/data type provided as command-line argument. 531#[derive(Clone, Copy, Debug, Eq, PartialEq)] 532pub enum ConfigArgKind { 533 /// `--config=NAME=VALUE` 534 Item, 535 /// `--config-toml=TOML` 536 Toml, 537 /// `--config-file=PATH` 538 File, 539} 540 541/// Parses `--config*` arguments. 542pub fn parse_config_args( 543 toml_strs: &[(ConfigArgKind, &str)], 544) -> Result<Vec<ConfigLayer>, CommandError> { 545 let source = ConfigSource::CommandArg; 546 let mut layers = Vec::new(); 547 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) { 548 match kind { 549 ConfigArgKind::Item => { 550 let mut layer = ConfigLayer::empty(source); 551 for (_, item) in chunk { 552 let (name, value) = parse_config_arg_item(item)?; 553 // Can fail depending on the argument order, but that 554 // wouldn't matter in practice. 555 layer.set_value(name, value).map_err(|err| { 556 config_error_with_message("--config argument cannot be set", err) 557 })?; 558 } 559 layers.push(layer); 560 } 561 ConfigArgKind::Toml => { 562 for (_, text) in chunk { 563 layers.push(ConfigLayer::parse(source, text)?); 564 } 565 } 566 ConfigArgKind::File => { 567 for (_, path) in chunk { 568 layers.push(ConfigLayer::load_from_file(source, path.into())?); 569 } 570 } 571 } 572 } 573 Ok(layers) 574} 575 576/// Parses `NAME=VALUE` string. 577fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> { 578 // split NAME=VALUE at the first parsable position 579 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'='); 580 let Some((name, value_str)) = split_candidates 581 .map(|p| (&item_str[..p], &item_str[p + 1..])) 582 .map(|(name, value)| name.parse().map(|name| (name, value))) 583 .find_or_last(Result::is_ok) 584 .transpose() 585 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))? 586 else { 587 return Err(config_error("--config must be specified as NAME=VALUE")); 588 }; 589 let value = parse_value_or_bare_string(value_str) 590 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?; 591 Ok((name, value)) 592} 593 594/// List of rules to migrate deprecated config variables. 595pub fn default_config_migrations() -> Vec<ConfigMigrationRule> { 596 vec![ 597 // TODO: Delete in jj 0.32+ 598 ConfigMigrationRule::rename_value("git.auto-local-branch", "git.auto-local-bookmark"), 599 // TODO: Delete in jj 0.33+ 600 ConfigMigrationRule::rename_update_value( 601 "signing.sign-all", 602 "signing.behavior", 603 |old_value| { 604 if old_value 605 .as_bool() 606 .ok_or("signing.sign-all expects a boolean")? 607 { 608 Ok("own".into()) 609 } else { 610 Ok("keep".into()) 611 } 612 }, 613 ), 614 // TODO: Delete in jj 0.34+ 615 ConfigMigrationRule::rename_value( 616 "core.watchman.register_snapshot_trigger", 617 "core.watchman.register-snapshot-trigger", 618 ), 619 // TODO: Delete in jj 0.34+ 620 ConfigMigrationRule::rename_value("diff.format", "ui.diff.format"), 621 // TODO: Delete with the `git.subprocess` setting. 622 #[cfg(not(feature = "git2"))] 623 ConfigMigrationRule::custom( 624 |layer| { 625 let Ok(Some(subprocess)) = layer.look_up_item("git.subprocess") else { 626 return false; 627 }; 628 subprocess.as_bool() == Some(false) 629 }, 630 |_| Ok("jj was compiled without `git.subprocess = false` support".into()), 631 ), 632 ] 633} 634 635/// Command name and arguments specified by config. 636#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] 637#[serde(untagged)] 638pub enum CommandNameAndArgs { 639 String(String), 640 Vec(NonEmptyCommandArgsVec), 641 Structured { 642 env: HashMap<String, String>, 643 command: NonEmptyCommandArgsVec, 644 }, 645} 646 647impl CommandNameAndArgs { 648 /// Returns command name without arguments. 649 pub fn split_name(&self) -> Cow<str> { 650 let (name, _) = self.split_name_and_args(); 651 name 652 } 653 654 /// Returns command name and arguments. 655 /// 656 /// The command name may be an empty string (as well as each argument.) 657 pub fn split_name_and_args(&self) -> (Cow<str>, Cow<[String]>) { 658 match self { 659 CommandNameAndArgs::String(s) => { 660 // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes) 661 let mut args = s.split(' ').map(|s| s.to_owned()); 662 (args.next().unwrap().into(), args.collect()) 663 } 664 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(a)) => { 665 (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])) 666 } 667 CommandNameAndArgs::Structured { 668 env: _, 669 command: cmd, 670 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])), 671 } 672 } 673 674 /// Returns process builder configured with this. 675 pub fn to_command(&self) -> Command { 676 let empty: HashMap<&str, &str> = HashMap::new(); 677 self.to_command_with_variables(&empty) 678 } 679 680 /// Returns process builder configured with this after interpolating 681 /// variables into the arguments. 682 pub fn to_command_with_variables<V: AsRef<str>>( 683 &self, 684 variables: &HashMap<&str, V>, 685 ) -> Command { 686 let (name, args) = self.split_name_and_args(); 687 let mut cmd = Command::new(name.as_ref()); 688 if let CommandNameAndArgs::Structured { env, .. } = self { 689 cmd.envs(env); 690 } 691 cmd.args(interpolate_variables(&args, variables)); 692 cmd 693 } 694} 695 696impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs { 697 fn from(s: &T) -> Self { 698 CommandNameAndArgs::String(s.as_ref().to_owned()) 699 } 700} 701 702impl fmt::Display for CommandNameAndArgs { 703 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 704 match self { 705 CommandNameAndArgs::String(s) => write!(f, "{s}"), 706 // TODO: format with shell escapes 707 CommandNameAndArgs::Vec(a) => write!(f, "{}", a.0.join(" ")), 708 CommandNameAndArgs::Structured { env, command } => { 709 for (k, v) in env { 710 write!(f, "{k}={v} ")?; 711 } 712 write!(f, "{}", command.0.join(" ")) 713 } 714 } 715 } 716} 717 718// Not interested in $UPPER_CASE_VARIABLES 719static VARIABLE_REGEX: once_cell::sync::Lazy<Regex> = 720 once_cell::sync::Lazy::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap()); 721 722pub fn interpolate_variables<V: AsRef<str>>( 723 args: &[String], 724 variables: &HashMap<&str, V>, 725) -> Vec<String> { 726 args.iter() 727 .map(|arg| { 728 VARIABLE_REGEX 729 .replace_all(arg, |caps: &Captures| { 730 let name = &caps[1]; 731 if let Some(subst) = variables.get(name) { 732 subst.as_ref().to_owned() 733 } else { 734 caps[0].to_owned() 735 } 736 }) 737 .into_owned() 738 }) 739 .collect() 740} 741 742/// Return all variable names found in the args, without the dollar sign 743pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> { 744 let regex = &*VARIABLE_REGEX; 745 args.iter() 746 .flat_map(|arg| regex.find_iter(arg)) 747 .map(|single_match| { 748 let s = single_match.as_str(); 749 &s[1..] 750 }) 751} 752 753/// Wrapper to reject an array without command name. 754// Based on https://github.com/serde-rs/serde/issues/939 755#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)] 756#[serde(try_from = "Vec<String>")] 757pub struct NonEmptyCommandArgsVec(Vec<String>); 758 759impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec { 760 type Error = &'static str; 761 762 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> { 763 if args.is_empty() { 764 Err("command arguments should not be empty") 765 } else { 766 Ok(NonEmptyCommandArgsVec(args)) 767 } 768 } 769} 770 771#[cfg(test)] 772mod tests { 773 use std::env::join_paths; 774 use std::fmt::Write as _; 775 776 use indoc::indoc; 777 use maplit::hashmap; 778 use test_case::test_case; 779 780 use super::*; 781 782 fn insta_settings() -> insta::Settings { 783 let mut settings = insta::Settings::clone_current(); 784 // Suppress Decor { .. } which is uninteresting 785 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }"); 786 settings 787 } 788 789 #[test] 790 fn test_parse_value_or_bare_string() { 791 let parse = |s: &str| parse_value_or_bare_string(s); 792 793 // Value in TOML syntax 794 assert_eq!(parse("true").unwrap().as_bool(), Some(true)); 795 assert_eq!(parse("42").unwrap().as_integer(), Some(42)); 796 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1)); 797 assert_eq!(parse("'a'").unwrap().as_str(), Some("a")); 798 assert!(parse("[]").unwrap().is_array()); 799 assert!(parse("{ a = 'b' }").unwrap().is_inline_table()); 800 801 // Bare string 802 assert_eq!(parse("").unwrap().as_str(), Some("")); 803 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe")); 804 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John")); 805 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay")); 806 assert_eq!( 807 parse("<foo+bar@example.org>").unwrap().as_str(), 808 Some("<foo+bar@example.org>") 809 ); 810 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa")); 811 assert_eq!(parse("all()").unwrap().as_str(), Some("all()")); 812 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*")); 813 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術")); 814 815 // Error in TOML value 816 assert!(parse("'foo").is_err()); 817 assert!(parse(r#" bar" "#).is_err()); 818 assert!(parse("[0 1]").is_err()); 819 assert!(parse("{ x = }").is_err()); 820 assert!(parse("\n { x").is_err()); 821 assert!(parse(" x ] ").is_err()); 822 assert!(parse("[table]\nkey = 'value'").is_err()); 823 } 824 825 #[test] 826 fn test_parse_config_arg_item() { 827 assert!(parse_config_arg_item("").is_err()); 828 assert!(parse_config_arg_item("a").is_err()); 829 assert!(parse_config_arg_item("=").is_err()); 830 // The value parser is sensitive to leading whitespaces, which seems 831 // good because the parsing falls back to a bare string. 832 assert!(parse_config_arg_item("a = 'b'").is_err()); 833 834 let (name, value) = parse_config_arg_item("a=b").unwrap(); 835 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"])); 836 assert_eq!(value.as_str(), Some("b")); 837 838 let (name, value) = parse_config_arg_item("a=").unwrap(); 839 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"])); 840 assert_eq!(value.as_str(), Some("")); 841 842 let (name, value) = parse_config_arg_item("a= ").unwrap(); 843 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"])); 844 assert_eq!(value.as_str(), Some(" ")); 845 846 // This one is a bit cryptic, but b=c can be a bare string. 847 let (name, value) = parse_config_arg_item("a=b=c").unwrap(); 848 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"])); 849 assert_eq!(value.as_str(), Some("b=c")); 850 851 let (name, value) = parse_config_arg_item("a.b=true").unwrap(); 852 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"])); 853 assert_eq!(value.as_bool(), Some(true)); 854 855 let (name, value) = parse_config_arg_item("a='b=c'").unwrap(); 856 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"])); 857 assert_eq!(value.as_str(), Some("b=c")); 858 859 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap(); 860 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"])); 861 assert_eq!(value.as_str(), Some("c")); 862 863 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap(); 864 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "])); 865 assert!(value.is_inline_table()); 866 assert_eq!(value.to_string(), "{d = 'e=f'}"); 867 } 868 869 #[test] 870 fn test_command_args() { 871 let mut config = StackedConfig::empty(); 872 config.add_layer( 873 ConfigLayer::parse( 874 ConfigSource::User, 875 indoc! {" 876 empty_array = [] 877 empty_string = '' 878 array = ['emacs', '-nw'] 879 string = 'emacs -nw' 880 structured.env = { KEY1 = 'value1', KEY2 = 'value2' } 881 structured.command = ['emacs', '-nw'] 882 "}, 883 ) 884 .unwrap(), 885 ); 886 887 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err()); 888 889 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap(); 890 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned())); 891 let (name, args) = command_args.split_name_and_args(); 892 assert_eq!(name, ""); 893 assert!(args.is_empty()); 894 895 let command_args: CommandNameAndArgs = config.get("array").unwrap(); 896 assert_eq!( 897 command_args, 898 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec( 899 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec() 900 )) 901 ); 902 let (name, args) = command_args.split_name_and_args(); 903 assert_eq!(name, "emacs"); 904 assert_eq!(args, ["-nw"].as_ref()); 905 906 let command_args: CommandNameAndArgs = config.get("string").unwrap(); 907 assert_eq!( 908 command_args, 909 CommandNameAndArgs::String("emacs -nw".to_owned()) 910 ); 911 let (name, args) = command_args.split_name_and_args(); 912 assert_eq!(name, "emacs"); 913 assert_eq!(args, ["-nw"].as_ref()); 914 915 let command_args: CommandNameAndArgs = config.get("structured").unwrap(); 916 assert_eq!( 917 command_args, 918 CommandNameAndArgs::Structured { 919 env: hashmap! { 920 "KEY1".to_string() => "value1".to_string(), 921 "KEY2".to_string() => "value2".to_string(), 922 }, 923 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec()) 924 } 925 ); 926 let (name, args) = command_args.split_name_and_args(); 927 assert_eq!(name, "emacs"); 928 assert_eq!(args, ["-nw"].as_ref()); 929 } 930 931 #[test] 932 fn test_resolved_config_values_empty() { 933 let config = StackedConfig::empty(); 934 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty()); 935 } 936 937 #[test] 938 fn test_resolved_config_values_single_key() { 939 let settings = insta_settings(); 940 let _guard = settings.bind_to_scope(); 941 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase); 942 env_base_layer 943 .set_value("user.name", "base-user-name") 944 .unwrap(); 945 env_base_layer 946 .set_value("user.email", "base@user.email") 947 .unwrap(); 948 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo); 949 repo_layer 950 .set_value("user.email", "repo@user.email") 951 .unwrap(); 952 let mut config = StackedConfig::empty(); 953 config.add_layer(env_base_layer); 954 config.add_layer(repo_layer); 955 // Note: "email" is alphabetized, before "name" from same layer. 956 insta::assert_debug_snapshot!( 957 resolved_config_values(&config, &ConfigNamePathBuf::root()), 958 @r#" 959 [ 960 AnnotatedValue { 961 name: ConfigNamePathBuf( 962 [ 963 Key { 964 key: "user", 965 repr: None, 966 leaf_decor: Decor { .. }, 967 dotted_decor: Decor { .. }, 968 }, 969 Key { 970 key: "name", 971 repr: None, 972 leaf_decor: Decor { .. }, 973 dotted_decor: Decor { .. }, 974 }, 975 ], 976 ), 977 value: String( 978 Formatted { 979 value: "base-user-name", 980 repr: "default", 981 decor: Decor { .. }, 982 }, 983 ), 984 source: EnvBase, 985 path: None, 986 is_overridden: false, 987 }, 988 AnnotatedValue { 989 name: ConfigNamePathBuf( 990 [ 991 Key { 992 key: "user", 993 repr: None, 994 leaf_decor: Decor { .. }, 995 dotted_decor: Decor { .. }, 996 }, 997 Key { 998 key: "email", 999 repr: None, 1000 leaf_decor: Decor { .. }, 1001 dotted_decor: Decor { .. }, 1002 }, 1003 ], 1004 ), 1005 value: String( 1006 Formatted { 1007 value: "base@user.email", 1008 repr: "default", 1009 decor: Decor { .. }, 1010 }, 1011 ), 1012 source: EnvBase, 1013 path: None, 1014 is_overridden: true, 1015 }, 1016 AnnotatedValue { 1017 name: ConfigNamePathBuf( 1018 [ 1019 Key { 1020 key: "user", 1021 repr: None, 1022 leaf_decor: Decor { .. }, 1023 dotted_decor: Decor { .. }, 1024 }, 1025 Key { 1026 key: "email", 1027 repr: None, 1028 leaf_decor: Decor { .. }, 1029 dotted_decor: Decor { .. }, 1030 }, 1031 ], 1032 ), 1033 value: String( 1034 Formatted { 1035 value: "repo@user.email", 1036 repr: "default", 1037 decor: Decor { .. }, 1038 }, 1039 ), 1040 source: Repo, 1041 path: None, 1042 is_overridden: false, 1043 }, 1044 ] 1045 "# 1046 ); 1047 } 1048 1049 #[test] 1050 fn test_resolved_config_values_filter_path() { 1051 let settings = insta_settings(); 1052 let _guard = settings.bind_to_scope(); 1053 let mut user_layer = ConfigLayer::empty(ConfigSource::User); 1054 user_layer.set_value("test-table1.foo", "user-FOO").unwrap(); 1055 user_layer.set_value("test-table2.bar", "user-BAR").unwrap(); 1056 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo); 1057 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap(); 1058 let mut config = StackedConfig::empty(); 1059 config.add_layer(user_layer); 1060 config.add_layer(repo_layer); 1061 insta::assert_debug_snapshot!( 1062 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])), 1063 @r#" 1064 [ 1065 AnnotatedValue { 1066 name: ConfigNamePathBuf( 1067 [ 1068 Key { 1069 key: "test-table1", 1070 repr: None, 1071 leaf_decor: Decor { .. }, 1072 dotted_decor: Decor { .. }, 1073 }, 1074 Key { 1075 key: "foo", 1076 repr: None, 1077 leaf_decor: Decor { .. }, 1078 dotted_decor: Decor { .. }, 1079 }, 1080 ], 1081 ), 1082 value: String( 1083 Formatted { 1084 value: "user-FOO", 1085 repr: "default", 1086 decor: Decor { .. }, 1087 }, 1088 ), 1089 source: User, 1090 path: None, 1091 is_overridden: false, 1092 }, 1093 AnnotatedValue { 1094 name: ConfigNamePathBuf( 1095 [ 1096 Key { 1097 key: "test-table1", 1098 repr: None, 1099 leaf_decor: Decor { .. }, 1100 dotted_decor: Decor { .. }, 1101 }, 1102 Key { 1103 key: "bar", 1104 repr: None, 1105 leaf_decor: Decor { .. }, 1106 dotted_decor: Decor { .. }, 1107 }, 1108 ], 1109 ), 1110 value: String( 1111 Formatted { 1112 value: "repo-BAR", 1113 repr: "default", 1114 decor: Decor { .. }, 1115 }, 1116 ), 1117 source: Repo, 1118 path: None, 1119 is_overridden: false, 1120 }, 1121 ] 1122 "# 1123 ); 1124 } 1125 1126 #[test] 1127 fn test_resolved_config_values_overridden() { 1128 let list = |layers: &[&ConfigLayer], prefix: &str| -> String { 1129 let mut config = StackedConfig::empty(); 1130 config.extend_layers(layers.iter().copied().cloned()); 1131 let prefix = if prefix.is_empty() { 1132 ConfigNamePathBuf::root() 1133 } else { 1134 prefix.parse().unwrap() 1135 }; 1136 let mut output = String::new(); 1137 for annotated in resolved_config_values(&config, &prefix) { 1138 let AnnotatedValue { name, value, .. } = &annotated; 1139 let sigil = if annotated.is_overridden { '!' } else { ' ' }; 1140 writeln!(output, "{sigil}{name} = {value}").unwrap(); 1141 } 1142 output 1143 }; 1144 1145 let mut layer0 = ConfigLayer::empty(ConfigSource::User); 1146 layer0.set_value("a.b.e", "0.0").unwrap(); 1147 layer0.set_value("a.b.c.f", "0.1").unwrap(); 1148 layer0.set_value("a.b.d", "0.2").unwrap(); 1149 let mut layer1 = ConfigLayer::empty(ConfigSource::User); 1150 layer1.set_value("a.b", "1.0").unwrap(); 1151 layer1.set_value("a.c", "1.1").unwrap(); 1152 let mut layer2 = ConfigLayer::empty(ConfigSource::User); 1153 layer2.set_value("a.b.g", "2.0").unwrap(); 1154 layer2.set_value("a.b.d", "2.1").unwrap(); 1155 1156 // a.b.* is shadowed by a.b 1157 let layers = [&layer0, &layer1]; 1158 insta::assert_snapshot!(list(&layers, ""), @r#" 1159 !a.b.e = "0.0" 1160 !a.b.c.f = "0.1" 1161 !a.b.d = "0.2" 1162 a.b = "1.0" 1163 a.c = "1.1" 1164 "#); 1165 insta::assert_snapshot!(list(&layers, "a.b"), @r#" 1166 !a.b.e = "0.0" 1167 !a.b.c.f = "0.1" 1168 !a.b.d = "0.2" 1169 a.b = "1.0" 1170 "#); 1171 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#); 1172 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#); 1173 1174 // a.b is shadowed by a.b.* 1175 let layers = [&layer1, &layer2]; 1176 insta::assert_snapshot!(list(&layers, ""), @r#" 1177 !a.b = "1.0" 1178 a.c = "1.1" 1179 a.b.g = "2.0" 1180 a.b.d = "2.1" 1181 "#); 1182 insta::assert_snapshot!(list(&layers, "a.b"), @r#" 1183 !a.b = "1.0" 1184 a.b.g = "2.0" 1185 a.b.d = "2.1" 1186 "#); 1187 1188 // a.b.d is shadowed by a.b.d 1189 let layers = [&layer0, &layer2]; 1190 insta::assert_snapshot!(list(&layers, ""), @r#" 1191 a.b.e = "0.0" 1192 a.b.c.f = "0.1" 1193 !a.b.d = "0.2" 1194 a.b.g = "2.0" 1195 a.b.d = "2.1" 1196 "#); 1197 insta::assert_snapshot!(list(&layers, "a.b"), @r#" 1198 a.b.e = "0.0" 1199 a.b.c.f = "0.1" 1200 !a.b.d = "0.2" 1201 a.b.g = "2.0" 1202 a.b.d = "2.1" 1203 "#); 1204 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#); 1205 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#" 1206 !a.b.d = "0.2" 1207 a.b.d = "2.1" 1208 "#); 1209 1210 // a.b.* is shadowed by a.b, which is shadowed by a.b.* 1211 let layers = [&layer0, &layer1, &layer2]; 1212 insta::assert_snapshot!(list(&layers, ""), @r#" 1213 !a.b.e = "0.0" 1214 !a.b.c.f = "0.1" 1215 !a.b.d = "0.2" 1216 !a.b = "1.0" 1217 a.c = "1.1" 1218 a.b.g = "2.0" 1219 a.b.d = "2.1" 1220 "#); 1221 insta::assert_snapshot!(list(&layers, "a.b"), @r#" 1222 !a.b.e = "0.0" 1223 !a.b.c.f = "0.1" 1224 !a.b.d = "0.2" 1225 !a.b = "1.0" 1226 a.b.g = "2.0" 1227 a.b.d = "2.1" 1228 "#); 1229 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#); 1230 } 1231 1232 struct TestCase { 1233 files: &'static [&'static str], 1234 env: UnresolvedConfigEnv, 1235 wants: &'static [Want], 1236 } 1237 1238 enum Want { 1239 New(&'static str), 1240 Existing(&'static str), 1241 } 1242 1243 fn config_path_home_existing() -> TestCase { 1244 TestCase { 1245 files: &["home/.jjconfig.toml"], 1246 env: UnresolvedConfigEnv { 1247 home_dir: Some("home".into()), 1248 ..Default::default() 1249 }, 1250 wants: &[Want::Existing("home/.jjconfig.toml")], 1251 } 1252 } 1253 1254 fn config_path_home_new() -> TestCase { 1255 TestCase { 1256 files: &[], 1257 env: UnresolvedConfigEnv { 1258 home_dir: Some("home".into()), 1259 ..Default::default() 1260 }, 1261 wants: &[Want::New("home/.jjconfig.toml")], 1262 } 1263 } 1264 1265 fn config_path_home_existing_platform_new() -> TestCase { 1266 TestCase { 1267 files: &["home/.jjconfig.toml"], 1268 env: UnresolvedConfigEnv { 1269 home_dir: Some("home".into()), 1270 config_dir: Some("config".into()), 1271 ..Default::default() 1272 }, 1273 wants: &[ 1274 Want::Existing("home/.jjconfig.toml"), 1275 Want::New("config/jj/config.toml"), 1276 ], 1277 } 1278 } 1279 1280 fn config_path_platform_existing() -> TestCase { 1281 TestCase { 1282 files: &["config/jj/config.toml"], 1283 env: UnresolvedConfigEnv { 1284 home_dir: Some("home".into()), 1285 config_dir: Some("config".into()), 1286 ..Default::default() 1287 }, 1288 wants: &[Want::Existing("config/jj/config.toml")], 1289 } 1290 } 1291 1292 fn config_path_platform_new() -> TestCase { 1293 TestCase { 1294 files: &[], 1295 env: UnresolvedConfigEnv { 1296 config_dir: Some("config".into()), 1297 ..Default::default() 1298 }, 1299 wants: &[Want::New("config/jj/config.toml")], 1300 } 1301 } 1302 1303 fn config_path_new_prefer_platform() -> TestCase { 1304 TestCase { 1305 files: &[], 1306 env: UnresolvedConfigEnv { 1307 home_dir: Some("home".into()), 1308 config_dir: Some("config".into()), 1309 ..Default::default() 1310 }, 1311 wants: &[Want::New("config/jj/config.toml")], 1312 } 1313 } 1314 1315 fn config_path_jj_config_existing() -> TestCase { 1316 TestCase { 1317 files: &["custom.toml"], 1318 env: UnresolvedConfigEnv { 1319 jj_config: Some("custom.toml".into()), 1320 ..Default::default() 1321 }, 1322 wants: &[Want::Existing("custom.toml")], 1323 } 1324 } 1325 1326 fn config_path_jj_config_new() -> TestCase { 1327 TestCase { 1328 files: &[], 1329 env: UnresolvedConfigEnv { 1330 jj_config: Some("custom.toml".into()), 1331 ..Default::default() 1332 }, 1333 wants: &[Want::New("custom.toml")], 1334 } 1335 } 1336 1337 fn config_path_jj_config_existing_multiple() -> TestCase { 1338 TestCase { 1339 files: &["custom1.toml", "custom2.toml"], 1340 env: UnresolvedConfigEnv { 1341 jj_config: Some( 1342 join_paths(["custom1.toml", "custom2.toml"]) 1343 .unwrap() 1344 .into_string() 1345 .unwrap(), 1346 ), 1347 ..Default::default() 1348 }, 1349 wants: &[ 1350 Want::Existing("custom1.toml"), 1351 Want::Existing("custom2.toml"), 1352 ], 1353 } 1354 } 1355 1356 fn config_path_jj_config_new_multiple() -> TestCase { 1357 TestCase { 1358 files: &["custom1.toml"], 1359 env: UnresolvedConfigEnv { 1360 jj_config: Some( 1361 join_paths(["custom1.toml", "custom2.toml"]) 1362 .unwrap() 1363 .into_string() 1364 .unwrap(), 1365 ), 1366 ..Default::default() 1367 }, 1368 wants: &[Want::Existing("custom1.toml"), Want::New("custom2.toml")], 1369 } 1370 } 1371 1372 fn config_path_jj_config_empty_paths_filtered() -> TestCase { 1373 TestCase { 1374 files: &["custom1.toml"], 1375 env: UnresolvedConfigEnv { 1376 jj_config: Some( 1377 join_paths(["custom1.toml", "", "custom2.toml"]) 1378 .unwrap() 1379 .into_string() 1380 .unwrap(), 1381 ), 1382 ..Default::default() 1383 }, 1384 wants: &[Want::Existing("custom1.toml"), Want::New("custom2.toml")], 1385 } 1386 } 1387 1388 fn config_path_jj_config_empty() -> TestCase { 1389 TestCase { 1390 files: &[], 1391 env: UnresolvedConfigEnv { 1392 jj_config: Some("".to_owned()), 1393 ..Default::default() 1394 }, 1395 wants: &[], 1396 } 1397 } 1398 1399 fn config_path_config_pick_platform() -> TestCase { 1400 TestCase { 1401 files: &["config/jj/config.toml"], 1402 env: UnresolvedConfigEnv { 1403 home_dir: Some("home".into()), 1404 config_dir: Some("config".into()), 1405 ..Default::default() 1406 }, 1407 wants: &[Want::Existing("config/jj/config.toml")], 1408 } 1409 } 1410 1411 fn config_path_config_pick_home() -> TestCase { 1412 TestCase { 1413 files: &["home/.jjconfig.toml"], 1414 env: UnresolvedConfigEnv { 1415 home_dir: Some("home".into()), 1416 config_dir: Some("config".into()), 1417 ..Default::default() 1418 }, 1419 wants: &[ 1420 Want::Existing("home/.jjconfig.toml"), 1421 Want::New("config/jj/config.toml"), 1422 ], 1423 } 1424 } 1425 1426 fn config_path_platform_new_conf_dir_existing() -> TestCase { 1427 TestCase { 1428 files: &["config/jj/conf.d/_"], 1429 env: UnresolvedConfigEnv { 1430 home_dir: Some("home".into()), 1431 config_dir: Some("config".into()), 1432 ..Default::default() 1433 }, 1434 wants: &[ 1435 Want::New("config/jj/config.toml"), 1436 Want::Existing("config/jj/conf.d"), 1437 ], 1438 } 1439 } 1440 1441 fn config_path_platform_existing_conf_dir_existing() -> TestCase { 1442 TestCase { 1443 files: &["config/jj/config.toml", "config/jj/conf.d/_"], 1444 env: UnresolvedConfigEnv { 1445 home_dir: Some("home".into()), 1446 config_dir: Some("config".into()), 1447 ..Default::default() 1448 }, 1449 wants: &[ 1450 Want::Existing("config/jj/config.toml"), 1451 Want::Existing("config/jj/conf.d"), 1452 ], 1453 } 1454 } 1455 1456 fn config_path_all_existing() -> TestCase { 1457 TestCase { 1458 files: &[ 1459 "config/jj/conf.d/_", 1460 "config/jj/config.toml", 1461 "home/.jjconfig.toml", 1462 ], 1463 env: UnresolvedConfigEnv { 1464 home_dir: Some("home".into()), 1465 config_dir: Some("config".into()), 1466 ..Default::default() 1467 }, 1468 // Precedence order is important 1469 wants: &[ 1470 Want::Existing("home/.jjconfig.toml"), 1471 Want::Existing("config/jj/config.toml"), 1472 Want::Existing("config/jj/conf.d"), 1473 ], 1474 } 1475 } 1476 1477 fn config_path_none() -> TestCase { 1478 TestCase { 1479 files: &[], 1480 env: Default::default(), 1481 wants: &[], 1482 } 1483 } 1484 1485 #[test_case(config_path_home_existing())] 1486 #[test_case(config_path_home_new())] 1487 #[test_case(config_path_home_existing_platform_new())] 1488 #[test_case(config_path_platform_existing())] 1489 #[test_case(config_path_platform_new())] 1490 #[test_case(config_path_new_prefer_platform())] 1491 #[test_case(config_path_jj_config_existing())] 1492 #[test_case(config_path_jj_config_new())] 1493 #[test_case(config_path_jj_config_existing_multiple())] 1494 #[test_case(config_path_jj_config_new_multiple())] 1495 #[test_case(config_path_jj_config_empty_paths_filtered())] 1496 #[test_case(config_path_jj_config_empty())] 1497 #[test_case(config_path_config_pick_platform())] 1498 #[test_case(config_path_config_pick_home())] 1499 #[test_case(config_path_platform_new_conf_dir_existing())] 1500 #[test_case(config_path_platform_existing_conf_dir_existing())] 1501 #[test_case(config_path_all_existing())] 1502 #[test_case(config_path_none())] 1503 fn test_config_path(case: TestCase) { 1504 let tmp = setup_config_fs(case.files); 1505 let env = resolve_config_env(&case.env, tmp.path()); 1506 1507 let expected_existing: Vec<PathBuf> = case 1508 .wants 1509 .iter() 1510 .filter_map(|want| match want { 1511 Want::Existing(path) => Some(tmp.path().join(path)), 1512 _ => None, 1513 }) 1514 .collect(); 1515 assert_eq!( 1516 env.existing_user_config_paths().collect_vec(), 1517 expected_existing 1518 ); 1519 1520 let expected_paths: Vec<PathBuf> = case 1521 .wants 1522 .iter() 1523 .map(|want| match want { 1524 Want::New(path) | Want::Existing(path) => tmp.path().join(path), 1525 }) 1526 .collect(); 1527 assert_eq!(env.user_config_paths().collect_vec(), expected_paths); 1528 } 1529 1530 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir { 1531 let tmp = testutils::new_temp_dir(); 1532 for file in files { 1533 let path = tmp.path().join(file); 1534 if let Some(parent) = path.parent() { 1535 std::fs::create_dir_all(parent).unwrap(); 1536 } 1537 std::fs::File::create(path).unwrap(); 1538 } 1539 tmp 1540 } 1541 1542 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv { 1543 let home_dir = env.home_dir.as_ref().map(|p| root.join(p)); 1544 let env = UnresolvedConfigEnv { 1545 config_dir: env.config_dir.as_ref().map(|p| root.join(p)), 1546 home_dir: home_dir.clone(), 1547 jj_config: env.jj_config.as_ref().map(|p| { 1548 join_paths(split_paths(p).map(|p| { 1549 if p.as_os_str().is_empty() { 1550 return p; 1551 } 1552 root.join(p) 1553 })) 1554 .unwrap() 1555 .into_string() 1556 .unwrap() 1557 }), 1558 }; 1559 ConfigEnv { 1560 home_dir, 1561 repo_path: None, 1562 user_config_paths: env.resolve(), 1563 repo_config_path: None, 1564 command: None, 1565 } 1566 } 1567}