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