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::LazyLock;
25
26use etcetera::BaseStrategy as _;
27use itertools::Itertools as _;
28use jj_lib::config::ConfigFile;
29use jj_lib::config::ConfigGetError;
30use jj_lib::config::ConfigLayer;
31use jj_lib::config::ConfigLoadError;
32use jj_lib::config::ConfigMigrationRule;
33use jj_lib::config::ConfigNamePathBuf;
34use jj_lib::config::ConfigResolutionContext;
35use jj_lib::config::ConfigSource;
36use jj_lib::config::ConfigValue;
37use jj_lib::config::StackedConfig;
38use jj_lib::dsl_util;
39use regex::Captures;
40use regex::Regex;
41use serde::Serialize as _;
42use tracing::instrument;
43
44use crate::command_error::CommandError;
45use crate::command_error::config_error;
46use crate::command_error::config_error_with_message;
47use crate::text_util;
48use crate::ui::Ui;
49
50pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
52
53pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
56 match value_str.parse() {
57 Ok(value) => Ok(value),
58 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
59 Err(err) => Err(err),
60 }
61}
62
63fn is_bare_string(value_str: &str) -> bool {
64 let trimmed = value_str.trim_ascii().as_bytes();
67 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
68 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
70 } else {
71 true }
73}
74
75pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
78 match value {
79 ConfigValue::String(v) => toml::Value::String(v.into_value()),
80 ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
81 ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
82 ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
83 ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
84 ConfigValue::Array(array) => {
85 let array = array.into_iter().map(to_serializable_value).collect();
86 toml::Value::Array(array)
87 }
88 ConfigValue::InlineTable(table) => {
89 let table = table
90 .into_iter()
91 .map(|(k, v)| (k, to_serializable_value(v)))
92 .collect();
93 toml::Value::Table(table)
94 }
95 }
96}
97
98#[derive(Clone, Debug, serde::Serialize)]
100pub struct AnnotatedValue {
101 #[serde(serialize_with = "serialize_name")]
103 pub name: ConfigNamePathBuf,
104 #[serde(serialize_with = "serialize_value")]
106 pub value: ConfigValue,
107 #[serde(serialize_with = "serialize_source")]
109 pub source: ConfigSource,
110 pub path: Option<PathBuf>,
112 pub is_overridden: bool,
114}
115
116fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
117where
118 S: serde::Serializer,
119{
120 name.to_string().serialize(serializer)
121}
122
123fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
124where
125 S: serde::Serializer,
126{
127 to_serializable_value(value.clone()).serialize(serializer)
128}
129
130fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
131where
132 S: serde::Serializer,
133{
134 source.to_string().serialize(serializer)
135}
136
137pub fn resolved_config_values(
140 stacked_config: &StackedConfig,
141 filter_prefix: &ConfigNamePathBuf,
142) -> Vec<AnnotatedValue> {
143 let mut config_vals = vec![];
146 let mut upper_value_names = BTreeSet::new();
147 for layer in stacked_config.layers().iter().rev() {
148 let top_item = match layer.look_up_item(filter_prefix) {
149 Ok(Some(item)) => item,
150 Ok(None) => continue, Err(_) => {
152 upper_value_names.insert(filter_prefix.clone());
154 continue;
155 }
156 };
157 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
158 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
159 if let Some(table) = item.as_table_like() {
162 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
164 for (k, v) in table.iter() {
165 let mut sub_name = name.clone();
166 sub_name.push(k);
167 config_stack.push((sub_name, v, is_overridden)); }
169 } else {
170 let maybe_child = upper_value_names
172 .range(&name..)
173 .next()
174 .filter(|next| next.starts_with(&name));
175 let is_overridden = is_parent_overridden || maybe_child.is_some();
176 if maybe_child != Some(&name) {
177 upper_value_names.insert(name.clone());
178 }
179 let value = item
180 .clone()
181 .into_value()
182 .expect("Item::None should not exist in table");
183 config_vals.push(AnnotatedValue {
184 name,
185 value,
186 source: layer.source,
187 path: layer.path.clone(),
188 is_overridden,
189 });
190 }
191 }
192 }
193 config_vals.reverse();
194 config_vals
195}
196
197#[derive(Clone, Debug)]
202pub struct RawConfig(StackedConfig);
203
204impl AsRef<StackedConfig> for RawConfig {
205 fn as_ref(&self) -> &StackedConfig {
206 &self.0
207 }
208}
209
210impl AsMut<StackedConfig> for RawConfig {
211 fn as_mut(&mut self) -> &mut StackedConfig {
212 &mut self.0
213 }
214}
215
216#[derive(Clone, Debug)]
217enum ConfigPathState {
218 New,
219 Exists,
220}
221
222#[derive(Clone, Debug)]
228struct ConfigPath {
229 path: PathBuf,
230 state: ConfigPathState,
231}
232
233impl ConfigPath {
234 fn new(path: PathBuf) -> Self {
235 use ConfigPathState::*;
236 Self {
237 state: if path.exists() { Exists } else { New },
238 path,
239 }
240 }
241
242 fn as_path(&self) -> &Path {
243 &self.path
244 }
245 fn exists(&self) -> bool {
246 match self.state {
247 ConfigPathState::Exists => true,
248 ConfigPathState::New => false,
249 }
250 }
251}
252
253fn create_dir_all(path: &Path) -> std::io::Result<()> {
256 let mut dir = std::fs::DirBuilder::new();
257 dir.recursive(true);
258 #[cfg(unix)]
259 {
260 use std::os::unix::fs::DirBuilderExt as _;
261 dir.mode(0o700);
262 }
263 dir.create(path)
264}
265
266#[derive(Clone, Default, Debug)]
268struct UnresolvedConfigEnv {
269 config_dir: Option<PathBuf>,
270 macos_legacy_config_dir: Option<PathBuf>,
272 home_dir: Option<PathBuf>,
273 jj_config: Option<String>,
274}
275
276impl UnresolvedConfigEnv {
277 fn resolve(self, ui: &Ui) -> Vec<ConfigPath> {
278 if let Some(paths) = self.jj_config {
279 return split_paths(&paths)
280 .filter(|path| !path.as_os_str().is_empty())
281 .map(ConfigPath::new)
282 .collect();
283 }
284
285 let mut paths = vec![];
286 let home_config_path = self.home_dir.map(|mut home_dir| {
287 home_dir.push(".jjconfig.toml");
288 ConfigPath::new(home_dir)
289 });
290 let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
291 config_dir.push("jj");
292 config_dir.push("config.toml");
293 ConfigPath::new(config_dir)
294 });
295 let platform_config_dir = self.config_dir.map(|mut config_dir| {
296 config_dir.push("jj");
297 config_dir.push("conf.d");
298 ConfigPath::new(config_dir)
299 });
300 let legacy_platform_config_path =
301 self.macos_legacy_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 legacy_platform_config_dir = self.macos_legacy_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()
314 || (platform_config_path.is_none() && legacy_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
328 && path.exists()
329 {
330 paths.push(path);
331 }
332
333 if let Some(path) = legacy_platform_config_path
334 && path.exists()
335 {
336 Self::warn_for_deprecated_path(
337 ui,
338 path.as_path(),
339 "~/Library/Application Support/jj",
340 "~/.config/jj",
341 );
342 paths.push(path);
343 }
344 if let Some(path) = legacy_platform_config_dir
345 && path.exists()
346 {
347 Self::warn_for_deprecated_path(
348 ui,
349 path.as_path(),
350 "~/Library/Application Support/jj",
351 "~/.config/jj",
352 );
353 paths.push(path);
354 }
355
356 paths
357 }
358
359 fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {
360 let _ = indoc::writedoc!(
361 ui.warning_default(),
362 r"
363 Deprecated configuration file `{}`.
364 Configuration files in `{old}` are deprecated, and support will be removed in a future release.
365 Instead, move your configuration files to `{new}`.
366 ",
367 path.display(),
368 );
369 }
370}
371
372#[derive(Clone, Debug)]
373pub struct ConfigEnv {
374 home_dir: Option<PathBuf>,
375 repo_path: Option<PathBuf>,
376 user_config_paths: Vec<ConfigPath>,
377 repo_config_path: Option<ConfigPath>,
378 command: Option<String>,
379}
380
381impl ConfigEnv {
382 pub fn from_environment(ui: &Ui) -> Self {
384 let config_dir = etcetera::choose_base_strategy()
385 .ok()
386 .map(|s| s.config_dir());
387
388 let macos_legacy_config_dir = if cfg!(target_os = "macos") {
391 etcetera::base_strategy::choose_native_strategy()
392 .ok()
393 .map(|s| {
394 s.data_dir()
397 })
398 .filter(|data_dir| {
399 Some(data_dir) != config_dir.as_ref()
401 })
402 } else {
403 None
404 };
405
406 let home_dir = etcetera::home_dir()
409 .ok()
410 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
411
412 let env = UnresolvedConfigEnv {
413 config_dir,
414 macos_legacy_config_dir,
415 home_dir: home_dir.clone(),
416 jj_config: env::var("JJ_CONFIG").ok(),
417 };
418 Self {
419 home_dir,
420 repo_path: None,
421 user_config_paths: env.resolve(ui),
422 repo_config_path: None,
423 command: None,
424 }
425 }
426
427 pub fn set_command_name(&mut self, command: String) {
428 self.command = Some(command);
429 }
430
431 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
433 self.user_config_paths.iter().map(ConfigPath::as_path)
434 }
435
436 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
439 self.user_config_paths
440 .iter()
441 .filter(|p| p.exists())
442 .map(ConfigPath::as_path)
443 }
444
445 pub fn user_config_files(
452 &self,
453 config: &RawConfig,
454 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
455 config_files_for(config, ConfigSource::User, || self.new_user_config_file())
456 }
457
458 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
459 self.user_config_paths()
460 .next()
461 .map(|path| {
462 if let Some(dir) = path.parent() {
465 create_dir_all(dir).ok();
466 }
467 ConfigFile::load_or_empty(ConfigSource::User, path)
470 })
471 .transpose()
472 }
473
474 #[instrument]
477 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
478 config.as_mut().remove_layers(ConfigSource::User);
479 for path in self.existing_user_config_paths() {
480 if path.is_dir() {
481 config.as_mut().load_dir(ConfigSource::User, path)?;
482 } else {
483 config.as_mut().load_file(ConfigSource::User, path)?;
484 }
485 }
486 Ok(())
487 }
488
489 pub fn reset_repo_path(&mut self, path: &Path) {
492 self.repo_path = Some(path.to_owned());
493 self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
494 }
495
496 pub fn repo_config_path(&self) -> Option<&Path> {
498 self.repo_config_path.as_ref().map(|p| p.as_path())
499 }
500
501 fn existing_repo_config_path(&self) -> Option<&Path> {
503 match self.repo_config_path {
504 Some(ref path) if path.exists() => Some(path.as_path()),
505 _ => None,
506 }
507 }
508
509 pub fn repo_config_files(
516 &self,
517 config: &RawConfig,
518 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
519 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
520 }
521
522 fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
523 self.repo_config_path()
524 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
527 .transpose()
528 }
529
530 #[instrument]
533 pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
534 config.as_mut().remove_layers(ConfigSource::Repo);
535 if let Some(path) = self.existing_repo_config_path() {
536 config.as_mut().load_file(ConfigSource::Repo, path)?;
537 }
538 Ok(())
539 }
540
541 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
544 let context = ConfigResolutionContext {
545 home_dir: self.home_dir.as_deref(),
546 repo_path: self.repo_path.as_deref(),
547 command: self.command.as_deref(),
548 };
549 jj_lib::config::resolve(config.as_ref(), &context)
550 }
551}
552
553fn config_files_for(
554 config: &RawConfig,
555 source: ConfigSource,
556 new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
557) -> Result<Vec<ConfigFile>, ConfigLoadError> {
558 let mut files = config
559 .as_ref()
560 .layers_for(source)
561 .iter()
562 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
563 .collect_vec();
564 if files.is_empty() {
565 files.extend(new_file()?);
566 }
567 Ok(files)
568}
569
570pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
584 let mut config = StackedConfig::with_defaults();
585 config.extend_layers(default_layers);
586 config.add_layer(env_base_layer());
587 config.add_layer(env_overrides_layer());
588 RawConfig(config)
589}
590
591const OP_HOSTNAME: &str = "operation.hostname";
592const OP_USERNAME: &str = "operation.username";
593
594fn env_base_layer() -> ConfigLayer {
596 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
597 if let Ok(value) = whoami::fallible::hostname()
598 .inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
599 {
600 layer.set_value(OP_HOSTNAME, value).unwrap();
601 }
602 if let Ok(value) = whoami::fallible::username()
603 .inspect_err(|err| tracing::warn!(?err, "failed to get username"))
604 {
605 layer.set_value(OP_USERNAME, value).unwrap();
606 } else if let Ok(value) = env::var("USER") {
607 layer.set_value(OP_USERNAME, value).unwrap();
610 }
611 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
612 layer.set_value("ui.color", "never").unwrap();
615 }
616 if let Ok(value) = env::var("PAGER") {
617 layer.set_value("ui.pager", value).unwrap();
618 }
619 if let Ok(value) = env::var("VISUAL") {
620 layer.set_value("ui.editor", value).unwrap();
621 } else if let Ok(value) = env::var("EDITOR") {
622 layer.set_value("ui.editor", value).unwrap();
623 }
624 layer
625}
626
627pub fn default_config_layers() -> Vec<ConfigLayer> {
628 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
631 let mut layers = vec![
632 parse(include_str!("config/colors.toml")),
633 parse(include_str!("config/hints.toml")),
634 parse(include_str!("config/merge_tools.toml")),
635 parse(include_str!("config/misc.toml")),
636 parse(include_str!("config/revsets.toml")),
637 parse(include_str!("config/templates.toml")),
638 ];
639 if cfg!(unix) {
640 layers.push(parse(include_str!("config/unix.toml")));
641 }
642 if cfg!(windows) {
643 layers.push(parse(include_str!("config/windows.toml")));
644 }
645 layers
646}
647
648fn env_overrides_layer() -> ConfigLayer {
650 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
651 if let Ok(value) = env::var("JJ_USER") {
652 layer.set_value("user.name", value).unwrap();
653 }
654 if let Ok(value) = env::var("JJ_EMAIL") {
655 layer.set_value("user.email", value).unwrap();
656 }
657 if let Ok(value) = env::var("JJ_TIMESTAMP") {
658 layer.set_value("debug.commit-timestamp", value).unwrap();
659 }
660 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
661 layer.set_value("debug.randomness-seed", value).unwrap();
662 }
663 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
664 layer.set_value("debug.operation-timestamp", value).unwrap();
665 }
666 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
667 layer.set_value(OP_HOSTNAME, value).unwrap();
668 }
669 if let Ok(value) = env::var("JJ_OP_USERNAME") {
670 layer.set_value(OP_USERNAME, value).unwrap();
671 }
672 if let Ok(value) = env::var("JJ_EDITOR") {
673 layer.set_value("ui.editor", value).unwrap();
674 }
675 layer
676}
677
678#[derive(Clone, Copy, Debug, Eq, PartialEq)]
680pub enum ConfigArgKind {
681 Item,
683 File,
685}
686
687pub fn parse_config_args(
689 toml_strs: &[(ConfigArgKind, &str)],
690) -> Result<Vec<ConfigLayer>, CommandError> {
691 let source = ConfigSource::CommandArg;
692 let mut layers = Vec::new();
693 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
694 match kind {
695 ConfigArgKind::Item => {
696 let mut layer = ConfigLayer::empty(source);
697 for (_, item) in chunk {
698 let (name, value) = parse_config_arg_item(item)?;
699 layer.set_value(name, value).map_err(|err| {
702 config_error_with_message("--config argument cannot be set", err)
703 })?;
704 }
705 layers.push(layer);
706 }
707 ConfigArgKind::File => {
708 for (_, path) in chunk {
709 layers.push(ConfigLayer::load_from_file(source, path.into())?);
710 }
711 }
712 }
713 }
714 Ok(layers)
715}
716
717fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
719 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
721 let Some((name, value_str)) = split_candidates
722 .map(|p| (&item_str[..p], &item_str[p + 1..]))
723 .map(|(name, value)| name.parse().map(|name| (name, value)))
724 .find_or_last(Result::is_ok)
725 .transpose()
726 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
727 else {
728 return Err(config_error("--config must be specified as NAME=VALUE"));
729 };
730 let value = parse_value_or_bare_string(value_str)
731 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
732 Ok((name, value))
733}
734
735pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
737 vec![
738 ConfigMigrationRule::rename_update_value(
740 "signing.sign-all",
741 "signing.behavior",
742 |old_value| {
743 if old_value
744 .as_bool()
745 .ok_or("signing.sign-all expects a boolean")?
746 {
747 Ok("own".into())
748 } else {
749 Ok("keep".into())
750 }
751 },
752 ),
753 ConfigMigrationRule::rename_value(
755 "core.watchman.register_snapshot_trigger",
756 "fsmonitor.watchman.register-snapshot-trigger",
757 ),
758 ConfigMigrationRule::rename_value("diff.format", "ui.diff.format"),
760 ConfigMigrationRule::rename_update_value(
762 "ui.default-description",
763 "template-aliases.default_commit_description",
764 |old_value| {
765 let value = old_value.as_str().ok_or("expected a string")?;
766 let value = text_util::complete_newline(value);
768 let escaped = dsl_util::escape_string(&value);
769 Ok(format!(r#""{escaped}""#).into())
770 },
771 ),
772 ConfigMigrationRule::rename_value("ui.diff.tool", "ui.diff-formatter"),
774 ConfigMigrationRule::rename_update_value(
776 "ui.diff.format",
777 "ui.diff-formatter",
778 |old_value| {
779 let value = old_value.as_str().ok_or("expected a string")?;
780 Ok(format!(":{value}").into())
781 },
782 ),
783 ConfigMigrationRule::rename_update_value(
785 "git.push-bookmark-prefix",
786 "templates.git_push_bookmark",
787 |old_value| {
788 let value = old_value.as_str().ok_or("expected a string")?;
789 let escaped = dsl_util::escape_string(value);
790 Ok(format!(r#""{escaped}" ++ change_id.short()"#).into())
791 },
792 ),
793 ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
795 ConfigMigrationRule::rename_value(
797 "core.watchman.register-snapshot-trigger",
798 "fsmonitor.watchman.register-snapshot-trigger",
799 ),
800 ]
801}
802
803#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
805#[serde(untagged)]
806pub enum CommandNameAndArgs {
807 String(String),
808 Vec(NonEmptyCommandArgsVec),
809 Structured {
810 env: HashMap<String, String>,
811 command: NonEmptyCommandArgsVec,
812 },
813}
814
815impl CommandNameAndArgs {
816 pub fn split_name(&self) -> Cow<'_, str> {
818 let (name, _) = self.split_name_and_args();
819 name
820 }
821
822 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
826 match self {
827 Self::String(s) => {
828 let mut args = s.split(' ').map(|s| s.to_owned());
830 (args.next().unwrap().into(), args.collect())
831 }
832 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
833 Self::Structured {
834 env: _,
835 command: cmd,
836 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
837 }
838 }
839
840 pub fn as_str(&self) -> Option<&str> {
845 match self {
846 Self::String(s) => Some(s),
847 Self::Vec(_) | Self::Structured { .. } => None,
848 }
849 }
850
851 pub fn to_command(&self) -> Command {
853 let empty: HashMap<&str, &str> = HashMap::new();
854 self.to_command_with_variables(&empty)
855 }
856
857 pub fn to_command_with_variables<V: AsRef<str>>(
860 &self,
861 variables: &HashMap<&str, V>,
862 ) -> Command {
863 let (name, args) = self.split_name_and_args();
864 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
865 if let Self::Structured { env, .. } = self {
866 cmd.envs(env);
867 }
868 cmd.args(interpolate_variables(&args, variables));
869 cmd
870 }
871}
872
873impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
874 fn from(s: &T) -> Self {
875 Self::String(s.as_ref().to_owned())
876 }
877}
878
879impl fmt::Display for CommandNameAndArgs {
880 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
881 match self {
882 Self::String(s) => write!(f, "{s}"),
883 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
885 Self::Structured { env, command } => {
886 for (k, v) in env {
887 write!(f, "{k}={v} ")?;
888 }
889 write!(f, "{}", command.0.join(" "))
890 }
891 }
892 }
893}
894
895static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
897
898pub fn interpolate_variables<V: AsRef<str>>(
899 args: &[String],
900 variables: &HashMap<&str, V>,
901) -> Vec<String> {
902 args.iter()
903 .map(|arg| interpolate_variables_single(arg, variables))
904 .collect()
905}
906
907fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
908 VARIABLE_REGEX
909 .replace_all(arg, |caps: &Captures| {
910 let name = &caps[1];
911 if let Some(subst) = variables.get(name) {
912 subst.as_ref().to_owned()
913 } else {
914 caps[0].to_owned()
915 }
916 })
917 .into_owned()
918}
919
920pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
922 let regex = &*VARIABLE_REGEX;
923 args.iter()
924 .flat_map(|arg| regex.find_iter(arg))
925 .map(|single_match| {
926 let s = single_match.as_str();
927 &s[1..]
928 })
929}
930
931#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
934#[serde(try_from = "Vec<String>")]
935pub struct NonEmptyCommandArgsVec(Vec<String>);
936
937impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
938 type Error = &'static str;
939
940 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
941 if args.is_empty() {
942 Err("command arguments should not be empty")
943 } else {
944 Ok(Self(args))
945 }
946 }
947}
948
949#[cfg(test)]
950mod tests {
951 use std::env::join_paths;
952 use std::fmt::Write as _;
953
954 use indoc::indoc;
955 use maplit::hashmap;
956 use test_case::test_case;
957
958 use super::*;
959
960 fn insta_settings() -> insta::Settings {
961 let mut settings = insta::Settings::clone_current();
962 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
964 settings
965 }
966
967 #[test]
968 fn test_parse_value_or_bare_string() {
969 let parse = |s: &str| parse_value_or_bare_string(s);
970
971 assert_eq!(parse("true").unwrap().as_bool(), Some(true));
973 assert_eq!(parse("42").unwrap().as_integer(), Some(42));
974 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
975 assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
976 assert!(parse("[]").unwrap().is_array());
977 assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
978
979 assert_eq!(parse("").unwrap().as_str(), Some(""));
981 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
982 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
983 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
984 assert_eq!(
985 parse("<foo+bar@example.org>").unwrap().as_str(),
986 Some("<foo+bar@example.org>")
987 );
988 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
989 assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
990 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
991 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
992
993 assert!(parse("'foo").is_err());
995 assert!(parse(r#" bar" "#).is_err());
996 assert!(parse("[0 1]").is_err());
997 assert!(parse("{ x = }").is_err());
998 assert!(parse("\n { x").is_err());
999 assert!(parse(" x ] ").is_err());
1000 assert!(parse("[table]\nkey = 'value'").is_err());
1001 }
1002
1003 #[test]
1004 fn test_parse_config_arg_item() {
1005 assert!(parse_config_arg_item("").is_err());
1006 assert!(parse_config_arg_item("a").is_err());
1007 assert!(parse_config_arg_item("=").is_err());
1008 assert!(parse_config_arg_item("a = 'b'").is_err());
1011
1012 let (name, value) = parse_config_arg_item("a=b").unwrap();
1013 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1014 assert_eq!(value.as_str(), Some("b"));
1015
1016 let (name, value) = parse_config_arg_item("a=").unwrap();
1017 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1018 assert_eq!(value.as_str(), Some(""));
1019
1020 let (name, value) = parse_config_arg_item("a= ").unwrap();
1021 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1022 assert_eq!(value.as_str(), Some(" "));
1023
1024 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1026 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1027 assert_eq!(value.as_str(), Some("b=c"));
1028
1029 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1030 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1031 assert_eq!(value.as_bool(), Some(true));
1032
1033 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1034 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1035 assert_eq!(value.as_str(), Some("b=c"));
1036
1037 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1038 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1039 assert_eq!(value.as_str(), Some("c"));
1040
1041 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1042 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1043 assert!(value.is_inline_table());
1044 assert_eq!(value.to_string(), "{d = 'e=f'}");
1045 }
1046
1047 #[test]
1048 fn test_command_args() {
1049 let mut config = StackedConfig::empty();
1050 config.add_layer(
1051 ConfigLayer::parse(
1052 ConfigSource::User,
1053 indoc! {"
1054 empty_array = []
1055 empty_string = ''
1056 array = ['emacs', '-nw']
1057 string = 'emacs -nw'
1058 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1059 structured.command = ['emacs', '-nw']
1060 "},
1061 )
1062 .unwrap(),
1063 );
1064
1065 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1066
1067 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1068 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1069 let (name, args) = command_args.split_name_and_args();
1070 assert_eq!(name, "");
1071 assert!(args.is_empty());
1072
1073 let command_args: CommandNameAndArgs = config.get("array").unwrap();
1074 assert_eq!(
1075 command_args,
1076 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1077 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1078 ))
1079 );
1080 let (name, args) = command_args.split_name_and_args();
1081 assert_eq!(name, "emacs");
1082 assert_eq!(args, ["-nw"].as_ref());
1083
1084 let command_args: CommandNameAndArgs = config.get("string").unwrap();
1085 assert_eq!(
1086 command_args,
1087 CommandNameAndArgs::String("emacs -nw".to_owned())
1088 );
1089 let (name, args) = command_args.split_name_and_args();
1090 assert_eq!(name, "emacs");
1091 assert_eq!(args, ["-nw"].as_ref());
1092
1093 let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1094 assert_eq!(
1095 command_args,
1096 CommandNameAndArgs::Structured {
1097 env: hashmap! {
1098 "KEY1".to_string() => "value1".to_string(),
1099 "KEY2".to_string() => "value2".to_string(),
1100 },
1101 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1102 }
1103 );
1104 let (name, args) = command_args.split_name_and_args();
1105 assert_eq!(name, "emacs");
1106 assert_eq!(args, ["-nw"].as_ref());
1107 }
1108
1109 #[test]
1110 fn test_resolved_config_values_empty() {
1111 let config = StackedConfig::empty();
1112 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1113 }
1114
1115 #[test]
1116 fn test_resolved_config_values_single_key() {
1117 let settings = insta_settings();
1118 let _guard = settings.bind_to_scope();
1119 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1120 env_base_layer
1121 .set_value("user.name", "base-user-name")
1122 .unwrap();
1123 env_base_layer
1124 .set_value("user.email", "base@user.email")
1125 .unwrap();
1126 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1127 repo_layer
1128 .set_value("user.email", "repo@user.email")
1129 .unwrap();
1130 let mut config = StackedConfig::empty();
1131 config.add_layer(env_base_layer);
1132 config.add_layer(repo_layer);
1133 insta::assert_debug_snapshot!(
1135 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1136 @r#"
1137 [
1138 AnnotatedValue {
1139 name: ConfigNamePathBuf(
1140 [
1141 Key {
1142 key: "user",
1143 repr: None,
1144 leaf_decor: Decor { .. },
1145 dotted_decor: Decor { .. },
1146 },
1147 Key {
1148 key: "name",
1149 repr: None,
1150 leaf_decor: Decor { .. },
1151 dotted_decor: Decor { .. },
1152 },
1153 ],
1154 ),
1155 value: String(
1156 Formatted {
1157 value: "base-user-name",
1158 repr: "default",
1159 decor: Decor { .. },
1160 },
1161 ),
1162 source: EnvBase,
1163 path: None,
1164 is_overridden: false,
1165 },
1166 AnnotatedValue {
1167 name: ConfigNamePathBuf(
1168 [
1169 Key {
1170 key: "user",
1171 repr: None,
1172 leaf_decor: Decor { .. },
1173 dotted_decor: Decor { .. },
1174 },
1175 Key {
1176 key: "email",
1177 repr: None,
1178 leaf_decor: Decor { .. },
1179 dotted_decor: Decor { .. },
1180 },
1181 ],
1182 ),
1183 value: String(
1184 Formatted {
1185 value: "base@user.email",
1186 repr: "default",
1187 decor: Decor { .. },
1188 },
1189 ),
1190 source: EnvBase,
1191 path: None,
1192 is_overridden: true,
1193 },
1194 AnnotatedValue {
1195 name: ConfigNamePathBuf(
1196 [
1197 Key {
1198 key: "user",
1199 repr: None,
1200 leaf_decor: Decor { .. },
1201 dotted_decor: Decor { .. },
1202 },
1203 Key {
1204 key: "email",
1205 repr: None,
1206 leaf_decor: Decor { .. },
1207 dotted_decor: Decor { .. },
1208 },
1209 ],
1210 ),
1211 value: String(
1212 Formatted {
1213 value: "repo@user.email",
1214 repr: "default",
1215 decor: Decor { .. },
1216 },
1217 ),
1218 source: Repo,
1219 path: None,
1220 is_overridden: false,
1221 },
1222 ]
1223 "#
1224 );
1225 }
1226
1227 #[test]
1228 fn test_resolved_config_values_filter_path() {
1229 let settings = insta_settings();
1230 let _guard = settings.bind_to_scope();
1231 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1232 user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1233 user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1234 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1235 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1236 let mut config = StackedConfig::empty();
1237 config.add_layer(user_layer);
1238 config.add_layer(repo_layer);
1239 insta::assert_debug_snapshot!(
1240 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1241 @r#"
1242 [
1243 AnnotatedValue {
1244 name: ConfigNamePathBuf(
1245 [
1246 Key {
1247 key: "test-table1",
1248 repr: None,
1249 leaf_decor: Decor { .. },
1250 dotted_decor: Decor { .. },
1251 },
1252 Key {
1253 key: "foo",
1254 repr: None,
1255 leaf_decor: Decor { .. },
1256 dotted_decor: Decor { .. },
1257 },
1258 ],
1259 ),
1260 value: String(
1261 Formatted {
1262 value: "user-FOO",
1263 repr: "default",
1264 decor: Decor { .. },
1265 },
1266 ),
1267 source: User,
1268 path: None,
1269 is_overridden: false,
1270 },
1271 AnnotatedValue {
1272 name: ConfigNamePathBuf(
1273 [
1274 Key {
1275 key: "test-table1",
1276 repr: None,
1277 leaf_decor: Decor { .. },
1278 dotted_decor: Decor { .. },
1279 },
1280 Key {
1281 key: "bar",
1282 repr: None,
1283 leaf_decor: Decor { .. },
1284 dotted_decor: Decor { .. },
1285 },
1286 ],
1287 ),
1288 value: String(
1289 Formatted {
1290 value: "repo-BAR",
1291 repr: "default",
1292 decor: Decor { .. },
1293 },
1294 ),
1295 source: Repo,
1296 path: None,
1297 is_overridden: false,
1298 },
1299 ]
1300 "#
1301 );
1302 }
1303
1304 #[test]
1305 fn test_resolved_config_values_overridden() {
1306 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1307 let mut config = StackedConfig::empty();
1308 config.extend_layers(layers.iter().copied().cloned());
1309 let prefix = if prefix.is_empty() {
1310 ConfigNamePathBuf::root()
1311 } else {
1312 prefix.parse().unwrap()
1313 };
1314 let mut output = String::new();
1315 for annotated in resolved_config_values(&config, &prefix) {
1316 let AnnotatedValue { name, value, .. } = &annotated;
1317 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1318 writeln!(output, "{sigil}{name} = {value}").unwrap();
1319 }
1320 output
1321 };
1322
1323 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1324 layer0.set_value("a.b.e", "0.0").unwrap();
1325 layer0.set_value("a.b.c.f", "0.1").unwrap();
1326 layer0.set_value("a.b.d", "0.2").unwrap();
1327 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1328 layer1.set_value("a.b", "1.0").unwrap();
1329 layer1.set_value("a.c", "1.1").unwrap();
1330 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1331 layer2.set_value("a.b.g", "2.0").unwrap();
1332 layer2.set_value("a.b.d", "2.1").unwrap();
1333
1334 let layers = [&layer0, &layer1];
1336 insta::assert_snapshot!(list(&layers, ""), @r#"
1337 !a.b.e = "0.0"
1338 !a.b.c.f = "0.1"
1339 !a.b.d = "0.2"
1340 a.b = "1.0"
1341 a.c = "1.1"
1342 "#);
1343 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1344 !a.b.e = "0.0"
1345 !a.b.c.f = "0.1"
1346 !a.b.d = "0.2"
1347 a.b = "1.0"
1348 "#);
1349 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1350 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1351
1352 let layers = [&layer1, &layer2];
1354 insta::assert_snapshot!(list(&layers, ""), @r#"
1355 !a.b = "1.0"
1356 a.c = "1.1"
1357 a.b.g = "2.0"
1358 a.b.d = "2.1"
1359 "#);
1360 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1361 !a.b = "1.0"
1362 a.b.g = "2.0"
1363 a.b.d = "2.1"
1364 "#);
1365
1366 let layers = [&layer0, &layer2];
1368 insta::assert_snapshot!(list(&layers, ""), @r#"
1369 a.b.e = "0.0"
1370 a.b.c.f = "0.1"
1371 !a.b.d = "0.2"
1372 a.b.g = "2.0"
1373 a.b.d = "2.1"
1374 "#);
1375 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1376 a.b.e = "0.0"
1377 a.b.c.f = "0.1"
1378 !a.b.d = "0.2"
1379 a.b.g = "2.0"
1380 a.b.d = "2.1"
1381 "#);
1382 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1383 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1384 !a.b.d = "0.2"
1385 a.b.d = "2.1"
1386 "#);
1387
1388 let layers = [&layer0, &layer1, &layer2];
1390 insta::assert_snapshot!(list(&layers, ""), @r#"
1391 !a.b.e = "0.0"
1392 !a.b.c.f = "0.1"
1393 !a.b.d = "0.2"
1394 !a.b = "1.0"
1395 a.c = "1.1"
1396 a.b.g = "2.0"
1397 a.b.d = "2.1"
1398 "#);
1399 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1400 !a.b.e = "0.0"
1401 !a.b.c.f = "0.1"
1402 !a.b.d = "0.2"
1403 !a.b = "1.0"
1404 a.b.g = "2.0"
1405 a.b.d = "2.1"
1406 "#);
1407 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1408 }
1409
1410 struct TestCase {
1411 files: &'static [&'static str],
1412 env: UnresolvedConfigEnv,
1413 wants: Vec<Want>,
1414 }
1415
1416 #[derive(Debug)]
1417 enum WantState {
1418 New,
1419 Existing,
1420 }
1421 #[derive(Debug)]
1422 struct Want {
1423 path: &'static str,
1424 state: WantState,
1425 }
1426
1427 impl Want {
1428 const fn new(path: &'static str) -> Self {
1429 Self {
1430 path,
1431 state: WantState::New,
1432 }
1433 }
1434
1435 const fn existing(path: &'static str) -> Self {
1436 Self {
1437 path,
1438 state: WantState::Existing,
1439 }
1440 }
1441
1442 fn rooted_path(&self, root: &Path) -> PathBuf {
1443 root.join(self.path)
1444 }
1445
1446 fn exists(&self) -> bool {
1447 matches!(self.state, WantState::Existing)
1448 }
1449 }
1450
1451 fn config_path_home_existing() -> TestCase {
1452 TestCase {
1453 files: &["home/.jjconfig.toml"],
1454 env: UnresolvedConfigEnv {
1455 home_dir: Some("home".into()),
1456 ..Default::default()
1457 },
1458 wants: vec![Want::existing("home/.jjconfig.toml")],
1459 }
1460 }
1461
1462 fn config_path_home_new() -> TestCase {
1463 TestCase {
1464 files: &[],
1465 env: UnresolvedConfigEnv {
1466 home_dir: Some("home".into()),
1467 ..Default::default()
1468 },
1469 wants: vec![Want::new("home/.jjconfig.toml")],
1470 }
1471 }
1472
1473 fn config_path_home_existing_platform_new() -> TestCase {
1474 TestCase {
1475 files: &["home/.jjconfig.toml"],
1476 env: UnresolvedConfigEnv {
1477 home_dir: Some("home".into()),
1478 config_dir: Some("config".into()),
1479 ..Default::default()
1480 },
1481 wants: vec![
1482 Want::existing("home/.jjconfig.toml"),
1483 Want::new("config/jj/config.toml"),
1484 ],
1485 }
1486 }
1487
1488 fn config_path_platform_existing() -> TestCase {
1489 TestCase {
1490 files: &["config/jj/config.toml"],
1491 env: UnresolvedConfigEnv {
1492 home_dir: Some("home".into()),
1493 config_dir: Some("config".into()),
1494 ..Default::default()
1495 },
1496 wants: vec![Want::existing("config/jj/config.toml")],
1497 }
1498 }
1499
1500 fn config_path_platform_new() -> TestCase {
1501 TestCase {
1502 files: &[],
1503 env: UnresolvedConfigEnv {
1504 config_dir: Some("config".into()),
1505 ..Default::default()
1506 },
1507 wants: vec![Want::new("config/jj/config.toml")],
1508 }
1509 }
1510
1511 fn config_path_new_prefer_platform() -> TestCase {
1512 TestCase {
1513 files: &[],
1514 env: UnresolvedConfigEnv {
1515 home_dir: Some("home".into()),
1516 config_dir: Some("config".into()),
1517 ..Default::default()
1518 },
1519 wants: vec![Want::new("config/jj/config.toml")],
1520 }
1521 }
1522
1523 fn config_path_jj_config_existing() -> TestCase {
1524 TestCase {
1525 files: &["custom.toml"],
1526 env: UnresolvedConfigEnv {
1527 jj_config: Some("custom.toml".into()),
1528 ..Default::default()
1529 },
1530 wants: vec![Want::existing("custom.toml")],
1531 }
1532 }
1533
1534 fn config_path_jj_config_new() -> TestCase {
1535 TestCase {
1536 files: &[],
1537 env: UnresolvedConfigEnv {
1538 jj_config: Some("custom.toml".into()),
1539 ..Default::default()
1540 },
1541 wants: vec![Want::new("custom.toml")],
1542 }
1543 }
1544
1545 fn config_path_jj_config_existing_multiple() -> TestCase {
1546 TestCase {
1547 files: &["custom1.toml", "custom2.toml"],
1548 env: UnresolvedConfigEnv {
1549 jj_config: Some(
1550 join_paths(["custom1.toml", "custom2.toml"])
1551 .unwrap()
1552 .into_string()
1553 .unwrap(),
1554 ),
1555 ..Default::default()
1556 },
1557 wants: vec![
1558 Want::existing("custom1.toml"),
1559 Want::existing("custom2.toml"),
1560 ],
1561 }
1562 }
1563
1564 fn config_path_jj_config_new_multiple() -> TestCase {
1565 TestCase {
1566 files: &["custom1.toml"],
1567 env: UnresolvedConfigEnv {
1568 jj_config: Some(
1569 join_paths(["custom1.toml", "custom2.toml"])
1570 .unwrap()
1571 .into_string()
1572 .unwrap(),
1573 ),
1574 ..Default::default()
1575 },
1576 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1577 }
1578 }
1579
1580 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1581 TestCase {
1582 files: &["custom1.toml"],
1583 env: UnresolvedConfigEnv {
1584 jj_config: Some(
1585 join_paths(["custom1.toml", "", "custom2.toml"])
1586 .unwrap()
1587 .into_string()
1588 .unwrap(),
1589 ),
1590 ..Default::default()
1591 },
1592 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1593 }
1594 }
1595
1596 fn config_path_jj_config_empty() -> TestCase {
1597 TestCase {
1598 files: &[],
1599 env: UnresolvedConfigEnv {
1600 jj_config: Some("".to_owned()),
1601 ..Default::default()
1602 },
1603 wants: vec![],
1604 }
1605 }
1606
1607 fn config_path_config_pick_platform() -> TestCase {
1608 TestCase {
1609 files: &["config/jj/config.toml"],
1610 env: UnresolvedConfigEnv {
1611 home_dir: Some("home".into()),
1612 config_dir: Some("config".into()),
1613 ..Default::default()
1614 },
1615 wants: vec![Want::existing("config/jj/config.toml")],
1616 }
1617 }
1618
1619 fn config_path_config_pick_home() -> TestCase {
1620 TestCase {
1621 files: &["home/.jjconfig.toml"],
1622 env: UnresolvedConfigEnv {
1623 home_dir: Some("home".into()),
1624 config_dir: Some("config".into()),
1625 ..Default::default()
1626 },
1627 wants: vec![
1628 Want::existing("home/.jjconfig.toml"),
1629 Want::new("config/jj/config.toml"),
1630 ],
1631 }
1632 }
1633
1634 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1635 TestCase {
1636 files: &["config/jj/conf.d/_"],
1637 env: UnresolvedConfigEnv {
1638 home_dir: Some("home".into()),
1639 config_dir: Some("config".into()),
1640 ..Default::default()
1641 },
1642 wants: vec![
1643 Want::new("config/jj/config.toml"),
1644 Want::existing("config/jj/conf.d"),
1645 ],
1646 }
1647 }
1648
1649 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1650 TestCase {
1651 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1652 env: UnresolvedConfigEnv {
1653 home_dir: Some("home".into()),
1654 config_dir: Some("config".into()),
1655 ..Default::default()
1656 },
1657 wants: vec![
1658 Want::existing("config/jj/config.toml"),
1659 Want::existing("config/jj/conf.d"),
1660 ],
1661 }
1662 }
1663
1664 fn config_path_all_existing() -> TestCase {
1665 TestCase {
1666 files: &[
1667 "config/jj/conf.d/_",
1668 "config/jj/config.toml",
1669 "home/.jjconfig.toml",
1670 ],
1671 env: UnresolvedConfigEnv {
1672 home_dir: Some("home".into()),
1673 config_dir: Some("config".into()),
1674 ..Default::default()
1675 },
1676 wants: vec![
1678 Want::existing("home/.jjconfig.toml"),
1679 Want::existing("config/jj/config.toml"),
1680 Want::existing("config/jj/conf.d"),
1681 ],
1682 }
1683 }
1684
1685 fn config_path_none() -> TestCase {
1686 TestCase {
1687 files: &[],
1688 env: Default::default(),
1689 wants: vec![],
1690 }
1691 }
1692
1693 fn config_path_macos_legacy_exists() -> TestCase {
1694 TestCase {
1695 files: &["macos-legacy/jj/config.toml"],
1696 env: UnresolvedConfigEnv {
1697 home_dir: Some("home".into()),
1698 config_dir: Some("config".into()),
1699 macos_legacy_config_dir: Some("macos-legacy".into()),
1700 ..Default::default()
1701 },
1702 wants: vec![
1703 Want::new("config/jj/config.toml"),
1704 Want::existing("macos-legacy/jj/config.toml"),
1705 ],
1706 }
1707 }
1708
1709 fn config_path_macos_legacy_both_exist() -> TestCase {
1710 TestCase {
1711 files: &["macos-legacy/jj/config.toml", "config/jj/config.toml"],
1712 env: UnresolvedConfigEnv {
1713 home_dir: Some("home".into()),
1714 config_dir: Some("config".into()),
1715 macos_legacy_config_dir: Some("macos-legacy".into()),
1716 ..Default::default()
1717 },
1718 wants: vec![
1719 Want::existing("config/jj/config.toml"),
1720 Want::existing("macos-legacy/jj/config.toml"),
1721 ],
1722 }
1723 }
1724
1725 fn config_path_macos_legacy_new() -> TestCase {
1726 TestCase {
1727 files: &[],
1728 env: UnresolvedConfigEnv {
1729 home_dir: Some("home".into()),
1730 config_dir: Some("config".into()),
1731 macos_legacy_config_dir: Some("macos-legacy".into()),
1732 ..Default::default()
1733 },
1734 wants: vec![Want::new("config/jj/config.toml")],
1735 }
1736 }
1737
1738 #[test_case(config_path_home_existing())]
1739 #[test_case(config_path_home_new())]
1740 #[test_case(config_path_home_existing_platform_new())]
1741 #[test_case(config_path_platform_existing())]
1742 #[test_case(config_path_platform_new())]
1743 #[test_case(config_path_new_prefer_platform())]
1744 #[test_case(config_path_jj_config_existing())]
1745 #[test_case(config_path_jj_config_new())]
1746 #[test_case(config_path_jj_config_existing_multiple())]
1747 #[test_case(config_path_jj_config_new_multiple())]
1748 #[test_case(config_path_jj_config_empty_paths_filtered())]
1749 #[test_case(config_path_jj_config_empty())]
1750 #[test_case(config_path_config_pick_platform())]
1751 #[test_case(config_path_config_pick_home())]
1752 #[test_case(config_path_platform_new_conf_dir_existing())]
1753 #[test_case(config_path_platform_existing_conf_dir_existing())]
1754 #[test_case(config_path_all_existing())]
1755 #[test_case(config_path_none())]
1756 #[test_case(config_path_macos_legacy_exists())]
1757 #[test_case(config_path_macos_legacy_both_exist())]
1758 #[test_case(config_path_macos_legacy_new())]
1759 fn test_config_path(case: TestCase) {
1760 let tmp = setup_config_fs(case.files);
1761 let env = resolve_config_env(&case.env, tmp.path());
1762
1763 let all_expected_paths = case
1764 .wants
1765 .iter()
1766 .map(|w| w.rooted_path(tmp.path()))
1767 .collect_vec();
1768 let exists_expected_paths = case
1769 .wants
1770 .iter()
1771 .filter(|w| w.exists())
1772 .map(|w| w.rooted_path(tmp.path()))
1773 .collect_vec();
1774
1775 let all_paths = env.user_config_paths().collect_vec();
1776 let exists_paths = env.existing_user_config_paths().collect_vec();
1777
1778 assert_eq!(all_paths, all_expected_paths);
1779 assert_eq!(exists_paths, exists_expected_paths);
1780 }
1781
1782 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1783 let tmp = testutils::new_temp_dir();
1784 for file in files {
1785 let path = tmp.path().join(file);
1786 if let Some(parent) = path.parent() {
1787 std::fs::create_dir_all(parent).unwrap();
1788 }
1789 std::fs::File::create(path).unwrap();
1790 }
1791 tmp
1792 }
1793
1794 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1795 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1796 let env = UnresolvedConfigEnv {
1797 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1798 macos_legacy_config_dir: env.macos_legacy_config_dir.as_ref().map(|p| root.join(p)),
1799 home_dir: home_dir.clone(),
1800 jj_config: env.jj_config.as_ref().map(|p| {
1801 join_paths(split_paths(p).map(|p| {
1802 if p.as_os_str().is_empty() {
1803 return p;
1804 }
1805 root.join(p)
1806 }))
1807 .unwrap()
1808 .into_string()
1809 .unwrap()
1810 }),
1811 };
1812 ConfigEnv {
1813 home_dir,
1814 repo_path: None,
1815 user_config_paths: env.resolve(&Ui::null()),
1816 repo_config_path: None,
1817 command: None,
1818 }
1819 }
1820}