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