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