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 config_dir: Option<PathBuf>,
279 home_dir: Option<PathBuf>,
280 jj_config: Option<String>,
281}
282
283impl UnresolvedConfigEnv {
284 fn root_config_dir(&self) -> Option<PathBuf> {
285 self.config_dir.as_deref().map(|c| c.join("jj"))
286 }
287
288 fn resolve(self) -> Vec<ConfigPath> {
289 if let Some(paths) = self.jj_config {
290 return split_paths(&paths)
291 .filter(|path| !path.as_os_str().is_empty())
292 .map(ConfigPath::new)
293 .collect();
294 }
295
296 let mut paths = vec![];
297 let home_config_path = self.home_dir.map(|mut home_dir| {
298 home_dir.push(".jjconfig.toml");
299 ConfigPath::new(home_dir)
300 });
301 let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
302 config_dir.push("jj");
303 config_dir.push("config.toml");
304 ConfigPath::new(config_dir)
305 });
306 let platform_config_dir = self.config_dir.map(|mut config_dir| {
307 config_dir.push("jj");
308 config_dir.push("conf.d");
309 ConfigPath::new(config_dir)
310 });
311
312 if let Some(path) = home_config_path
313 && (path.exists() || platform_config_path.is_none())
314 {
315 paths.push(path);
316 }
317
318 if let Some(path) = platform_config_path {
321 paths.push(path);
322 }
323
324 if let Some(path) = platform_config_dir
325 && path.exists()
326 {
327 paths.push(path);
328 }
329
330 paths
331 }
332}
333
334#[derive(Clone, Debug)]
335pub struct ConfigEnv {
336 home_dir: Option<PathBuf>,
337 root_config_dir: Option<PathBuf>,
338 repo_path: Option<PathBuf>,
339 workspace_path: Option<PathBuf>,
340 user_config_paths: Vec<ConfigPath>,
341 repo_config: Option<SecureConfig>,
342 workspace_config: Option<SecureConfig>,
343 command: Option<String>,
344 hostname: Option<String>,
345 environment: HashMap<String, String>,
346 rng: Arc<Mutex<ChaCha20Rng>>,
347}
348
349impl ConfigEnv {
350 pub fn from_environment() -> Self {
352 let config_dir = etcetera::choose_base_strategy()
353 .ok()
354 .map(|s| s.config_dir());
355
356 let home_dir = etcetera::home_dir()
359 .ok()
360 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
361
362 let env = UnresolvedConfigEnv {
363 config_dir,
364 home_dir: home_dir.clone(),
365 jj_config: env::var("JJ_CONFIG").ok(),
366 };
367 let environment = env::vars_os()
368 .filter_map(|(k, v)| {
369 let k = k.into_string().ok()?;
371 let v = v.into_string().ok()?;
372 Some((k, v))
373 })
374 .collect();
375 Self {
376 home_dir,
377 root_config_dir: env.root_config_dir(),
378 repo_path: None,
379 workspace_path: None,
380 user_config_paths: env.resolve(),
381 repo_config: None,
382 workspace_config: None,
383 command: None,
384 hostname: whoami::hostname().ok(),
385 environment,
386 rng: Arc::new(Mutex::new(
389 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) {
390 ChaCha20Rng::seed_from_u64(value)
391 } else {
392 rand::make_rng()
393 },
394 )),
395 }
396 }
397
398 pub fn set_command_name(&mut self, command: String) {
399 self.command = Some(command);
400 }
401
402 fn load_secure_config(
403 &self,
404 ui: &Ui,
405 config: Option<&SecureConfig>,
406 kind: &str,
407 force: bool,
408 ) -> Result<Option<LoadedSecureConfig>, CommandError> {
409 Ok(match (config, self.root_config_dir.as_ref()) {
410 (Some(config), Some(root_config_dir)) => {
411 let mut guard = self.rng.lock().unwrap();
412 let loaded_config = if force {
413 config.load_config(&mut guard, &root_config_dir.join(kind))
414 } else {
415 config.maybe_load_config(&mut guard, &root_config_dir.join(kind))
416 }?;
417 for warning in &loaded_config.warnings {
418 writeln!(ui.warning_default(), "{warning}")?;
419 }
420 Some(loaded_config)
421 }
422 _ => None,
423 })
424 }
425
426 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
428 self.user_config_paths.iter().map(ConfigPath::as_path)
429 }
430
431 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
434 self.user_config_paths
435 .iter()
436 .filter(|p| p.exists())
437 .map(ConfigPath::as_path)
438 }
439
440 pub fn user_config_files(&self, config: &RawConfig) -> Result<Vec<ConfigFile>, CommandError> {
447 config_files_for(config, ConfigSource::User, || {
448 Ok(self.new_user_config_file()?)
449 })
450 }
451
452 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
453 self.user_config_paths()
454 .next()
455 .map(|path| {
456 if let Some(dir) = path.parent() {
459 create_dir_all(dir).ok();
460 }
461 ConfigFile::load_or_empty(ConfigSource::User, path)
464 })
465 .transpose()
466 }
467
468 #[instrument]
471 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
472 config.as_mut().remove_layers(ConfigSource::User);
473 for path in self.existing_user_config_paths() {
474 if path.is_dir() {
475 config.as_mut().load_dir(ConfigSource::User, path)?;
476 } else {
477 config.as_mut().load_file(ConfigSource::User, path)?;
478 }
479 }
480 Ok(())
481 }
482
483 pub fn reset_repo_path(&mut self, path: &Path) {
486 self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf()));
487 self.repo_path = Some(path.to_owned());
488 }
489
490 fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
492 Ok(self
493 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)?
494 .and_then(|c| c.config_file))
495 }
496
497 pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
501 Ok(self
502 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)?
503 .and_then(|c| c.config_file))
504 }
505
506 pub fn repo_config_files(
513 &self,
514 ui: &Ui,
515 config: &RawConfig,
516 ) -> Result<Vec<ConfigFile>, CommandError> {
517 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file(ui))
518 }
519
520 fn new_repo_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
521 Ok(self
522 .repo_config_path(ui)?
523 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
526 .transpose()?)
527 }
528
529 #[instrument(skip(ui))]
532 pub fn reload_repo_config(&self, ui: &Ui, config: &mut RawConfig) -> Result<(), CommandError> {
533 config.as_mut().remove_layers(ConfigSource::Repo);
534 if let Some(path) = self.maybe_repo_config_path(ui)?
535 && path.exists()
536 {
537 config.as_mut().load_file(ConfigSource::Repo, path)?;
538 }
539 Ok(())
540 }
541
542 pub fn reset_workspace_path(&mut self, path: &Path) {
544 self.workspace_config = Some(SecureConfig::new_workspace(path.join(".jj")));
545 self.workspace_path = Some(path.to_owned());
546 }
547
548 fn maybe_workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
550 Ok(self
551 .load_secure_config(
552 ui,
553 self.workspace_config.as_ref(),
554 WORKSPACE_CONFIG_DIR,
555 false,
556 )?
557 .and_then(|c| c.config_file))
558 }
559
560 pub fn workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
564 Ok(self
565 .load_secure_config(
566 ui,
567 self.workspace_config.as_ref(),
568 WORKSPACE_CONFIG_DIR,
569 true,
570 )?
571 .and_then(|c| c.config_file))
572 }
573
574 pub fn workspace_config_files(
581 &self,
582 ui: &Ui,
583 config: &RawConfig,
584 ) -> Result<Vec<ConfigFile>, CommandError> {
585 config_files_for(config, ConfigSource::Workspace, || {
586 self.new_workspace_config_file(ui)
587 })
588 }
589
590 fn new_workspace_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
591 Ok(self
592 .workspace_config_path(ui)?
593 .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
594 .transpose()?)
595 }
596
597 #[instrument(skip(ui))]
600 pub fn reload_workspace_config(
601 &self,
602 ui: &Ui,
603 config: &mut RawConfig,
604 ) -> Result<(), CommandError> {
605 config.as_mut().remove_layers(ConfigSource::Workspace);
606 if let Some(path) = self.maybe_workspace_config_path(ui)?
607 && path.exists()
608 {
609 config.as_mut().load_file(ConfigSource::Workspace, path)?;
610 }
611 Ok(())
612 }
613
614 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
617 let context = ConfigResolutionContext {
618 home_dir: self.home_dir.as_deref(),
619 repo_path: self.repo_path.as_deref(),
620 workspace_path: self.workspace_path.as_deref(),
621 command: self.command.as_deref(),
622 hostname: self.hostname.as_deref().unwrap_or(""),
623 environment: &self.environment,
624 };
625 jj_lib::config::resolve(config.as_ref(), &context)
626 }
627}
628
629pub fn existing_repo_config_file(config: &RawConfig) -> Option<ConfigFile> {
632 config
634 .as_ref()
635 .layers_for(ConfigSource::Repo)
636 .iter()
637 .find_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
638}
639
640fn config_files_for(
641 config: &RawConfig,
642 source: ConfigSource,
643 new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
644) -> Result<Vec<ConfigFile>, CommandError> {
645 let mut files = config
646 .as_ref()
647 .layers_for(source)
648 .iter()
649 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
650 .collect_vec();
651 if files.is_empty() {
652 files.extend(new_file()?);
653 }
654 Ok(files)
655}
656
657pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
671 let mut config = StackedConfig::with_defaults();
672 config.extend_layers(default_layers);
673 config.add_layer(env_base_layer());
674 config.add_layer(env_overrides_layer());
675 RawConfig(config)
676}
677
678const OP_HOSTNAME: &str = "operation.hostname";
679const OP_USERNAME: &str = "operation.username";
680
681fn env_base_layer() -> ConfigLayer {
683 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
684 if let Ok(value) =
685 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
686 {
687 layer.set_value(OP_HOSTNAME, value).unwrap();
688 }
689 if let Ok(value) =
690 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
691 {
692 layer.set_value(OP_USERNAME, value).unwrap();
693 } else if let Ok(value) = env::var("USER") {
694 layer.set_value(OP_USERNAME, value).unwrap();
697 }
698 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
699 layer.set_value("ui.color", "never").unwrap();
702 }
703 if let Ok(value) = env::var("VISUAL") {
704 layer.set_value("ui.editor", value).unwrap();
705 } else if let Ok(value) = env::var("EDITOR") {
706 layer.set_value("ui.editor", value).unwrap();
707 }
708 layer
711}
712
713pub fn default_config_layers() -> Vec<ConfigLayer> {
714 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
717 let mut layers = vec![
718 parse(include_str!("config/colors.toml")),
719 parse(include_str!("config/hints.toml")),
720 parse(include_str!("config/merge_tools.toml")),
721 parse(include_str!("config/misc.toml")),
722 parse(include_str!("config/revsets.toml")),
723 parse(include_str!("config/templates.toml")),
724 ];
725 if cfg!(unix) {
726 layers.push(parse(include_str!("config/unix.toml")));
727 }
728 if cfg!(windows) {
729 layers.push(parse(include_str!("config/windows.toml")));
730 }
731 layers
732}
733
734fn env_overrides_layer() -> ConfigLayer {
736 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
737 if let Ok(value) = env::var("JJ_USER") {
738 layer.set_value("user.name", value).unwrap();
739 }
740 if let Ok(value) = env::var("JJ_EMAIL") {
741 layer.set_value("user.email", value).unwrap();
742 }
743 if let Ok(value) = env::var("JJ_TIMESTAMP") {
744 layer.set_value("debug.commit-timestamp", value).unwrap();
745 }
746 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
747 layer.set_value("debug.randomness-seed", value).unwrap();
748 }
749 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
750 layer.set_value("debug.operation-timestamp", value).unwrap();
751 }
752 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
753 layer.set_value(OP_HOSTNAME, value).unwrap();
754 }
755 if let Ok(value) = env::var("JJ_OP_USERNAME") {
756 layer.set_value(OP_USERNAME, value).unwrap();
757 }
758 if let Ok(value) = env::var("JJ_EDITOR") {
759 layer.set_value("ui.editor", value).unwrap();
760 }
761 if let Ok(value) = env::var("JJ_PAGER") {
762 layer.set_value("ui.pager", value).unwrap();
763 }
764 layer
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
769pub enum ConfigArgKind {
770 Item,
772 File,
774}
775
776pub fn parse_config_args(
778 toml_strs: &[(ConfigArgKind, &str)],
779) -> Result<Vec<ConfigLayer>, CommandError> {
780 let source = ConfigSource::CommandArg;
781 let mut layers = Vec::new();
782 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
783 match kind {
784 ConfigArgKind::Item => {
785 let mut layer = ConfigLayer::empty(source);
786 for (_, item) in chunk {
787 let (name, value) = parse_config_arg_item(item)?;
788 layer.set_value(name, value).map_err(|err| {
791 config_error_with_message("--config argument cannot be set", err)
792 })?;
793 }
794 layers.push(layer);
795 }
796 ConfigArgKind::File => {
797 for (_, path) in chunk {
798 layers.push(ConfigLayer::load_from_file(source, path.into())?);
799 }
800 }
801 }
802 }
803 Ok(layers)
804}
805
806fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
808 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
810 let Some((name, value_str)) = split_candidates
811 .map(|p| (&item_str[..p], &item_str[p + 1..]))
812 .map(|(name, value)| name.parse().map(|name| (name, value)))
813 .find_or_last(Result::is_ok)
814 .transpose()
815 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
816 else {
817 return Err(config_error("--config must be specified as NAME=VALUE"));
818 };
819 let value = parse_value_or_bare_string(value_str)
820 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
821 Ok((name, value))
822}
823
824pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
826 vec![]
827}
828
829#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
831#[serde(untagged)]
832pub enum CommandNameAndArgs {
833 String(String),
834 Vec(NonEmptyCommandArgsVec),
835 Structured {
836 env: HashMap<String, String>,
837 command: NonEmptyCommandArgsVec,
838 },
839}
840
841impl CommandNameAndArgs {
842 pub fn split_name(&self) -> Cow<'_, str> {
844 let (name, _) = self.split_name_and_args();
845 name
846 }
847
848 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
852 match self {
853 Self::String(s) => {
854 if s.contains('"') || s.contains('\'') {
855 let mut parts = shlex::Shlex::new(s);
856 let res = (
857 parts.next().unwrap_or_default().into(),
858 parts.by_ref().collect(),
859 );
860 if !parts.had_error {
861 return res;
862 }
863 }
864 let mut args = s.split(' ').map(|s| s.to_owned());
865 (args.next().unwrap().into(), args.collect())
866 }
867 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
868 Self::Structured {
869 env: _,
870 command: cmd,
871 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
872 }
873 }
874
875 pub fn as_str(&self) -> Option<&str> {
880 match self {
881 Self::String(s) => Some(s),
882 Self::Vec(_) | Self::Structured { .. } => None,
883 }
884 }
885
886 pub fn to_command(&self) -> Command {
888 let empty: HashMap<&str, &str> = HashMap::new();
889 self.to_command_with_variables(&empty)
890 }
891
892 pub fn to_command_with_variables<V: AsRef<str>>(
895 &self,
896 variables: &HashMap<&str, V>,
897 ) -> Command {
898 let (name, args) = self.split_name_and_args();
899 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
900 if let Self::Structured { env, .. } = self {
901 cmd.envs(env);
902 }
903 cmd.args(interpolate_variables(&args, variables));
904 cmd
905 }
906}
907
908impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
909 fn from(s: &T) -> Self {
910 Self::String(s.as_ref().to_owned())
911 }
912}
913
914impl fmt::Display for CommandNameAndArgs {
915 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916 match self {
917 Self::String(s) => write!(f, "{s}"),
918 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
920 Self::Structured { env, command } => {
921 for (k, v) in env {
922 write!(f, "{k}={v} ")?;
923 }
924 write!(f, "{}", command.0.join(" "))
925 }
926 }
927 }
928}
929
930pub fn load_aliases_map<P>(
931 ui: &Ui,
932 config: &StackedConfig,
933 table_name: &ConfigNamePathBuf,
934) -> Result<AliasesMap<P, String>, CommandError>
935where
936 P: AliasDeclarationParser + Default,
937 P::Error: fmt::Display,
938{
939 let mut aliases_map = AliasesMap::new();
940 for layer in config.layers() {
943 let table = match layer.look_up_table(table_name) {
944 Ok(Some(table)) => table,
945 Ok(None) => continue,
946 Err(item) => {
947 return Err(ConfigGetError::Type {
948 name: table_name.to_string(),
949 error: format!("Expected a table, but is {}", item.type_name()).into(),
950 source_path: layer.path.clone(),
951 }
952 .into());
953 }
954 };
955 for (decl, item) in table.iter() {
956 let (definition, doc) = if let Some(t) = item.as_table_like() {
957 let definition = t.get("definition").and_then(|i| i.as_str());
958 let doc = t.get("doc").and_then(|i| i.as_str()).map(|s| s.to_owned());
959 (definition, doc)
960 } else {
961 (item.as_str(), None)
962 };
963
964 let r = definition
965 .ok_or_else(|| {
966 format!(
967 "Expected a string or a table with a `definition` string key, but is {}",
968 item.type_name()
969 )
970 })
971 .and_then(|v| aliases_map.insert(decl, v, doc).map_err(|e| format!("{e}")));
972 if let Err(s) = r {
973 writeln!(
974 ui.warning_default(),
975 "Failed to load `{table_name}.{decl}`: {s}"
976 )?;
977 }
978 }
979 }
980 Ok(aliases_map)
981}
982
983static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
985
986pub fn interpolate_variables<V: AsRef<str>>(
987 args: &[String],
988 variables: &HashMap<&str, V>,
989) -> Vec<String> {
990 args.iter()
991 .map(|arg| interpolate_variables_single(arg, variables))
992 .collect()
993}
994
995fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
996 VARIABLE_REGEX
997 .replace_all(arg, |caps: &Captures| {
998 let name = &caps[1];
999 if let Some(subst) = variables.get(name) {
1000 subst.as_ref().to_owned()
1001 } else {
1002 caps[0].to_owned()
1003 }
1004 })
1005 .into_owned()
1006}
1007
1008pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
1010 let regex = &*VARIABLE_REGEX;
1011 args.iter()
1012 .flat_map(|arg| regex.find_iter(arg))
1013 .map(|single_match| {
1014 let s = single_match.as_str();
1015 &s[1..]
1016 })
1017}
1018
1019#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
1022#[serde(try_from = "Vec<String>")]
1023pub struct NonEmptyCommandArgsVec(Vec<String>);
1024
1025impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
1026 type Error = &'static str;
1027
1028 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
1029 if args.is_empty() {
1030 Err("command arguments should not be empty")
1031 } else {
1032 Ok(Self(args))
1033 }
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use std::env::join_paths;
1040 use std::fmt::Write as _;
1041
1042 use indoc::indoc;
1043 use maplit::hashmap;
1044 use test_case::test_case;
1045 use testutils::TestResult;
1046
1047 use super::*;
1048
1049 fn insta_settings() -> insta::Settings {
1050 let mut settings = insta::Settings::clone_current();
1051 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1053 settings
1054 }
1055
1056 #[test]
1057 fn test_parse_value_or_bare_string() -> TestResult {
1058 let parse = |s: &str| parse_value_or_bare_string(s);
1059
1060 assert_eq!(parse("true")?.as_bool(), Some(true));
1062 assert_eq!(parse("42")?.as_integer(), Some(42));
1063 assert_eq!(parse("-1")?.as_integer(), Some(-1));
1064 assert_eq!(parse("'a'")?.as_str(), Some("a"));
1065 assert!(parse("[]")?.is_array());
1066 assert!(parse("{ a = 'b' }")?.is_inline_table());
1067
1068 assert_eq!(parse("")?.as_str(), Some(""));
1070 assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
1071 assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
1072 assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
1073 assert_eq!(
1074 parse("<foo+bar@example.org>")?.as_str(),
1075 Some("<foo+bar@example.org>")
1076 );
1077 assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
1078 assert_eq!(parse("all()")?.as_str(), Some("all()"));
1079 assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
1080 assert_eq!(parse("柔術")?.as_str(), Some("柔術"));
1081
1082 assert!(parse("'foo").is_err());
1084 assert!(parse(r#" bar" "#).is_err());
1085 assert!(parse("[0 1]").is_err());
1086 assert!(parse("{ x = y }").is_err());
1087 assert!(parse("\n { x").is_err());
1088 assert!(parse(" x ] ").is_err());
1089 assert!(parse("[table]\nkey = 'value'").is_err());
1090 Ok(())
1091 }
1092
1093 #[test]
1094 fn test_parse_config_arg_item() {
1095 assert!(parse_config_arg_item("").is_err());
1096 assert!(parse_config_arg_item("a").is_err());
1097 assert!(parse_config_arg_item("=").is_err());
1098 assert!(parse_config_arg_item("a = 'b'").is_err());
1101
1102 let (name, value) = parse_config_arg_item("a=b").unwrap();
1103 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1104 assert_eq!(value.as_str(), Some("b"));
1105
1106 let (name, value) = parse_config_arg_item("a=").unwrap();
1107 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1108 assert_eq!(value.as_str(), Some(""));
1109
1110 let (name, value) = parse_config_arg_item("a= ").unwrap();
1111 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1112 assert_eq!(value.as_str(), Some(" "));
1113
1114 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1116 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1117 assert_eq!(value.as_str(), Some("b=c"));
1118
1119 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1120 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1121 assert_eq!(value.as_bool(), Some(true));
1122
1123 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1124 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1125 assert_eq!(value.as_str(), Some("b=c"));
1126
1127 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1128 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1129 assert_eq!(value.as_str(), Some("c"));
1130
1131 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1132 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1133 assert!(value.is_inline_table());
1134 assert_eq!(value.to_string(), "{d = 'e=f'}");
1135 }
1136
1137 #[test]
1138 fn test_command_args() -> TestResult {
1139 let mut config = StackedConfig::empty();
1140 config.add_layer(ConfigLayer::parse(
1141 ConfigSource::User,
1142 indoc! {"
1143 empty_array = []
1144 empty_string = ''
1145 array = ['emacs', '-nw']
1146 string = 'emacs -nw'
1147 string_quoted = '\"spaced path/to/emacs\" -nw'
1148 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1149 structured.command = ['emacs', '-nw']
1150 "},
1151 )?);
1152
1153 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1154
1155 let command_args: CommandNameAndArgs = config.get("empty_string")?;
1156 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1157 let (name, args) = command_args.split_name_and_args();
1158 assert_eq!(name, "");
1159 assert!(args.is_empty());
1160
1161 let command_args: CommandNameAndArgs = config.get("array")?;
1162 assert_eq!(
1163 command_args,
1164 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1165 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1166 ))
1167 );
1168 let (name, args) = command_args.split_name_and_args();
1169 assert_eq!(name, "emacs");
1170 assert_eq!(args, ["-nw"].as_ref());
1171
1172 let command_args: CommandNameAndArgs = config.get("string")?;
1173 assert_eq!(
1174 command_args,
1175 CommandNameAndArgs::String("emacs -nw".to_owned())
1176 );
1177 let (name, args) = command_args.split_name_and_args();
1178 assert_eq!(name, "emacs");
1179 assert_eq!(args, ["-nw"].as_ref());
1180
1181 let command_args: CommandNameAndArgs = config.get("string_quoted")?;
1182 assert_eq!(
1183 command_args,
1184 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1185 );
1186 let (name, args) = command_args.split_name_and_args();
1187 assert_eq!(name, "spaced path/to/emacs");
1188 assert_eq!(args, ["-nw"].as_ref());
1189
1190 let command_args: CommandNameAndArgs = config.get("structured")?;
1191 assert_eq!(
1192 command_args,
1193 CommandNameAndArgs::Structured {
1194 env: hashmap! {
1195 "KEY1".to_string() => "value1".to_string(),
1196 "KEY2".to_string() => "value2".to_string(),
1197 },
1198 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1199 }
1200 );
1201 let (name, args) = command_args.split_name_and_args();
1202 assert_eq!(name, "emacs");
1203 assert_eq!(args, ["-nw"].as_ref());
1204 Ok(())
1205 }
1206
1207 #[test]
1208 fn test_resolved_config_values_empty() {
1209 let config = StackedConfig::empty();
1210 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1211 }
1212
1213 #[test]
1214 fn test_resolved_config_values_single_key() -> TestResult {
1215 let settings = insta_settings();
1216 let _guard = settings.bind_to_scope();
1217 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1218 env_base_layer.set_value("user.name", "base-user-name")?;
1219 env_base_layer.set_value("user.email", "base@user.email")?;
1220 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1221 repo_layer.set_value("user.email", "repo@user.email")?;
1222 let mut config = StackedConfig::empty();
1223 config.add_layer(env_base_layer);
1224 config.add_layer(repo_layer);
1225 insta::assert_debug_snapshot!(
1227 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1228 @r#"
1229 [
1230 AnnotatedValue {
1231 name: ConfigNamePathBuf(
1232 [
1233 Key {
1234 key: "user",
1235 repr: None,
1236 leaf_decor: Decor { .. },
1237 dotted_decor: Decor { .. },
1238 },
1239 Key {
1240 key: "name",
1241 repr: None,
1242 leaf_decor: Decor { .. },
1243 dotted_decor: Decor { .. },
1244 },
1245 ],
1246 ),
1247 value: String(
1248 Formatted {
1249 value: "base-user-name",
1250 repr: "default",
1251 decor: Decor { .. },
1252 },
1253 ),
1254 source: EnvBase,
1255 path: None,
1256 is_overridden: false,
1257 },
1258 AnnotatedValue {
1259 name: ConfigNamePathBuf(
1260 [
1261 Key {
1262 key: "user",
1263 repr: None,
1264 leaf_decor: Decor { .. },
1265 dotted_decor: Decor { .. },
1266 },
1267 Key {
1268 key: "email",
1269 repr: None,
1270 leaf_decor: Decor { .. },
1271 dotted_decor: Decor { .. },
1272 },
1273 ],
1274 ),
1275 value: String(
1276 Formatted {
1277 value: "base@user.email",
1278 repr: "default",
1279 decor: Decor { .. },
1280 },
1281 ),
1282 source: EnvBase,
1283 path: None,
1284 is_overridden: true,
1285 },
1286 AnnotatedValue {
1287 name: ConfigNamePathBuf(
1288 [
1289 Key {
1290 key: "user",
1291 repr: None,
1292 leaf_decor: Decor { .. },
1293 dotted_decor: Decor { .. },
1294 },
1295 Key {
1296 key: "email",
1297 repr: None,
1298 leaf_decor: Decor { .. },
1299 dotted_decor: Decor { .. },
1300 },
1301 ],
1302 ),
1303 value: String(
1304 Formatted {
1305 value: "repo@user.email",
1306 repr: "default",
1307 decor: Decor { .. },
1308 },
1309 ),
1310 source: Repo,
1311 path: None,
1312 is_overridden: false,
1313 },
1314 ]
1315 "#
1316 );
1317 Ok(())
1318 }
1319
1320 #[test]
1321 fn test_resolved_config_values_filter_path() -> TestResult {
1322 let settings = insta_settings();
1323 let _guard = settings.bind_to_scope();
1324 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1325 user_layer.set_value("test-table1.foo", "user-FOO")?;
1326 user_layer.set_value("test-table2.bar", "user-BAR")?;
1327 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1328 repo_layer.set_value("test-table1.bar", "repo-BAR")?;
1329 let mut config = StackedConfig::empty();
1330 config.add_layer(user_layer);
1331 config.add_layer(repo_layer);
1332 insta::assert_debug_snapshot!(
1333 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1334 @r#"
1335 [
1336 AnnotatedValue {
1337 name: ConfigNamePathBuf(
1338 [
1339 Key {
1340 key: "test-table1",
1341 repr: None,
1342 leaf_decor: Decor { .. },
1343 dotted_decor: Decor { .. },
1344 },
1345 Key {
1346 key: "foo",
1347 repr: None,
1348 leaf_decor: Decor { .. },
1349 dotted_decor: Decor { .. },
1350 },
1351 ],
1352 ),
1353 value: String(
1354 Formatted {
1355 value: "user-FOO",
1356 repr: "default",
1357 decor: Decor { .. },
1358 },
1359 ),
1360 source: User,
1361 path: None,
1362 is_overridden: false,
1363 },
1364 AnnotatedValue {
1365 name: ConfigNamePathBuf(
1366 [
1367 Key {
1368 key: "test-table1",
1369 repr: None,
1370 leaf_decor: Decor { .. },
1371 dotted_decor: Decor { .. },
1372 },
1373 Key {
1374 key: "bar",
1375 repr: None,
1376 leaf_decor: Decor { .. },
1377 dotted_decor: Decor { .. },
1378 },
1379 ],
1380 ),
1381 value: String(
1382 Formatted {
1383 value: "repo-BAR",
1384 repr: "default",
1385 decor: Decor { .. },
1386 },
1387 ),
1388 source: Repo,
1389 path: None,
1390 is_overridden: false,
1391 },
1392 ]
1393 "#
1394 );
1395 Ok(())
1396 }
1397
1398 #[test]
1399 fn test_resolved_config_values_overridden() -> TestResult {
1400 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1401 let mut config = StackedConfig::empty();
1402 config.extend_layers(layers.iter().copied().cloned());
1403 let prefix = if prefix.is_empty() {
1404 ConfigNamePathBuf::root()
1405 } else {
1406 prefix.parse().unwrap()
1407 };
1408 let mut output = String::new();
1409 for annotated in resolved_config_values(&config, &prefix) {
1410 let AnnotatedValue { name, value, .. } = &annotated;
1411 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1412 writeln!(output, "{sigil}{name} = {value}").unwrap();
1413 }
1414 output
1415 };
1416
1417 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1418 layer0.set_value("a.b.e", "0.0")?;
1419 layer0.set_value("a.b.c.f", "0.1")?;
1420 layer0.set_value("a.b.d", "0.2")?;
1421 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1422 layer1.set_value("a.b", "1.0")?;
1423 layer1.set_value("a.c", "1.1")?;
1424 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1425 layer2.set_value("a.b.g", "2.0")?;
1426 layer2.set_value("a.b.d", "2.1")?;
1427
1428 let layers = [&layer0, &layer1];
1430 insta::assert_snapshot!(list(&layers, ""), @r#"
1431 !a.b.e = "0.0"
1432 !a.b.c.f = "0.1"
1433 !a.b.d = "0.2"
1434 a.b = "1.0"
1435 a.c = "1.1"
1436 "#);
1437 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1438 !a.b.e = "0.0"
1439 !a.b.c.f = "0.1"
1440 !a.b.d = "0.2"
1441 a.b = "1.0"
1442 "#);
1443 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1444 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1445
1446 let layers = [&layer1, &layer2];
1448 insta::assert_snapshot!(list(&layers, ""), @r#"
1449 !a.b = "1.0"
1450 a.c = "1.1"
1451 a.b.g = "2.0"
1452 a.b.d = "2.1"
1453 "#);
1454 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1455 !a.b = "1.0"
1456 a.b.g = "2.0"
1457 a.b.d = "2.1"
1458 "#);
1459
1460 let layers = [&layer0, &layer2];
1462 insta::assert_snapshot!(list(&layers, ""), @r#"
1463 a.b.e = "0.0"
1464 a.b.c.f = "0.1"
1465 !a.b.d = "0.2"
1466 a.b.g = "2.0"
1467 a.b.d = "2.1"
1468 "#);
1469 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1470 a.b.e = "0.0"
1471 a.b.c.f = "0.1"
1472 !a.b.d = "0.2"
1473 a.b.g = "2.0"
1474 a.b.d = "2.1"
1475 "#);
1476 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1477 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1478 !a.b.d = "0.2"
1479 a.b.d = "2.1"
1480 "#);
1481
1482 let layers = [&layer0, &layer1, &layer2];
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 a.b.g = "2.0"
1491 a.b.d = "2.1"
1492 "#);
1493 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1494 !a.b.e = "0.0"
1495 !a.b.c.f = "0.1"
1496 !a.b.d = "0.2"
1497 !a.b = "1.0"
1498 a.b.g = "2.0"
1499 a.b.d = "2.1"
1500 "#);
1501 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1502 Ok(())
1503 }
1504
1505 struct TestCase {
1506 files: &'static [&'static str],
1507 env: UnresolvedConfigEnv,
1508 wants: Vec<Want>,
1509 }
1510
1511 #[derive(Debug)]
1512 enum WantState {
1513 New,
1514 Existing,
1515 }
1516 #[derive(Debug)]
1517 struct Want {
1518 path: &'static str,
1519 state: WantState,
1520 }
1521
1522 impl Want {
1523 const fn new(path: &'static str) -> Self {
1524 Self {
1525 path,
1526 state: WantState::New,
1527 }
1528 }
1529
1530 const fn existing(path: &'static str) -> Self {
1531 Self {
1532 path,
1533 state: WantState::Existing,
1534 }
1535 }
1536
1537 fn rooted_path(&self, root: &Path) -> PathBuf {
1538 root.join(self.path)
1539 }
1540
1541 fn exists(&self) -> bool {
1542 matches!(self.state, WantState::Existing)
1543 }
1544 }
1545
1546 fn config_path_home_existing() -> TestCase {
1547 TestCase {
1548 files: &["home/.jjconfig.toml"],
1549 env: UnresolvedConfigEnv {
1550 home_dir: Some("home".into()),
1551 ..Default::default()
1552 },
1553 wants: vec![Want::existing("home/.jjconfig.toml")],
1554 }
1555 }
1556
1557 fn config_path_home_new() -> TestCase {
1558 TestCase {
1559 files: &[],
1560 env: UnresolvedConfigEnv {
1561 home_dir: Some("home".into()),
1562 ..Default::default()
1563 },
1564 wants: vec![Want::new("home/.jjconfig.toml")],
1565 }
1566 }
1567
1568 fn config_path_home_existing_platform_new() -> TestCase {
1569 TestCase {
1570 files: &["home/.jjconfig.toml"],
1571 env: UnresolvedConfigEnv {
1572 home_dir: Some("home".into()),
1573 config_dir: Some("config".into()),
1574 ..Default::default()
1575 },
1576 wants: vec![
1577 Want::existing("home/.jjconfig.toml"),
1578 Want::new("config/jj/config.toml"),
1579 ],
1580 }
1581 }
1582
1583 fn config_path_platform_existing() -> TestCase {
1584 TestCase {
1585 files: &["config/jj/config.toml"],
1586 env: UnresolvedConfigEnv {
1587 home_dir: Some("home".into()),
1588 config_dir: Some("config".into()),
1589 ..Default::default()
1590 },
1591 wants: vec![Want::existing("config/jj/config.toml")],
1592 }
1593 }
1594
1595 fn config_path_platform_new() -> TestCase {
1596 TestCase {
1597 files: &[],
1598 env: UnresolvedConfigEnv {
1599 config_dir: Some("config".into()),
1600 ..Default::default()
1601 },
1602 wants: vec![Want::new("config/jj/config.toml")],
1603 }
1604 }
1605
1606 fn config_path_new_prefer_platform() -> TestCase {
1607 TestCase {
1608 files: &[],
1609 env: UnresolvedConfigEnv {
1610 home_dir: Some("home".into()),
1611 config_dir: Some("config".into()),
1612 ..Default::default()
1613 },
1614 wants: vec![Want::new("config/jj/config.toml")],
1615 }
1616 }
1617
1618 fn config_path_jj_config_existing() -> TestCase {
1619 TestCase {
1620 files: &["custom.toml"],
1621 env: UnresolvedConfigEnv {
1622 jj_config: Some("custom.toml".into()),
1623 ..Default::default()
1624 },
1625 wants: vec![Want::existing("custom.toml")],
1626 }
1627 }
1628
1629 fn config_path_jj_config_new() -> TestCase {
1630 TestCase {
1631 files: &[],
1632 env: UnresolvedConfigEnv {
1633 jj_config: Some("custom.toml".into()),
1634 ..Default::default()
1635 },
1636 wants: vec![Want::new("custom.toml")],
1637 }
1638 }
1639
1640 fn config_path_jj_config_existing_multiple() -> TestCase {
1641 TestCase {
1642 files: &["custom1.toml", "custom2.toml"],
1643 env: UnresolvedConfigEnv {
1644 jj_config: Some(
1645 join_paths(["custom1.toml", "custom2.toml"])
1646 .unwrap()
1647 .into_string()
1648 .unwrap(),
1649 ),
1650 ..Default::default()
1651 },
1652 wants: vec![
1653 Want::existing("custom1.toml"),
1654 Want::existing("custom2.toml"),
1655 ],
1656 }
1657 }
1658
1659 fn config_path_jj_config_new_multiple() -> TestCase {
1660 TestCase {
1661 files: &["custom1.toml"],
1662 env: UnresolvedConfigEnv {
1663 jj_config: Some(
1664 join_paths(["custom1.toml", "custom2.toml"])
1665 .unwrap()
1666 .into_string()
1667 .unwrap(),
1668 ),
1669 ..Default::default()
1670 },
1671 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1672 }
1673 }
1674
1675 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1676 TestCase {
1677 files: &["custom1.toml"],
1678 env: UnresolvedConfigEnv {
1679 jj_config: Some(
1680 join_paths(["custom1.toml", "", "custom2.toml"])
1681 .unwrap()
1682 .into_string()
1683 .unwrap(),
1684 ),
1685 ..Default::default()
1686 },
1687 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1688 }
1689 }
1690
1691 fn config_path_jj_config_empty() -> TestCase {
1692 TestCase {
1693 files: &[],
1694 env: UnresolvedConfigEnv {
1695 jj_config: Some("".to_owned()),
1696 ..Default::default()
1697 },
1698 wants: vec![],
1699 }
1700 }
1701
1702 fn config_path_config_pick_platform() -> TestCase {
1703 TestCase {
1704 files: &["config/jj/config.toml"],
1705 env: UnresolvedConfigEnv {
1706 home_dir: Some("home".into()),
1707 config_dir: Some("config".into()),
1708 ..Default::default()
1709 },
1710 wants: vec![Want::existing("config/jj/config.toml")],
1711 }
1712 }
1713
1714 fn config_path_config_pick_home() -> TestCase {
1715 TestCase {
1716 files: &["home/.jjconfig.toml"],
1717 env: UnresolvedConfigEnv {
1718 home_dir: Some("home".into()),
1719 config_dir: Some("config".into()),
1720 ..Default::default()
1721 },
1722 wants: vec![
1723 Want::existing("home/.jjconfig.toml"),
1724 Want::new("config/jj/config.toml"),
1725 ],
1726 }
1727 }
1728
1729 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1730 TestCase {
1731 files: &["config/jj/conf.d/_"],
1732 env: UnresolvedConfigEnv {
1733 home_dir: Some("home".into()),
1734 config_dir: Some("config".into()),
1735 ..Default::default()
1736 },
1737 wants: vec![
1738 Want::new("config/jj/config.toml"),
1739 Want::existing("config/jj/conf.d"),
1740 ],
1741 }
1742 }
1743
1744 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1745 TestCase {
1746 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1747 env: UnresolvedConfigEnv {
1748 home_dir: Some("home".into()),
1749 config_dir: Some("config".into()),
1750 ..Default::default()
1751 },
1752 wants: vec![
1753 Want::existing("config/jj/config.toml"),
1754 Want::existing("config/jj/conf.d"),
1755 ],
1756 }
1757 }
1758
1759 fn config_path_all_existing() -> TestCase {
1760 TestCase {
1761 files: &[
1762 "config/jj/conf.d/_",
1763 "config/jj/config.toml",
1764 "home/.jjconfig.toml",
1765 ],
1766 env: UnresolvedConfigEnv {
1767 home_dir: Some("home".into()),
1768 config_dir: Some("config".into()),
1769 ..Default::default()
1770 },
1771 wants: vec![
1773 Want::existing("home/.jjconfig.toml"),
1774 Want::existing("config/jj/config.toml"),
1775 Want::existing("config/jj/conf.d"),
1776 ],
1777 }
1778 }
1779
1780 fn config_path_none() -> TestCase {
1781 TestCase {
1782 files: &[],
1783 env: Default::default(),
1784 wants: vec![],
1785 }
1786 }
1787
1788 #[test_case(config_path_home_existing())]
1789 #[test_case(config_path_home_new())]
1790 #[test_case(config_path_home_existing_platform_new())]
1791 #[test_case(config_path_platform_existing())]
1792 #[test_case(config_path_platform_new())]
1793 #[test_case(config_path_new_prefer_platform())]
1794 #[test_case(config_path_jj_config_existing())]
1795 #[test_case(config_path_jj_config_new())]
1796 #[test_case(config_path_jj_config_existing_multiple())]
1797 #[test_case(config_path_jj_config_new_multiple())]
1798 #[test_case(config_path_jj_config_empty_paths_filtered())]
1799 #[test_case(config_path_jj_config_empty())]
1800 #[test_case(config_path_config_pick_platform())]
1801 #[test_case(config_path_config_pick_home())]
1802 #[test_case(config_path_platform_new_conf_dir_existing())]
1803 #[test_case(config_path_platform_existing_conf_dir_existing())]
1804 #[test_case(config_path_all_existing())]
1805 #[test_case(config_path_none())]
1806 fn test_config_path(case: TestCase) {
1807 let tmp = setup_config_fs(case.files);
1808 let env = resolve_config_env(&case.env, tmp.path());
1809
1810 let all_expected_paths = case
1811 .wants
1812 .iter()
1813 .map(|w| w.rooted_path(tmp.path()))
1814 .collect_vec();
1815 let exists_expected_paths = case
1816 .wants
1817 .iter()
1818 .filter(|w| w.exists())
1819 .map(|w| w.rooted_path(tmp.path()))
1820 .collect_vec();
1821
1822 let all_paths = env.user_config_paths().collect_vec();
1823 let exists_paths = env.existing_user_config_paths().collect_vec();
1824
1825 assert_eq!(all_paths, all_expected_paths);
1826 assert_eq!(exists_paths, exists_expected_paths);
1827 }
1828
1829 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1830 let tmp = testutils::new_temp_dir();
1831 for file in files {
1832 let path = tmp.path().join(file);
1833 if let Some(parent) = path.parent() {
1834 std::fs::create_dir_all(parent).unwrap();
1835 }
1836 std::fs::File::create(path).unwrap();
1837 }
1838 tmp
1839 }
1840
1841 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1842 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1843 let env = UnresolvedConfigEnv {
1844 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1845 home_dir: home_dir.clone(),
1846 jj_config: env.jj_config.as_ref().map(|p| {
1847 join_paths(split_paths(p).map(|p| {
1848 if p.as_os_str().is_empty() {
1849 return p;
1850 }
1851 root.join(p)
1852 }))
1853 .unwrap()
1854 .into_string()
1855 .unwrap()
1856 }),
1857 };
1858 ConfigEnv {
1859 home_dir,
1860 root_config_dir: None,
1861 repo_path: None,
1862 workspace_path: None,
1863 user_config_paths: env.resolve(),
1864 repo_config: None,
1865 workspace_config: None,
1866 command: None,
1867 hostname: None,
1868 environment: HashMap::new(),
1869 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1870 }
1871 }
1872}