1use 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;
24use std::sync::Arc;
25use std::sync::LazyLock;
26use std::sync::Mutex;
27
28use etcetera::BaseStrategy as _;
29use itertools::Itertools as _;
30use jj_lib::config::ConfigFile;
31use jj_lib::config::ConfigGetError;
32use jj_lib::config::ConfigLayer;
33use jj_lib::config::ConfigLoadError;
34use jj_lib::config::ConfigMigrationRule;
35use jj_lib::config::ConfigNamePathBuf;
36use jj_lib::config::ConfigResolutionContext;
37use jj_lib::config::ConfigSource;
38use jj_lib::config::ConfigValue;
39use jj_lib::config::StackedConfig;
40use jj_lib::dsl_util::AliasDeclarationParser;
41use jj_lib::dsl_util::AliasesMap;
42use jj_lib::secure_config::LoadedSecureConfig;
43use jj_lib::secure_config::SecureConfig;
44use rand::SeedableRng as _;
45use rand_chacha::ChaCha20Rng;
46use regex::Captures;
47use regex::Regex;
48use serde::Serialize as _;
49use tracing::instrument;
50
51use crate::command_error::CommandError;
52use crate::command_error::config_error;
53use crate::command_error::config_error_with_message;
54use crate::ui::Ui;
55
56pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
58
59const REPO_CONFIG_DIR: &str = "repos";
60const WORKSPACE_CONFIG_DIR: &str = "workspaces";
61
62pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
65 match value_str.parse() {
66 Ok(value) => Ok(value),
67 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
68 Err(err) => Err(err),
69 }
70}
71
72fn is_bare_string(value_str: &str) -> bool {
73 let trimmed = value_str.trim_ascii().as_bytes();
76 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
77 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
79 } else {
80 true }
82}
83
84pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
87 match value {
88 ConfigValue::String(v) => toml::Value::String(v.into_value()),
89 ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
90 ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
91 ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
92 ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
93 ConfigValue::Array(array) => {
94 let array = array.into_iter().map(to_serializable_value).collect();
95 toml::Value::Array(array)
96 }
97 ConfigValue::InlineTable(table) => {
98 let table = table
99 .into_iter()
100 .map(|(k, v)| (k, to_serializable_value(v)))
101 .collect();
102 toml::Value::Table(table)
103 }
104 }
105}
106
107#[derive(Clone, Debug, serde::Serialize)]
109pub struct AnnotatedValue {
110 #[serde(serialize_with = "serialize_name")]
112 pub name: ConfigNamePathBuf,
113 #[serde(serialize_with = "serialize_value")]
115 pub value: ConfigValue,
116 #[serde(serialize_with = "serialize_source")]
118 pub source: ConfigSource,
119 pub path: Option<PathBuf>,
121 pub is_overridden: bool,
123}
124
125fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
126where
127 S: serde::Serializer,
128{
129 name.to_string().serialize(serializer)
130}
131
132fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
133where
134 S: serde::Serializer,
135{
136 to_serializable_value(value.clone()).serialize(serializer)
137}
138
139fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
140where
141 S: serde::Serializer,
142{
143 source.to_string().serialize(serializer)
144}
145
146pub fn resolved_config_values(
149 stacked_config: &StackedConfig,
150 filter_prefix: &ConfigNamePathBuf,
151) -> Vec<AnnotatedValue> {
152 let mut config_vals = vec![];
155 let mut upper_value_names = BTreeSet::new();
156 for layer in stacked_config.layers().iter().rev() {
157 let top_item = match layer.look_up_item(filter_prefix) {
158 Ok(Some(item)) => item,
159 Ok(None) => continue, Err(_) => {
161 upper_value_names.insert(filter_prefix.clone());
163 continue;
164 }
165 };
166 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
167 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
168 if let Some(table) = item.as_table_like() {
171 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
173 for (k, v) in table.iter() {
174 let mut sub_name = name.clone();
175 sub_name.push(k);
176 config_stack.push((sub_name, v, is_overridden)); }
178 } else {
179 let maybe_child = upper_value_names
181 .range(&name..)
182 .next()
183 .filter(|next| next.starts_with(&name));
184 let is_overridden = is_parent_overridden || maybe_child.is_some();
185 if maybe_child != Some(&name) {
186 upper_value_names.insert(name.clone());
187 }
188 let value = item
189 .clone()
190 .into_value()
191 .expect("Item::None should not exist in table");
192 config_vals.push(AnnotatedValue {
193 name,
194 value,
195 source: layer.source,
196 path: layer.path.clone(),
197 is_overridden,
198 });
199 }
200 }
201 }
202 config_vals.reverse();
203 config_vals
204}
205
206#[derive(Clone, Debug)]
211pub struct RawConfig(StackedConfig);
212
213impl AsRef<StackedConfig> for RawConfig {
214 fn as_ref(&self) -> &StackedConfig {
215 &self.0
216 }
217}
218
219impl AsMut<StackedConfig> for RawConfig {
220 fn as_mut(&mut self) -> &mut StackedConfig {
221 &mut self.0
222 }
223}
224
225#[derive(Clone, Debug)]
226enum ConfigPathState {
227 New,
228 Exists,
229}
230
231#[derive(Clone, Debug)]
237struct ConfigPath {
238 path: PathBuf,
239 state: ConfigPathState,
240}
241
242impl ConfigPath {
243 fn new(path: PathBuf) -> Self {
244 use ConfigPathState::*;
245 Self {
246 state: if path.exists() { Exists } else { New },
247 path,
248 }
249 }
250
251 fn as_path(&self) -> &Path {
252 &self.path
253 }
254 fn exists(&self) -> bool {
255 match self.state {
256 ConfigPathState::Exists => true,
257 ConfigPathState::New => false,
258 }
259 }
260}
261
262fn create_dir_all(path: &Path) -> std::io::Result<()> {
265 let mut dir = std::fs::DirBuilder::new();
266 dir.recursive(true);
267 #[cfg(unix)]
268 {
269 use std::os::unix::fs::DirBuilderExt as _;
270 dir.mode(0o700);
271 }
272 dir.create(path)
273}
274
275#[derive(Clone, Default, Debug)]
277struct UnresolvedConfigEnv {
278 user_config_dir: Option<PathBuf>,
279 home_dir: Option<PathBuf>,
280 jj_config: Option<String>,
281 system_config_dir: Option<PathBuf>,
282}
283
284impl UnresolvedConfigEnv {
285 fn root_config_dir(&self) -> Option<PathBuf> {
286 self.user_config_dir.as_deref().map(|c| c.join("jj"))
287 }
288
289 fn resolve_user(self) -> Vec<ConfigPath> {
290 if let Some(paths) = self.jj_config {
291 return split_paths(&paths)
292 .filter(|path| !path.as_os_str().is_empty())
293 .map(ConfigPath::new)
294 .collect();
295 }
296
297 let mut paths = vec![];
298 let home_config_path = self.home_dir.map(|mut home_dir| {
299 home_dir.push(".jjconfig.toml");
300 ConfigPath::new(home_dir)
301 });
302 let platform_config_path = self.user_config_dir.clone().map(|mut config_dir| {
303 config_dir.push("jj");
304 config_dir.push("config.toml");
305 ConfigPath::new(config_dir)
306 });
307 let platform_config_dir = self.user_config_dir.map(|mut config_dir| {
308 config_dir.push("jj");
309 config_dir.push("conf.d");
310 ConfigPath::new(config_dir)
311 });
312
313 if let Some(path) = home_config_path
314 && (path.exists() || platform_config_path.is_none())
315 {
316 paths.push(path);
317 }
318
319 if let Some(path) = platform_config_path {
322 paths.push(path);
323 }
324
325 if let Some(path) = platform_config_dir
326 && path.exists()
327 {
328 paths.push(path);
329 }
330
331 paths
332 }
333
334 fn resolve_system(&self) -> Vec<ConfigPath> {
335 if let Some(path) = self.system_config_dir.as_ref()
336 && self.jj_config.is_none()
337 {
338 [path.join("jj/config.toml"), path.join("jj/conf.d")]
339 .into_iter()
340 .map(ConfigPath::new)
341 .collect()
342 } else {
343 Vec::new()
344 }
345 }
346}
347
348#[derive(Clone, Debug)]
349pub struct ConfigEnv {
350 home_dir: Option<PathBuf>,
351 root_config_dir: Option<PathBuf>,
352 repo_path: Option<PathBuf>,
353 workspace_path: Option<PathBuf>,
354 system_config_paths: Vec<ConfigPath>,
355 user_config_paths: Vec<ConfigPath>,
356 repo_config: Option<SecureConfig>,
357 workspace_config: Option<SecureConfig>,
358 command: Option<String>,
359 hostname: Option<String>,
360 environment: HashMap<String, String>,
361 rng: Arc<Mutex<ChaCha20Rng>>,
362}
363
364impl ConfigEnv {
365 pub fn from_environment() -> Self {
367 let user_config_dir = etcetera::choose_base_strategy()
368 .ok()
369 .map(|s| s.config_dir());
370
371 let home_dir = etcetera::home_dir()
374 .ok()
375 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
376
377 let system_config_dir = if cfg!(unix) {
378 Some("/etc".into())
379 } else {
380 None
381 };
382
383 let env = UnresolvedConfigEnv {
384 user_config_dir,
385 home_dir: home_dir.clone(),
386 jj_config: env::var("JJ_CONFIG").ok(),
387 system_config_dir,
388 };
389 let environment = env::vars_os()
390 .filter_map(|(k, v)| {
391 let k = k.into_string().ok()?;
393 let v = v.into_string().ok()?;
394 Some((k, v))
395 })
396 .collect();
397 Self {
398 home_dir,
399 root_config_dir: env.root_config_dir(),
400 repo_path: None,
401 workspace_path: None,
402 system_config_paths: env.resolve_system(),
403 user_config_paths: env.resolve_user(),
404 repo_config: None,
405 workspace_config: None,
406 command: None,
407 hostname: whoami::hostname().ok(),
408 environment,
409 rng: Arc::new(Mutex::new(
412 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) {
413 ChaCha20Rng::seed_from_u64(value)
414 } else {
415 rand::make_rng()
416 },
417 )),
418 }
419 }
420
421 pub fn set_command_name(&mut self, command: String) {
422 self.command = Some(command);
423 }
424
425 #[instrument]
428 pub fn reload_system_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
429 config.as_mut().remove_layers(ConfigSource::System);
430 for path in self.existing_system_config_paths() {
431 if path.is_dir() {
432 config.as_mut().load_dir(ConfigSource::System, path)?;
433 } else {
434 config.as_mut().load_file(ConfigSource::System, path)?;
435 }
436 }
437 Ok(())
438 }
439
440 pub fn existing_system_config_paths(&self) -> impl Iterator<Item = &Path> {
441 self.system_config_paths
442 .iter()
443 .filter(|p| p.exists())
444 .map(ConfigPath::as_path)
445 }
446
447 fn load_secure_config(
448 &self,
449 ui: &Ui,
450 config: Option<&SecureConfig>,
451 kind: &str,
452 force: bool,
453 ) -> Result<Option<LoadedSecureConfig>, CommandError> {
454 Ok(match (config, self.root_config_dir.as_ref()) {
455 (Some(config), Some(root_config_dir)) => {
456 let mut guard = self.rng.lock().unwrap();
457 let loaded_config = if force {
458 config.load_config(&mut guard, &root_config_dir.join(kind))
459 } else {
460 config.maybe_load_config(&mut guard, &root_config_dir.join(kind))
461 }?;
462 for warning in &loaded_config.warnings {
463 writeln!(ui.warning_default(), "{warning}")?;
464 }
465 Some(loaded_config)
466 }
467 _ => None,
468 })
469 }
470
471 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
473 self.user_config_paths.iter().map(ConfigPath::as_path)
474 }
475
476 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
479 self.user_config_paths
480 .iter()
481 .filter(|p| p.exists())
482 .map(ConfigPath::as_path)
483 }
484
485 pub fn user_config_files(&self, config: &RawConfig) -> Result<Vec<ConfigFile>, CommandError> {
492 config_files_for(config, ConfigSource::User, || {
493 Ok(self.new_user_config_file()?)
494 })
495 }
496
497 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
498 self.user_config_paths()
499 .next()
500 .map(|path| {
501 if let Some(dir) = path.parent() {
504 create_dir_all(dir).ok();
505 }
506 ConfigFile::load_or_empty(ConfigSource::User, path)
509 })
510 .transpose()
511 }
512
513 #[instrument]
516 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
517 config.as_mut().remove_layers(ConfigSource::User);
518 for path in self.existing_user_config_paths() {
519 if path.is_dir() {
520 config.as_mut().load_dir(ConfigSource::User, path)?;
521 } else {
522 config.as_mut().load_file(ConfigSource::User, path)?;
523 }
524 }
525 Ok(())
526 }
527
528 pub fn reset_repo_path(&mut self, path: &Path) {
531 self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf()));
532 self.repo_path = Some(path.to_owned());
533 }
534
535 fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
537 Ok(self
538 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)?
539 .and_then(|c| c.config_file))
540 }
541
542 pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
546 Ok(self
547 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)?
548 .and_then(|c| c.config_file))
549 }
550
551 pub fn repo_configs_root_dir(&self) -> Option<PathBuf> {
554 self.root_config_dir
555 .as_ref()
556 .map(|dir| dir.join(REPO_CONFIG_DIR))
557 }
558
559 pub fn repo_config_files(
566 &self,
567 ui: &Ui,
568 config: &RawConfig,
569 ) -> Result<Vec<ConfigFile>, CommandError> {
570 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file(ui))
571 }
572
573 fn new_repo_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
574 Ok(self
575 .repo_config_path(ui)?
576 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
579 .transpose()?)
580 }
581
582 #[instrument(skip(ui))]
585 pub fn reload_repo_config(&self, ui: &Ui, config: &mut RawConfig) -> Result<(), CommandError> {
586 config.as_mut().remove_layers(ConfigSource::Repo);
587 if let Some(path) = self.maybe_repo_config_path(ui)?
588 && path.exists()
589 {
590 config.as_mut().load_file(ConfigSource::Repo, path)?;
591 }
592 Ok(())
593 }
594
595 pub fn reset_workspace_path(&mut self, path: &Path) {
597 self.workspace_config = Some(SecureConfig::new_workspace(path.join(".jj")));
598 self.workspace_path = Some(path.to_owned());
599 }
600
601 fn maybe_workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
603 Ok(self
604 .load_secure_config(
605 ui,
606 self.workspace_config.as_ref(),
607 WORKSPACE_CONFIG_DIR,
608 false,
609 )?
610 .and_then(|c| c.config_file))
611 }
612
613 pub fn workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
617 Ok(self
618 .load_secure_config(
619 ui,
620 self.workspace_config.as_ref(),
621 WORKSPACE_CONFIG_DIR,
622 true,
623 )?
624 .and_then(|c| c.config_file))
625 }
626
627 pub fn workspace_config_files(
634 &self,
635 ui: &Ui,
636 config: &RawConfig,
637 ) -> Result<Vec<ConfigFile>, CommandError> {
638 config_files_for(config, ConfigSource::Workspace, || {
639 self.new_workspace_config_file(ui)
640 })
641 }
642
643 fn new_workspace_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
644 Ok(self
645 .workspace_config_path(ui)?
646 .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
647 .transpose()?)
648 }
649
650 #[instrument(skip(ui))]
653 pub fn reload_workspace_config(
654 &self,
655 ui: &Ui,
656 config: &mut RawConfig,
657 ) -> Result<(), CommandError> {
658 config.as_mut().remove_layers(ConfigSource::Workspace);
659 if let Some(path) = self.maybe_workspace_config_path(ui)?
660 && path.exists()
661 {
662 config.as_mut().load_file(ConfigSource::Workspace, path)?;
663 }
664 Ok(())
665 }
666
667 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
670 let context = ConfigResolutionContext {
671 home_dir: self.home_dir.as_deref(),
672 repo_path: self.repo_path.as_deref(),
673 workspace_path: self.workspace_path.as_deref(),
674 command: self.command.as_deref(),
675 hostname: self.hostname.as_deref().unwrap_or(""),
676 environment: &self.environment,
677 };
678 jj_lib::config::resolve(config.as_ref(), &context)
679 }
680}
681
682pub fn existing_repo_config_file(config: &RawConfig) -> Option<ConfigFile> {
685 config
687 .as_ref()
688 .layers_for(ConfigSource::Repo)
689 .iter()
690 .find_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
691}
692
693fn config_files_for(
694 config: &RawConfig,
695 source: ConfigSource,
696 new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
697) -> Result<Vec<ConfigFile>, CommandError> {
698 let mut files = config
699 .as_ref()
700 .layers_for(source)
701 .iter()
702 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
703 .collect_vec();
704 if files.is_empty() {
705 files.extend(new_file()?);
706 }
707 Ok(files)
708}
709
710pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
725 let mut config = StackedConfig::with_defaults();
726 config.extend_layers(default_layers);
727 config.add_layer(env_base_layer());
728 config.add_layer(env_overrides_layer());
729 RawConfig(config)
730}
731
732const OP_HOSTNAME: &str = "operation.hostname";
733const OP_USERNAME: &str = "operation.username";
734
735fn env_base_layer() -> ConfigLayer {
737 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
738 if let Ok(value) =
739 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
740 {
741 layer.set_value(OP_HOSTNAME, value).unwrap();
742 }
743 if let Ok(value) =
744 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
745 {
746 layer.set_value(OP_USERNAME, value).unwrap();
747 } else if let Ok(value) = env::var("USER") {
748 layer.set_value(OP_USERNAME, value).unwrap();
751 }
752 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
753 layer.set_value("ui.color", "never").unwrap();
756 }
757 if let Ok(value) = env::var("VISUAL") {
758 layer.set_value("ui.editor", value).unwrap();
759 } else if let Ok(value) = env::var("EDITOR") {
760 layer.set_value("ui.editor", value).unwrap();
761 }
762 layer
765}
766
767pub fn default_config_layers() -> Vec<ConfigLayer> {
768 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
771 let mut layers = vec![
772 parse(include_str!("config/colors.toml")),
773 parse(include_str!("config/hints.toml")),
774 parse(include_str!("config/merge_tools.toml")),
775 parse(include_str!("config/misc.toml")),
776 parse(include_str!("config/revsets.toml")),
777 parse(include_str!("config/templates.toml")),
778 ];
779 if cfg!(unix) {
780 layers.push(parse(include_str!("config/unix.toml")));
781 }
782 if cfg!(windows) {
783 layers.push(parse(include_str!("config/windows.toml")));
784 }
785 layers
786}
787
788fn env_overrides_layer() -> ConfigLayer {
790 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
791 if let Ok(value) = env::var("JJ_USER") {
792 layer.set_value("user.name", value).unwrap();
793 }
794 if let Ok(value) = env::var("JJ_EMAIL") {
795 layer.set_value("user.email", value).unwrap();
796 }
797 if let Ok(value) = env::var("JJ_TIMESTAMP") {
798 layer.set_value("debug.commit-timestamp", value).unwrap();
799 }
800 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
801 layer.set_value("debug.randomness-seed", value).unwrap();
802 }
803 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
804 layer.set_value("debug.operation-timestamp", value).unwrap();
805 }
806 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
807 layer.set_value(OP_HOSTNAME, value).unwrap();
808 }
809 if let Ok(value) = env::var("JJ_OP_USERNAME") {
810 layer.set_value(OP_USERNAME, value).unwrap();
811 }
812 if let Ok(value) = env::var("JJ_EDITOR") {
813 layer.set_value("ui.editor", value).unwrap();
814 }
815 if let Ok(value) = env::var("JJ_PAGER") {
816 layer.set_value("ui.pager", value).unwrap();
817 }
818 layer
819}
820
821#[derive(Clone, Copy, Debug, Eq, PartialEq)]
823pub enum ConfigArgKind {
824 Item,
826 File,
828}
829
830pub fn parse_config_args(
832 toml_strs: &[(ConfigArgKind, &str)],
833) -> Result<Vec<ConfigLayer>, CommandError> {
834 let source = ConfigSource::CommandArg;
835 let mut layers = Vec::new();
836 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
837 match kind {
838 ConfigArgKind::Item => {
839 let mut layer = ConfigLayer::empty(source);
840 for (_, item) in chunk {
841 let (name, value) = parse_config_arg_item(item)?;
842 layer.set_value(name, value).map_err(|err| {
845 config_error_with_message("--config argument cannot be set", err)
846 })?;
847 }
848 layers.push(layer);
849 }
850 ConfigArgKind::File => {
851 for (_, path) in chunk {
852 layers.push(ConfigLayer::load_from_file(source, path.into())?);
853 }
854 }
855 }
856 }
857 Ok(layers)
858}
859
860fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
862 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
864 let Some((name, value_str)) = split_candidates
865 .map(|p| (&item_str[..p], &item_str[p + 1..]))
866 .map(|(name, value)| name.parse().map(|name| (name, value)))
867 .find_or_last(Result::is_ok)
868 .transpose()
869 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
870 else {
871 return Err(config_error("--config must be specified as NAME=VALUE"));
872 };
873 let value = parse_value_or_bare_string(value_str)
874 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
875 Ok((name, value))
876}
877
878pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
880 vec![]
881}
882
883#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
885#[serde(untagged)]
886pub enum CommandNameAndArgs {
887 String(String),
888 Vec(NonEmptyCommandArgsVec),
889 Structured {
890 env: HashMap<String, String>,
891 command: NonEmptyCommandArgsVec,
892 },
893}
894
895impl CommandNameAndArgs {
896 pub fn split_name(&self) -> Cow<'_, str> {
898 let (name, _) = self.split_name_and_args();
899 name
900 }
901
902 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
906 match self {
907 Self::String(s) => {
908 if s.contains('"') || s.contains('\'') {
909 let mut parts = shlex::Shlex::new(s);
910 let res = (
911 parts.next().unwrap_or_default().into(),
912 parts.by_ref().collect(),
913 );
914 if !parts.had_error {
915 return res;
916 }
917 }
918 let mut args = s.split(' ').map(|s| s.to_owned());
919 (args.next().unwrap().into(), args.collect())
920 }
921 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
922 Self::Structured {
923 env: _,
924 command: cmd,
925 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
926 }
927 }
928
929 pub fn as_str(&self) -> Option<&str> {
934 match self {
935 Self::String(s) => Some(s),
936 Self::Vec(_) | Self::Structured { .. } => None,
937 }
938 }
939
940 pub fn to_command(&self) -> Command {
942 let empty: HashMap<&str, &str> = HashMap::new();
943 self.to_command_with_variables(&empty)
944 }
945
946 pub fn to_command_with_variables<V: AsRef<str>>(
949 &self,
950 variables: &HashMap<&str, V>,
951 ) -> Command {
952 let (name, args) = self.split_name_and_args();
953 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
954 if let Self::Structured { env, .. } = self {
955 cmd.envs(env);
956 }
957 cmd.args(interpolate_variables(&args, variables));
958 cmd
959 }
960}
961
962impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
963 fn from(s: &T) -> Self {
964 Self::String(s.as_ref().to_owned())
965 }
966}
967
968impl fmt::Display for CommandNameAndArgs {
969 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
970 match self {
971 Self::String(s) => write!(f, "{s}"),
972 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
974 Self::Structured { env, command } => {
975 for (k, v) in env {
976 write!(f, "{k}={v} ")?;
977 }
978 write!(f, "{}", command.0.join(" "))
979 }
980 }
981 }
982}
983
984pub fn load_aliases_map<P>(
985 ui: &Ui,
986 config: &StackedConfig,
987 table_name: &ConfigNamePathBuf,
988) -> Result<AliasesMap<P, String>, CommandError>
989where
990 P: AliasDeclarationParser + Default,
991 P::Error: fmt::Display,
992{
993 let mut aliases_map = AliasesMap::new();
994 for layer in config.layers() {
997 let table = match layer.look_up_table(table_name) {
998 Ok(Some(table)) => table,
999 Ok(None) => continue,
1000 Err(item) => {
1001 return Err(ConfigGetError::Type {
1002 name: table_name.to_string(),
1003 error: format!("Expected a table, but is {}", item.type_name()).into(),
1004 source_path: layer.path.clone(),
1005 }
1006 .into());
1007 }
1008 };
1009 for (decl, item) in table.iter() {
1010 let (definition, doc) = if let Some(t) = item.as_table_like() {
1011 let definition = t.get("definition").and_then(|i| i.as_str());
1012 let doc = t.get("doc").and_then(|i| i.as_str()).map(|s| s.to_owned());
1013 (definition, doc)
1014 } else {
1015 (item.as_str(), None)
1016 };
1017
1018 let r = definition
1019 .ok_or_else(|| {
1020 format!(
1021 "Expected a string or a table with a `definition` string key, but is {}",
1022 item.type_name()
1023 )
1024 })
1025 .and_then(|v| aliases_map.insert(decl, v, doc).map_err(|e| format!("{e}")));
1026 if let Err(s) = r {
1027 writeln!(
1028 ui.warning_default(),
1029 "Failed to load `{table_name}.{decl}`: {s}"
1030 )?;
1031 }
1032 }
1033 }
1034 Ok(aliases_map)
1035}
1036
1037static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
1039
1040pub fn interpolate_variables<V: AsRef<str>>(
1041 args: &[String],
1042 variables: &HashMap<&str, V>,
1043) -> Vec<String> {
1044 args.iter()
1045 .map(|arg| interpolate_variables_single(arg, variables))
1046 .collect()
1047}
1048
1049fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
1050 VARIABLE_REGEX
1051 .replace_all(arg, |caps: &Captures| {
1052 let name = &caps[1];
1053 if let Some(subst) = variables.get(name) {
1054 subst.as_ref().to_owned()
1055 } else {
1056 caps[0].to_owned()
1057 }
1058 })
1059 .into_owned()
1060}
1061
1062pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
1064 let regex = &*VARIABLE_REGEX;
1065 args.iter()
1066 .flat_map(|arg| regex.find_iter(arg))
1067 .map(|single_match| {
1068 let s = single_match.as_str();
1069 &s[1..]
1070 })
1071}
1072
1073#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
1076#[serde(try_from = "Vec<String>")]
1077pub struct NonEmptyCommandArgsVec(Vec<String>);
1078
1079impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
1080 type Error = &'static str;
1081
1082 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
1083 if args.is_empty() {
1084 Err("command arguments should not be empty")
1085 } else {
1086 Ok(Self(args))
1087 }
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use std::env::join_paths;
1094 use std::fmt::Write as _;
1095
1096 use indoc::indoc;
1097 use maplit::hashmap;
1098 use test_case::test_case;
1099 use testutils::TestResult;
1100
1101 use super::*;
1102
1103 fn insta_settings() -> insta::Settings {
1104 let mut settings = insta::Settings::clone_current();
1105 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1107 settings
1108 }
1109
1110 #[test]
1111 fn test_parse_value_or_bare_string() -> TestResult {
1112 let parse = |s: &str| parse_value_or_bare_string(s);
1113
1114 assert_eq!(parse("true")?.as_bool(), Some(true));
1116 assert_eq!(parse("42")?.as_integer(), Some(42));
1117 assert_eq!(parse("-1")?.as_integer(), Some(-1));
1118 assert_eq!(parse("'a'")?.as_str(), Some("a"));
1119 assert!(parse("[]")?.is_array());
1120 assert!(parse("{ a = 'b' }")?.is_inline_table());
1121
1122 assert_eq!(parse("")?.as_str(), Some(""));
1124 assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
1125 assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
1126 assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
1127 assert_eq!(
1128 parse("<foo+bar@example.org>")?.as_str(),
1129 Some("<foo+bar@example.org>")
1130 );
1131 assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
1132 assert_eq!(parse("all()")?.as_str(), Some("all()"));
1133 assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
1134 assert_eq!(parse("柔術")?.as_str(), Some("柔術"));
1135
1136 assert!(parse("'foo").is_err());
1138 assert!(parse(r#" bar" "#).is_err());
1139 assert!(parse("[0 1]").is_err());
1140 assert!(parse("{ x = y }").is_err());
1141 assert!(parse("\n { x").is_err());
1142 assert!(parse(" x ] ").is_err());
1143 assert!(parse("[table]\nkey = 'value'").is_err());
1144 Ok(())
1145 }
1146
1147 #[test]
1148 fn test_parse_config_arg_item() {
1149 assert!(parse_config_arg_item("").is_err());
1150 assert!(parse_config_arg_item("a").is_err());
1151 assert!(parse_config_arg_item("=").is_err());
1152 assert!(parse_config_arg_item("a = 'b'").is_err());
1155
1156 let (name, value) = parse_config_arg_item("a=b").unwrap();
1157 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1158 assert_eq!(value.as_str(), Some("b"));
1159
1160 let (name, value) = parse_config_arg_item("a=").unwrap();
1161 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1162 assert_eq!(value.as_str(), Some(""));
1163
1164 let (name, value) = parse_config_arg_item("a= ").unwrap();
1165 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1166 assert_eq!(value.as_str(), Some(" "));
1167
1168 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1170 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1171 assert_eq!(value.as_str(), Some("b=c"));
1172
1173 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1174 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1175 assert_eq!(value.as_bool(), Some(true));
1176
1177 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1178 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1179 assert_eq!(value.as_str(), Some("b=c"));
1180
1181 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1182 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1183 assert_eq!(value.as_str(), Some("c"));
1184
1185 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1186 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1187 assert!(value.is_inline_table());
1188 assert_eq!(value.to_string(), "{d = 'e=f'}");
1189 }
1190
1191 #[test]
1192 fn test_command_args() -> TestResult {
1193 let mut config = StackedConfig::empty();
1194 config.add_layer(ConfigLayer::parse(
1195 ConfigSource::User,
1196 indoc! {"
1197 empty_array = []
1198 empty_string = ''
1199 array = ['emacs', '-nw']
1200 string = 'emacs -nw'
1201 string_quoted = '\"spaced path/to/emacs\" -nw'
1202 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1203 structured.command = ['emacs', '-nw']
1204 "},
1205 )?);
1206
1207 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1208
1209 let command_args: CommandNameAndArgs = config.get("empty_string")?;
1210 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1211 let (name, args) = command_args.split_name_and_args();
1212 assert_eq!(name, "");
1213 assert!(args.is_empty());
1214
1215 let command_args: CommandNameAndArgs = config.get("array")?;
1216 assert_eq!(
1217 command_args,
1218 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1219 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1220 ))
1221 );
1222 let (name, args) = command_args.split_name_and_args();
1223 assert_eq!(name, "emacs");
1224 assert_eq!(args, ["-nw"].as_ref());
1225
1226 let command_args: CommandNameAndArgs = config.get("string")?;
1227 assert_eq!(
1228 command_args,
1229 CommandNameAndArgs::String("emacs -nw".to_owned())
1230 );
1231 let (name, args) = command_args.split_name_and_args();
1232 assert_eq!(name, "emacs");
1233 assert_eq!(args, ["-nw"].as_ref());
1234
1235 let command_args: CommandNameAndArgs = config.get("string_quoted")?;
1236 assert_eq!(
1237 command_args,
1238 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1239 );
1240 let (name, args) = command_args.split_name_and_args();
1241 assert_eq!(name, "spaced path/to/emacs");
1242 assert_eq!(args, ["-nw"].as_ref());
1243
1244 let command_args: CommandNameAndArgs = config.get("structured")?;
1245 assert_eq!(
1246 command_args,
1247 CommandNameAndArgs::Structured {
1248 env: hashmap! {
1249 "KEY1".to_string() => "value1".to_string(),
1250 "KEY2".to_string() => "value2".to_string(),
1251 },
1252 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1253 }
1254 );
1255 let (name, args) = command_args.split_name_and_args();
1256 assert_eq!(name, "emacs");
1257 assert_eq!(args, ["-nw"].as_ref());
1258 Ok(())
1259 }
1260
1261 #[test]
1262 fn test_resolved_config_values_empty() {
1263 let config = StackedConfig::empty();
1264 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1265 }
1266
1267 #[test]
1268 fn test_resolved_config_values_single_key() -> TestResult {
1269 let settings = insta_settings();
1270 let _guard = settings.bind_to_scope();
1271 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1272 env_base_layer.set_value("user.name", "base-user-name")?;
1273 env_base_layer.set_value("user.email", "base@user.email")?;
1274 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1275 repo_layer.set_value("user.email", "repo@user.email")?;
1276 let mut config = StackedConfig::empty();
1277 config.add_layer(env_base_layer);
1278 config.add_layer(repo_layer);
1279 insta::assert_debug_snapshot!(
1281 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1282 @r#"
1283 [
1284 AnnotatedValue {
1285 name: ConfigNamePathBuf(
1286 [
1287 Key {
1288 key: "user",
1289 repr: None,
1290 leaf_decor: Decor { .. },
1291 dotted_decor: Decor { .. },
1292 },
1293 Key {
1294 key: "name",
1295 repr: None,
1296 leaf_decor: Decor { .. },
1297 dotted_decor: Decor { .. },
1298 },
1299 ],
1300 ),
1301 value: String(
1302 Formatted {
1303 value: "base-user-name",
1304 repr: "default",
1305 decor: Decor { .. },
1306 },
1307 ),
1308 source: EnvBase,
1309 path: None,
1310 is_overridden: false,
1311 },
1312 AnnotatedValue {
1313 name: ConfigNamePathBuf(
1314 [
1315 Key {
1316 key: "user",
1317 repr: None,
1318 leaf_decor: Decor { .. },
1319 dotted_decor: Decor { .. },
1320 },
1321 Key {
1322 key: "email",
1323 repr: None,
1324 leaf_decor: Decor { .. },
1325 dotted_decor: Decor { .. },
1326 },
1327 ],
1328 ),
1329 value: String(
1330 Formatted {
1331 value: "base@user.email",
1332 repr: "default",
1333 decor: Decor { .. },
1334 },
1335 ),
1336 source: EnvBase,
1337 path: None,
1338 is_overridden: true,
1339 },
1340 AnnotatedValue {
1341 name: ConfigNamePathBuf(
1342 [
1343 Key {
1344 key: "user",
1345 repr: None,
1346 leaf_decor: Decor { .. },
1347 dotted_decor: Decor { .. },
1348 },
1349 Key {
1350 key: "email",
1351 repr: None,
1352 leaf_decor: Decor { .. },
1353 dotted_decor: Decor { .. },
1354 },
1355 ],
1356 ),
1357 value: String(
1358 Formatted {
1359 value: "repo@user.email",
1360 repr: "default",
1361 decor: Decor { .. },
1362 },
1363 ),
1364 source: Repo,
1365 path: None,
1366 is_overridden: false,
1367 },
1368 ]
1369 "#
1370 );
1371 Ok(())
1372 }
1373
1374 #[test]
1375 fn test_resolved_config_values_filter_path() -> TestResult {
1376 let settings = insta_settings();
1377 let _guard = settings.bind_to_scope();
1378 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1379 user_layer.set_value("test-table1.foo", "user-FOO")?;
1380 user_layer.set_value("test-table2.bar", "user-BAR")?;
1381 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1382 repo_layer.set_value("test-table1.bar", "repo-BAR")?;
1383 let mut config = StackedConfig::empty();
1384 config.add_layer(user_layer);
1385 config.add_layer(repo_layer);
1386 insta::assert_debug_snapshot!(
1387 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1388 @r#"
1389 [
1390 AnnotatedValue {
1391 name: ConfigNamePathBuf(
1392 [
1393 Key {
1394 key: "test-table1",
1395 repr: None,
1396 leaf_decor: Decor { .. },
1397 dotted_decor: Decor { .. },
1398 },
1399 Key {
1400 key: "foo",
1401 repr: None,
1402 leaf_decor: Decor { .. },
1403 dotted_decor: Decor { .. },
1404 },
1405 ],
1406 ),
1407 value: String(
1408 Formatted {
1409 value: "user-FOO",
1410 repr: "default",
1411 decor: Decor { .. },
1412 },
1413 ),
1414 source: User,
1415 path: None,
1416 is_overridden: false,
1417 },
1418 AnnotatedValue {
1419 name: ConfigNamePathBuf(
1420 [
1421 Key {
1422 key: "test-table1",
1423 repr: None,
1424 leaf_decor: Decor { .. },
1425 dotted_decor: Decor { .. },
1426 },
1427 Key {
1428 key: "bar",
1429 repr: None,
1430 leaf_decor: Decor { .. },
1431 dotted_decor: Decor { .. },
1432 },
1433 ],
1434 ),
1435 value: String(
1436 Formatted {
1437 value: "repo-BAR",
1438 repr: "default",
1439 decor: Decor { .. },
1440 },
1441 ),
1442 source: Repo,
1443 path: None,
1444 is_overridden: false,
1445 },
1446 ]
1447 "#
1448 );
1449 Ok(())
1450 }
1451
1452 #[test]
1453 fn test_resolved_config_values_overridden() -> TestResult {
1454 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1455 let mut config = StackedConfig::empty();
1456 config.extend_layers(layers.iter().copied().cloned());
1457 let prefix = if prefix.is_empty() {
1458 ConfigNamePathBuf::root()
1459 } else {
1460 prefix.parse().unwrap()
1461 };
1462 let mut output = String::new();
1463 for annotated in resolved_config_values(&config, &prefix) {
1464 let AnnotatedValue { name, value, .. } = &annotated;
1465 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1466 writeln!(output, "{sigil}{name} = {value}").unwrap();
1467 }
1468 output
1469 };
1470
1471 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1472 layer0.set_value("a.b.e", "0.0")?;
1473 layer0.set_value("a.b.c.f", "0.1")?;
1474 layer0.set_value("a.b.d", "0.2")?;
1475 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1476 layer1.set_value("a.b", "1.0")?;
1477 layer1.set_value("a.c", "1.1")?;
1478 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1479 layer2.set_value("a.b.g", "2.0")?;
1480 layer2.set_value("a.b.d", "2.1")?;
1481
1482 let layers = [&layer0, &layer1];
1484 insta::assert_snapshot!(list(&layers, ""), @r#"
1485 !a.b.e = "0.0"
1486 !a.b.c.f = "0.1"
1487 !a.b.d = "0.2"
1488 a.b = "1.0"
1489 a.c = "1.1"
1490 "#);
1491 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1492 !a.b.e = "0.0"
1493 !a.b.c.f = "0.1"
1494 !a.b.d = "0.2"
1495 a.b = "1.0"
1496 "#);
1497 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1498 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1499
1500 let layers = [&layer1, &layer2];
1502 insta::assert_snapshot!(list(&layers, ""), @r#"
1503 !a.b = "1.0"
1504 a.c = "1.1"
1505 a.b.g = "2.0"
1506 a.b.d = "2.1"
1507 "#);
1508 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1509 !a.b = "1.0"
1510 a.b.g = "2.0"
1511 a.b.d = "2.1"
1512 "#);
1513
1514 let layers = [&layer0, &layer2];
1516 insta::assert_snapshot!(list(&layers, ""), @r#"
1517 a.b.e = "0.0"
1518 a.b.c.f = "0.1"
1519 !a.b.d = "0.2"
1520 a.b.g = "2.0"
1521 a.b.d = "2.1"
1522 "#);
1523 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1524 a.b.e = "0.0"
1525 a.b.c.f = "0.1"
1526 !a.b.d = "0.2"
1527 a.b.g = "2.0"
1528 a.b.d = "2.1"
1529 "#);
1530 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1531 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1532 !a.b.d = "0.2"
1533 a.b.d = "2.1"
1534 "#);
1535
1536 let layers = [&layer0, &layer1, &layer2];
1538 insta::assert_snapshot!(list(&layers, ""), @r#"
1539 !a.b.e = "0.0"
1540 !a.b.c.f = "0.1"
1541 !a.b.d = "0.2"
1542 !a.b = "1.0"
1543 a.c = "1.1"
1544 a.b.g = "2.0"
1545 a.b.d = "2.1"
1546 "#);
1547 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1548 !a.b.e = "0.0"
1549 !a.b.c.f = "0.1"
1550 !a.b.d = "0.2"
1551 !a.b = "1.0"
1552 a.b.g = "2.0"
1553 a.b.d = "2.1"
1554 "#);
1555 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1556 Ok(())
1557 }
1558
1559 struct TestCase {
1560 files: &'static [&'static str],
1561 env: UnresolvedConfigEnv,
1562 wants: Vec<Want>,
1563 }
1564
1565 #[derive(Debug)]
1566 enum WantState {
1567 New,
1568 Existing,
1569 }
1570 #[derive(Debug)]
1571 struct Want {
1572 path: &'static str,
1573 state: WantState,
1574 }
1575
1576 impl Want {
1577 const fn new(path: &'static str) -> Self {
1578 Self {
1579 path,
1580 state: WantState::New,
1581 }
1582 }
1583
1584 const fn existing(path: &'static str) -> Self {
1585 Self {
1586 path,
1587 state: WantState::Existing,
1588 }
1589 }
1590
1591 fn rooted_path(&self, root: &Path) -> PathBuf {
1592 root.join(self.path)
1593 }
1594
1595 fn exists(&self) -> bool {
1596 matches!(self.state, WantState::Existing)
1597 }
1598 }
1599
1600 fn config_path_home_existing() -> TestCase {
1601 TestCase {
1602 files: &["home/.jjconfig.toml"],
1603 env: UnresolvedConfigEnv {
1604 home_dir: Some("home".into()),
1605 ..Default::default()
1606 },
1607 wants: vec![Want::existing("home/.jjconfig.toml")],
1608 }
1609 }
1610
1611 fn config_path_home_new() -> TestCase {
1612 TestCase {
1613 files: &[],
1614 env: UnresolvedConfigEnv {
1615 home_dir: Some("home".into()),
1616 ..Default::default()
1617 },
1618 wants: vec![Want::new("home/.jjconfig.toml")],
1619 }
1620 }
1621
1622 fn config_path_home_existing_platform_new() -> TestCase {
1623 TestCase {
1624 files: &["home/.jjconfig.toml"],
1625 env: UnresolvedConfigEnv {
1626 home_dir: Some("home".into()),
1627 user_config_dir: Some("config".into()),
1628 ..Default::default()
1629 },
1630 wants: vec![
1631 Want::existing("home/.jjconfig.toml"),
1632 Want::new("config/jj/config.toml"),
1633 ],
1634 }
1635 }
1636
1637 fn config_path_platform_existing() -> TestCase {
1638 TestCase {
1639 files: &["config/jj/config.toml"],
1640 env: UnresolvedConfigEnv {
1641 home_dir: Some("home".into()),
1642 user_config_dir: Some("config".into()),
1643 ..Default::default()
1644 },
1645 wants: vec![Want::existing("config/jj/config.toml")],
1646 }
1647 }
1648
1649 fn config_path_platform_new() -> TestCase {
1650 TestCase {
1651 files: &[],
1652 env: UnresolvedConfigEnv {
1653 user_config_dir: Some("config".into()),
1654 ..Default::default()
1655 },
1656 wants: vec![Want::new("config/jj/config.toml")],
1657 }
1658 }
1659
1660 fn config_path_new_prefer_platform() -> TestCase {
1661 TestCase {
1662 files: &[],
1663 env: UnresolvedConfigEnv {
1664 home_dir: Some("home".into()),
1665 user_config_dir: Some("config".into()),
1666 ..Default::default()
1667 },
1668 wants: vec![Want::new("config/jj/config.toml")],
1669 }
1670 }
1671
1672 fn config_path_jj_config_existing() -> TestCase {
1673 TestCase {
1674 files: &["custom.toml"],
1675 env: UnresolvedConfigEnv {
1676 jj_config: Some("custom.toml".into()),
1677 ..Default::default()
1678 },
1679 wants: vec![Want::existing("custom.toml")],
1680 }
1681 }
1682
1683 fn config_path_jj_config_new() -> TestCase {
1684 TestCase {
1685 files: &[],
1686 env: UnresolvedConfigEnv {
1687 jj_config: Some("custom.toml".into()),
1688 ..Default::default()
1689 },
1690 wants: vec![Want::new("custom.toml")],
1691 }
1692 }
1693
1694 fn config_path_jj_config_existing_multiple() -> TestCase {
1695 TestCase {
1696 files: &["custom1.toml", "custom2.toml"],
1697 env: UnresolvedConfigEnv {
1698 jj_config: Some(
1699 join_paths(["custom1.toml", "custom2.toml"])
1700 .unwrap()
1701 .into_string()
1702 .unwrap(),
1703 ),
1704 ..Default::default()
1705 },
1706 wants: vec![
1707 Want::existing("custom1.toml"),
1708 Want::existing("custom2.toml"),
1709 ],
1710 }
1711 }
1712
1713 fn config_path_jj_config_new_multiple() -> TestCase {
1714 TestCase {
1715 files: &["custom1.toml"],
1716 env: UnresolvedConfigEnv {
1717 jj_config: Some(
1718 join_paths(["custom1.toml", "custom2.toml"])
1719 .unwrap()
1720 .into_string()
1721 .unwrap(),
1722 ),
1723 ..Default::default()
1724 },
1725 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1726 }
1727 }
1728
1729 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1730 TestCase {
1731 files: &["custom1.toml"],
1732 env: UnresolvedConfigEnv {
1733 jj_config: Some(
1734 join_paths(["custom1.toml", "", "custom2.toml"])
1735 .unwrap()
1736 .into_string()
1737 .unwrap(),
1738 ),
1739 ..Default::default()
1740 },
1741 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1742 }
1743 }
1744
1745 fn config_path_jj_config_empty() -> TestCase {
1746 TestCase {
1747 files: &[],
1748 env: UnresolvedConfigEnv {
1749 jj_config: Some("".to_owned()),
1750 ..Default::default()
1751 },
1752 wants: vec![],
1753 }
1754 }
1755
1756 fn config_path_config_pick_platform() -> TestCase {
1757 TestCase {
1758 files: &["config/jj/config.toml"],
1759 env: UnresolvedConfigEnv {
1760 home_dir: Some("home".into()),
1761 user_config_dir: Some("config".into()),
1762 ..Default::default()
1763 },
1764 wants: vec![Want::existing("config/jj/config.toml")],
1765 }
1766 }
1767
1768 fn config_path_config_pick_home() -> TestCase {
1769 TestCase {
1770 files: &["home/.jjconfig.toml"],
1771 env: UnresolvedConfigEnv {
1772 home_dir: Some("home".into()),
1773 user_config_dir: Some("config".into()),
1774 ..Default::default()
1775 },
1776 wants: vec![
1777 Want::existing("home/.jjconfig.toml"),
1778 Want::new("config/jj/config.toml"),
1779 ],
1780 }
1781 }
1782
1783 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1784 TestCase {
1785 files: &["config/jj/conf.d/_"],
1786 env: UnresolvedConfigEnv {
1787 home_dir: Some("home".into()),
1788 user_config_dir: Some("config".into()),
1789 ..Default::default()
1790 },
1791 wants: vec![
1792 Want::new("config/jj/config.toml"),
1793 Want::existing("config/jj/conf.d"),
1794 ],
1795 }
1796 }
1797
1798 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1799 TestCase {
1800 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1801 env: UnresolvedConfigEnv {
1802 home_dir: Some("home".into()),
1803 user_config_dir: Some("config".into()),
1804 ..Default::default()
1805 },
1806 wants: vec![
1807 Want::existing("config/jj/config.toml"),
1808 Want::existing("config/jj/conf.d"),
1809 ],
1810 }
1811 }
1812
1813 fn config_path_all_existing() -> TestCase {
1814 TestCase {
1815 files: &[
1816 "config/jj/conf.d/_",
1817 "config/jj/config.toml",
1818 "home/.jjconfig.toml",
1819 ],
1820 env: UnresolvedConfigEnv {
1821 home_dir: Some("home".into()),
1822 user_config_dir: Some("config".into()),
1823 ..Default::default()
1824 },
1825 wants: vec![
1827 Want::existing("home/.jjconfig.toml"),
1828 Want::existing("config/jj/config.toml"),
1829 Want::existing("config/jj/conf.d"),
1830 ],
1831 }
1832 }
1833
1834 fn config_path_none() -> TestCase {
1835 TestCase {
1836 files: &[],
1837 env: Default::default(),
1838 wants: vec![],
1839 }
1840 }
1841
1842 #[test_case(config_path_home_existing())]
1843 #[test_case(config_path_home_new())]
1844 #[test_case(config_path_home_existing_platform_new())]
1845 #[test_case(config_path_platform_existing())]
1846 #[test_case(config_path_platform_new())]
1847 #[test_case(config_path_new_prefer_platform())]
1848 #[test_case(config_path_jj_config_existing())]
1849 #[test_case(config_path_jj_config_new())]
1850 #[test_case(config_path_jj_config_existing_multiple())]
1851 #[test_case(config_path_jj_config_new_multiple())]
1852 #[test_case(config_path_jj_config_empty_paths_filtered())]
1853 #[test_case(config_path_jj_config_empty())]
1854 #[test_case(config_path_config_pick_platform())]
1855 #[test_case(config_path_config_pick_home())]
1856 #[test_case(config_path_platform_new_conf_dir_existing())]
1857 #[test_case(config_path_platform_existing_conf_dir_existing())]
1858 #[test_case(config_path_all_existing())]
1859 #[test_case(config_path_none())]
1860 fn test_config_path(case: TestCase) {
1861 let tmp = setup_config_fs(case.files);
1862 let env = resolve_config_env(&case.env, tmp.path());
1863
1864 let all_expected_paths = case
1865 .wants
1866 .iter()
1867 .map(|w| w.rooted_path(tmp.path()))
1868 .collect_vec();
1869 let exists_expected_paths = case
1870 .wants
1871 .iter()
1872 .filter(|w| w.exists())
1873 .map(|w| w.rooted_path(tmp.path()))
1874 .collect_vec();
1875
1876 let all_paths = env.user_config_paths().collect_vec();
1877 let exists_paths = env.existing_user_config_paths().collect_vec();
1878
1879 assert_eq!(all_paths, all_expected_paths);
1880 assert_eq!(exists_paths, exists_expected_paths);
1881 }
1882
1883 fn system_config_path_none() -> TestCase {
1884 TestCase {
1885 files: &["etc/jj/config.toml", "system/jj/conf.d/_"],
1886 env: Default::default(),
1887 wants: vec![],
1888 }
1889 }
1890
1891 fn system_config_path_existing() -> TestCase {
1892 TestCase {
1893 files: &["system/jj/config.toml", "system/jj/conf.d/_"],
1894 env: UnresolvedConfigEnv {
1895 system_config_dir: Some("system".into()),
1896 ..Default::default()
1897 },
1898 wants: vec![
1899 Want::existing("system/jj/config.toml"),
1900 Want::existing("system/jj/conf.d"),
1901 ],
1902 }
1903 }
1904
1905 fn system_config_path_jj_config() -> TestCase {
1906 TestCase {
1907 files: &["system/jj/config.toml"],
1908 env: UnresolvedConfigEnv {
1909 jj_config: Some("custom.toml".into()),
1910 system_config_dir: Some("system".into()),
1911 ..Default::default()
1912 },
1913 wants: vec![],
1914 }
1915 }
1916
1917 #[test_case(system_config_path_none())]
1918 #[test_case(system_config_path_existing())]
1919 #[test_case(system_config_path_jj_config())]
1920 fn test_system_config_path(case: TestCase) {
1921 let tmp = setup_config_fs(case.files);
1922 let env = resolve_config_env(&case.env, tmp.path());
1923
1924 let all_expected_paths = case
1925 .wants
1926 .iter()
1927 .map(|w| w.rooted_path(tmp.path()))
1928 .collect_vec();
1929 let exists_expected_paths = case
1930 .wants
1931 .iter()
1932 .filter(|w| w.exists())
1933 .map(|w| w.rooted_path(tmp.path()))
1934 .collect_vec();
1935
1936 let all_paths = env
1937 .system_config_paths
1938 .iter()
1939 .map(ConfigPath::as_path)
1940 .collect_vec();
1941 let exists_paths = env.existing_system_config_paths().collect_vec();
1942
1943 assert_eq!(all_paths, all_expected_paths);
1944 assert_eq!(exists_paths, exists_expected_paths);
1945 }
1946
1947 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1948 let tmp = testutils::new_temp_dir();
1949 for file in files {
1950 let path = tmp.path().join(file);
1951 if let Some(parent) = path.parent() {
1952 std::fs::create_dir_all(parent).unwrap();
1953 }
1954 std::fs::File::create(path).unwrap();
1955 }
1956 tmp
1957 }
1958
1959 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1960 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1961 let env = UnresolvedConfigEnv {
1962 user_config_dir: env.user_config_dir.as_ref().map(|p| root.join(p)),
1963 home_dir: home_dir.clone(),
1964 jj_config: env.jj_config.as_ref().map(|p| {
1965 join_paths(split_paths(p).map(|p| {
1966 if p.as_os_str().is_empty() {
1967 return p;
1968 }
1969 root.join(p)
1970 }))
1971 .unwrap()
1972 .into_string()
1973 .unwrap()
1974 }),
1975 system_config_dir: env.system_config_dir.as_ref().map(|p| root.join(p)),
1976 };
1977 ConfigEnv {
1978 home_dir,
1979 root_config_dir: None,
1980 repo_path: None,
1981 workspace_path: None,
1982 system_config_paths: env.resolve_system(),
1983 user_config_paths: env.resolve_user(),
1984 repo_config: None,
1985 workspace_config: None,
1986 command: None,
1987 hostname: None,
1988 environment: HashMap::new(),
1989 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1990 }
1991 }
1992}