1use std::borrow::Cow;
16use std::collections::BTreeSet;
17use std::collections::HashMap;
18use std::env;
19use std::env::split_paths;
20use std::fmt;
21use std::path::Path;
22use std::path::PathBuf;
23use std::process::Command;
24use std::sync::Arc;
25use std::sync::LazyLock;
26use std::sync::Mutex;
27
28use etcetera::BaseStrategy as _;
29use itertools::Itertools as _;
30use jj_lib::config::ConfigFile;
31use jj_lib::config::ConfigGetError;
32use jj_lib::config::ConfigLayer;
33use jj_lib::config::ConfigLoadError;
34use jj_lib::config::ConfigMigrationRule;
35use jj_lib::config::ConfigNamePathBuf;
36use jj_lib::config::ConfigResolutionContext;
37use jj_lib::config::ConfigSource;
38use jj_lib::config::ConfigValue;
39use jj_lib::config::StackedConfig;
40use jj_lib::dsl_util::AliasDeclarationParser;
41use jj_lib::dsl_util::AliasesMap;
42use jj_lib::secure_config::LoadedSecureConfig;
43use jj_lib::secure_config::SecureConfig;
44use rand::SeedableRng as _;
45use rand_chacha::ChaCha20Rng;
46use regex::Captures;
47use regex::Regex;
48use serde::Serialize as _;
49use tracing::instrument;
50
51use crate::command_error::CommandError;
52use crate::command_error::config_error;
53use crate::command_error::config_error_with_message;
54use crate::command_error::user_error;
55use crate::ui::Ui;
56
57pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
59
60const REPO_CONFIG_DIR: &str = "repos";
61const WORKSPACE_CONFIG_DIR: &str = "workspaces";
62
63pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
66 match value_str.parse() {
67 Ok(value) => Ok(value),
68 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
69 Err(err) => Err(err),
70 }
71}
72
73fn is_bare_string(value_str: &str) -> bool {
74 let trimmed = value_str.trim_ascii().as_bytes();
77 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
78 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
80 } else {
81 true }
83}
84
85pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
88 match value {
89 ConfigValue::String(v) => toml::Value::String(v.into_value()),
90 ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
91 ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
92 ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
93 ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
94 ConfigValue::Array(array) => {
95 let array = array.into_iter().map(to_serializable_value).collect();
96 toml::Value::Array(array)
97 }
98 ConfigValue::InlineTable(table) => {
99 let table = table
100 .into_iter()
101 .map(|(k, v)| (k, to_serializable_value(v)))
102 .collect();
103 toml::Value::Table(table)
104 }
105 }
106}
107
108#[derive(Clone, Debug, serde::Serialize)]
110pub struct AnnotatedValue {
111 #[serde(serialize_with = "serialize_name")]
113 pub name: ConfigNamePathBuf,
114 #[serde(serialize_with = "serialize_value")]
116 pub value: ConfigValue,
117 #[serde(serialize_with = "serialize_source")]
119 pub source: ConfigSource,
120 pub path: Option<PathBuf>,
122 pub is_overridden: bool,
124}
125
126fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
127where
128 S: serde::Serializer,
129{
130 name.to_string().serialize(serializer)
131}
132
133fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
134where
135 S: serde::Serializer,
136{
137 to_serializable_value(value.clone()).serialize(serializer)
138}
139
140fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
141where
142 S: serde::Serializer,
143{
144 source.to_string().serialize(serializer)
145}
146
147pub fn resolved_config_values(
150 stacked_config: &StackedConfig,
151 filter_prefix: &ConfigNamePathBuf,
152) -> Vec<AnnotatedValue> {
153 let mut config_vals = vec![];
156 let mut upper_value_names = BTreeSet::new();
157 for layer in stacked_config.layers().iter().rev() {
158 let top_item = match layer.look_up_item(filter_prefix) {
159 Ok(Some(item)) => item,
160 Ok(None) => continue, Err(_) => {
162 upper_value_names.insert(filter_prefix.clone());
164 continue;
165 }
166 };
167 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
168 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
169 if let Some(table) = item.as_table_like() {
172 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
174 for (k, v) in table.iter() {
175 let mut sub_name = name.clone();
176 sub_name.push(k);
177 config_stack.push((sub_name, v, is_overridden)); }
179 } else {
180 let maybe_child = upper_value_names
182 .range(&name..)
183 .next()
184 .filter(|next| next.starts_with(&name));
185 let is_overridden = is_parent_overridden || maybe_child.is_some();
186 if maybe_child != Some(&name) {
187 upper_value_names.insert(name.clone());
188 }
189 let value = item
190 .clone()
191 .into_value()
192 .expect("Item::None should not exist in table");
193 config_vals.push(AnnotatedValue {
194 name,
195 value,
196 source: layer.source,
197 path: layer.path.clone(),
198 is_overridden,
199 });
200 }
201 }
202 }
203 config_vals.reverse();
204 config_vals
205}
206
207#[derive(Clone, Debug)]
212pub struct RawConfig(StackedConfig);
213
214impl AsRef<StackedConfig> for RawConfig {
215 fn as_ref(&self) -> &StackedConfig {
216 &self.0
217 }
218}
219
220impl AsMut<StackedConfig> for RawConfig {
221 fn as_mut(&mut self) -> &mut StackedConfig {
222 &mut self.0
223 }
224}
225
226#[derive(Clone, Debug)]
227enum ConfigPathState {
228 New,
229 Exists,
230}
231
232#[derive(Clone, Debug)]
238struct ConfigPath {
239 path: PathBuf,
240 state: ConfigPathState,
241}
242
243impl ConfigPath {
244 fn new(path: PathBuf) -> Self {
245 use ConfigPathState::*;
246 Self {
247 state: if path.exists() { Exists } else { New },
248 path,
249 }
250 }
251
252 fn as_path(&self) -> &Path {
253 &self.path
254 }
255 fn exists(&self) -> bool {
256 match self.state {
257 ConfigPathState::Exists => true,
258 ConfigPathState::New => false,
259 }
260 }
261}
262
263fn create_dir_all(path: &Path) -> std::io::Result<()> {
266 let mut dir = std::fs::DirBuilder::new();
267 dir.recursive(true);
268 #[cfg(unix)]
269 {
270 use std::os::unix::fs::DirBuilderExt as _;
271 dir.mode(0o700);
272 }
273 dir.create(path)
274}
275
276#[derive(Clone, Default, Debug)]
278struct UnresolvedConfigEnv {
279 user_config_dir: Option<PathBuf>,
280 home_dir: Option<PathBuf>,
281 jj_config: Option<String>,
282 system_config_dir: Option<PathBuf>,
283}
284
285impl UnresolvedConfigEnv {
286 fn root_config_dir(&self) -> Option<PathBuf> {
287 self.user_config_dir.as_deref().map(|c| c.join("jj"))
288 }
289
290 fn resolve_user(self) -> Vec<ConfigPath> {
291 if let Some(paths) = self.jj_config {
292 return split_paths(&paths)
293 .filter(|path| !path.as_os_str().is_empty())
294 .map(ConfigPath::new)
295 .collect();
296 }
297
298 let mut paths = vec![];
299 let home_config_path = self.home_dir.map(|mut home_dir| {
300 home_dir.push(".jjconfig.toml");
301 ConfigPath::new(home_dir)
302 });
303 let platform_config_path = self.user_config_dir.clone().map(|mut config_dir| {
304 config_dir.push("jj");
305 config_dir.push("config.toml");
306 ConfigPath::new(config_dir)
307 });
308 let platform_config_dir = self.user_config_dir.map(|mut config_dir| {
309 config_dir.push("jj");
310 config_dir.push("conf.d");
311 ConfigPath::new(config_dir)
312 });
313
314 if let Some(path) = home_config_path
315 && (path.exists() || platform_config_path.is_none())
316 {
317 paths.push(path);
318 }
319
320 if let Some(path) = platform_config_path {
323 paths.push(path);
324 }
325
326 if let Some(path) = platform_config_dir
327 && path.exists()
328 {
329 paths.push(path);
330 }
331
332 paths
333 }
334
335 fn resolve_system(&self) -> Vec<ConfigPath> {
336 if let Some(path) = self.system_config_dir.as_ref()
337 && self.jj_config.is_none()
338 {
339 [path.join("jj/config.toml"), path.join("jj/conf.d")]
340 .into_iter()
341 .map(ConfigPath::new)
342 .collect()
343 } else {
344 Vec::new()
345 }
346 }
347}
348
349#[derive(Clone, Debug)]
350pub struct ConfigEnv {
351 home_dir: Option<PathBuf>,
352 root_config_dir: Option<PathBuf>,
353 repo_path: Option<PathBuf>,
354 workspace_path: Option<PathBuf>,
355 system_config_paths: Vec<ConfigPath>,
356 user_config_paths: Vec<ConfigPath>,
357 repo_config: Option<SecureConfig>,
358 workspace_config: Option<SecureConfig>,
359 command: Option<String>,
360 hostname: Option<String>,
361 environment: HashMap<String, String>,
362 rng: Arc<Mutex<ChaCha20Rng>>,
363}
364
365impl ConfigEnv {
366 pub fn from_environment() -> Self {
368 let user_config_dir = etcetera::choose_base_strategy()
369 .ok()
370 .map(|s| s.config_dir());
371
372 let home_dir = etcetera::home_dir()
375 .ok()
376 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
377
378 let system_config_dir = if cfg!(unix) {
379 Some("/etc".into())
380 } else {
381 None
382 };
383
384 let env = UnresolvedConfigEnv {
385 user_config_dir,
386 home_dir: home_dir.clone(),
387 jj_config: env::var("JJ_CONFIG").ok(),
388 system_config_dir,
389 };
390 let environment = env::vars_os()
391 .filter_map(|(k, v)| {
392 let k = k.into_string().ok()?;
394 let v = v.into_string().ok()?;
395 Some((k, v))
396 })
397 .collect();
398 Self {
399 home_dir,
400 root_config_dir: env.root_config_dir(),
401 repo_path: None,
402 workspace_path: None,
403 system_config_paths: env.resolve_system(),
404 user_config_paths: env.resolve_user(),
405 repo_config: None,
406 workspace_config: None,
407 command: None,
408 hostname: whoami::hostname().ok(),
409 environment,
410 rng: Arc::new(Mutex::new(
413 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) {
414 ChaCha20Rng::seed_from_u64(value)
415 } else {
416 rand::make_rng()
417 },
418 )),
419 }
420 }
421
422 pub fn set_command_name(&mut self, command: String) {
423 self.command = Some(command);
424 }
425
426 #[instrument]
429 pub fn reload_system_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
430 config.as_mut().remove_layers(ConfigSource::System);
431 for path in self.existing_system_config_paths() {
432 if path.is_dir() {
433 config.as_mut().load_dir(ConfigSource::System, path)?;
434 } else {
435 config.as_mut().load_file(ConfigSource::System, path)?;
436 }
437 }
438 Ok(())
439 }
440
441 pub fn existing_system_config_paths(&self) -> impl Iterator<Item = &Path> {
442 self.system_config_paths
443 .iter()
444 .filter(|p| p.exists())
445 .map(ConfigPath::as_path)
446 }
447
448 fn load_secure_config(
449 &self,
450 ui: &Ui,
451 config: Option<&SecureConfig>,
452 kind: &str,
453 force: bool,
454 ) -> Result<Option<LoadedSecureConfig>, CommandError> {
455 Ok(match (config, self.root_config_dir.as_ref()) {
456 (Some(config), Some(root_config_dir)) => {
457 let mut guard = self.rng.lock().unwrap();
458 let loaded_config = if force {
459 config.load_config(&mut guard, &root_config_dir.join(kind))
460 } else {
461 config.maybe_load_config(&mut guard, &root_config_dir.join(kind))
462 }?;
463 for warning in &loaded_config.warnings {
464 writeln!(ui.warning_default(), "{warning}")?;
465 }
466 Some(loaded_config)
467 }
468 _ => None,
469 })
470 }
471
472 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
474 self.user_config_paths.iter().map(ConfigPath::as_path)
475 }
476
477 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
480 self.user_config_paths
481 .iter()
482 .filter(|p| p.exists())
483 .map(ConfigPath::as_path)
484 }
485
486 pub fn user_config_files(&self, config: &RawConfig) -> Result<Vec<ConfigFile>, CommandError> {
493 config_files_for(config, ConfigSource::User, || {
494 Ok(self.new_user_config_file()?)
495 })
496 }
497
498 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
499 self.user_config_paths()
500 .next()
501 .map(|path| {
502 if let Some(dir) = path.parent() {
505 create_dir_all(dir).ok();
506 }
507 ConfigFile::load_or_empty(ConfigSource::User, path)
510 })
511 .transpose()
512 }
513
514 #[instrument]
517 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
518 config.as_mut().remove_layers(ConfigSource::User);
519 for path in self.existing_user_config_paths() {
520 if path.is_dir() {
521 config.as_mut().load_dir(ConfigSource::User, path)?;
522 } else {
523 config.as_mut().load_file(ConfigSource::User, path)?;
524 }
525 }
526 Ok(())
527 }
528
529 pub fn reset_repo_path(&mut self, path: &Path) {
532 self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf()));
533 self.repo_path = Some(path.to_owned());
534 }
535
536 fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
538 Ok(self
539 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)?
540 .and_then(|c| c.config_file))
541 }
542
543 pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
547 Ok(self
548 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)?
549 .and_then(|c| c.config_file))
550 }
551
552 pub fn repo_configs_root_dir(&self) -> Option<PathBuf> {
555 self.root_config_dir
556 .as_ref()
557 .map(|dir| dir.join(REPO_CONFIG_DIR))
558 }
559
560 pub fn repo_config_files(
567 &self,
568 ui: &Ui,
569 config: &RawConfig,
570 ) -> Result<Vec<ConfigFile>, CommandError> {
571 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file(ui))
572 }
573
574 fn new_repo_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
575 Ok(self
576 .repo_config_path(ui)?
577 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
580 .transpose()?)
581 }
582
583 #[instrument(skip(ui))]
586 pub fn reload_repo_config(&self, ui: &Ui, config: &mut RawConfig) -> Result<(), CommandError> {
587 config.as_mut().remove_layers(ConfigSource::Repo);
588 if let Some(path) = self.maybe_repo_config_path(ui)?
589 && path.exists()
590 {
591 config.as_mut().load_file(ConfigSource::Repo, path)?;
592 }
593 Ok(())
594 }
595
596 pub fn reset_workspace_path(&mut self, path: &Path) {
598 self.workspace_config = Some(SecureConfig::new_workspace(path.join(".jj")));
599 self.workspace_path = Some(path.to_owned());
600 }
601
602 fn maybe_workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
604 Ok(self
605 .load_secure_config(
606 ui,
607 self.workspace_config.as_ref(),
608 WORKSPACE_CONFIG_DIR,
609 false,
610 )?
611 .and_then(|c| c.config_file))
612 }
613
614 pub fn workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
618 Ok(self
619 .load_secure_config(
620 ui,
621 self.workspace_config.as_ref(),
622 WORKSPACE_CONFIG_DIR,
623 true,
624 )?
625 .and_then(|c| c.config_file))
626 }
627
628 pub fn workspace_config_files(
635 &self,
636 ui: &Ui,
637 config: &RawConfig,
638 ) -> Result<Vec<ConfigFile>, CommandError> {
639 config_files_for(config, ConfigSource::Workspace, || {
640 self.new_workspace_config_file(ui)
641 })
642 }
643
644 fn new_workspace_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
645 Ok(self
646 .workspace_config_path(ui)?
647 .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
648 .transpose()?)
649 }
650
651 #[instrument(skip(ui))]
654 pub fn reload_workspace_config(
655 &self,
656 ui: &Ui,
657 config: &mut RawConfig,
658 ) -> Result<(), CommandError> {
659 config.as_mut().remove_layers(ConfigSource::Workspace);
660 if let Some(path) = self.maybe_workspace_config_path(ui)?
661 && path.exists()
662 {
663 config.as_mut().load_file(ConfigSource::Workspace, path)?;
664 }
665 Ok(())
666 }
667
668 pub fn resolve_file_to_edit(
676 &self,
677 ui: &Ui,
678 config: &RawConfig,
679 path: &Path,
680 ) -> Result<ConfigFile, CommandError> {
681 let canonical_path = dunce::canonicalize(path).ok();
682 let matches_path = |p: &Path| p == path || Some(p) == canonical_path.as_deref();
683
684 for layer in config.as_ref().layers() {
687 if let Some(layer_path) = layer.path.as_ref()
688 && matches_path(layer_path)
689 && let Ok(file) = ConfigFile::from_layer(layer.clone())
690 {
691 return Ok(file);
692 }
693 }
694
695 if let Ok(Some(repo_path)) = self.repo_config_path(ui)
697 && matches_path(&repo_path)
698 {
699 if let Some(parent) = path.parent() {
700 create_dir_all(parent).ok();
701 }
702 return Ok(ConfigFile::load_or_empty(ConfigSource::Repo, path)?);
703 }
704
705 if let Ok(Some(workspace_path)) = self.workspace_config_path(ui)
707 && matches_path(&workspace_path)
708 {
709 if let Some(parent) = path.parent() {
710 create_dir_all(parent).ok();
711 }
712 return Ok(ConfigFile::load_or_empty(ConfigSource::Workspace, path)?);
713 }
714
715 for user_path in self.user_config_paths() {
718 if matches_path(user_path) || is_file_in_config_dir(path, user_path) {
719 if let Some(parent) = path.parent() {
720 create_dir_all(parent).ok();
721 }
722 return Ok(ConfigFile::load_or_empty(ConfigSource::User, path)?);
723 }
724 }
725
726 Err(user_error(format!(
727 "Configuration file '{}' is not a valid jj configuration file location",
728 path.display()
729 ))
730 .hinted(
731 "Valid config locations include user configs (`~/.config/jj/config.toml` or \
732 `conf.d/*.toml`), repo/workspace configs, or files loaded with the global flag \
733 `--config-file <PATH>`.",
734 ))
735 }
736
737 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
740 let context = ConfigResolutionContext {
741 home_dir: self.home_dir.as_deref(),
742 repo_path: self.repo_path.as_deref(),
743 workspace_path: self.workspace_path.as_deref(),
744 command: self.command.as_deref(),
745 hostname: self.hostname.as_deref().unwrap_or(""),
746 environment: &self.environment,
747 };
748 jj_lib::config::resolve(config.as_ref(), &context)
749 }
750}
751
752pub fn existing_repo_config_file(config: &RawConfig) -> Option<ConfigFile> {
755 config
757 .as_ref()
758 .layers_for(ConfigSource::Repo)
759 .iter()
760 .find_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
761}
762
763fn config_files_for(
764 config: &RawConfig,
765 source: ConfigSource,
766 new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
767) -> Result<Vec<ConfigFile>, CommandError> {
768 let mut files = config
769 .as_ref()
770 .layers_for(source)
771 .iter()
772 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
773 .collect_vec();
774 if files.is_empty() {
775 files.extend(new_file()?);
776 }
777 Ok(files)
778}
779
780fn is_file_in_config_dir(file_path: &Path, dir_path: &Path) -> bool {
781 if file_path.extension() != Some("toml".as_ref()) {
782 return false;
783 }
784 if dir_path.is_file() {
785 return false;
786 }
787 let Some(parent) = file_path.parent() else {
788 return false;
789 };
790 if parent == dir_path {
791 return true;
792 }
793 dunce::canonicalize(parent).ok().as_deref() == Some(dir_path)
794}
795
796pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
811 let mut config = StackedConfig::with_defaults();
812 config.extend_layers(default_layers);
813 config.add_layer(env_base_layer());
814 config.add_layer(env_overrides_layer());
815 RawConfig(config)
816}
817
818const OP_HOSTNAME: &str = "operation.hostname";
819const OP_USERNAME: &str = "operation.username";
820
821fn env_base_layer() -> ConfigLayer {
823 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
824 if let Ok(value) =
825 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
826 {
827 layer.set_value(OP_HOSTNAME, value).unwrap();
828 }
829 if let Ok(value) =
830 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
831 {
832 layer.set_value(OP_USERNAME, value).unwrap();
833 } else if let Ok(value) = env::var("USER") {
834 layer.set_value(OP_USERNAME, value).unwrap();
837 }
838 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
839 layer.set_value("ui.color", "never").unwrap();
842 }
843 if let Ok(value) = env::var("VISUAL") {
844 layer.set_value("ui.editor", value).unwrap();
845 } else if let Ok(value) = env::var("EDITOR") {
846 layer.set_value("ui.editor", value).unwrap();
847 }
848 layer
851}
852
853pub fn default_config_layers() -> Vec<ConfigLayer> {
854 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
857 let mut layers = vec![
858 parse(include_str!("config/colors.toml")),
859 parse(include_str!("config/hints.toml")),
860 parse(include_str!("config/merge_tools.toml")),
861 parse(include_str!("config/misc.toml")),
862 parse(include_str!("config/revsets.toml")),
863 parse(include_str!("config/templates.toml")),
864 ];
865 if cfg!(unix) {
866 layers.push(parse(include_str!("config/unix.toml")));
867 }
868 if cfg!(windows) {
869 layers.push(parse(include_str!("config/windows.toml")));
870 }
871 layers
872}
873
874fn env_overrides_layer() -> ConfigLayer {
876 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
877 if let Ok(value) = env::var("JJ_USER") {
878 layer.set_value("user.name", value).unwrap();
879 }
880 if let Ok(value) = env::var("JJ_EMAIL") {
881 layer.set_value("user.email", value).unwrap();
882 }
883 if let Ok(value) = env::var("JJ_TIMESTAMP") {
884 layer.set_value("debug.commit-timestamp", value).unwrap();
885 }
886 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
887 layer.set_value("debug.randomness-seed", value).unwrap();
888 }
889 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
890 layer.set_value("debug.operation-timestamp", value).unwrap();
891 }
892 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
893 layer.set_value(OP_HOSTNAME, value).unwrap();
894 }
895 if let Ok(value) = env::var("JJ_OP_USERNAME") {
896 layer.set_value(OP_USERNAME, value).unwrap();
897 }
898 if let Ok(value) = env::var("JJ_EDITOR") {
899 layer.set_value("ui.editor", value).unwrap();
900 }
901 if let Ok(value) = env::var("JJ_PAGER") {
902 layer.set_value("ui.pager", value).unwrap();
903 }
904 layer
905}
906
907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
909pub enum ConfigArgKind {
910 Item,
912 File,
914}
915
916pub fn parse_config_args(
918 toml_strs: &[(ConfigArgKind, &str)],
919) -> Result<Vec<ConfigLayer>, CommandError> {
920 let source = ConfigSource::CommandArg;
921 let mut layers = Vec::new();
922 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
923 match kind {
924 ConfigArgKind::Item => {
925 let mut layer = ConfigLayer::empty(source);
926 for (_, item) in chunk {
927 let (name, value) = parse_config_arg_item(item)?;
928 layer.set_value(name, value).map_err(|err| {
931 config_error_with_message("--config argument cannot be set", err)
932 })?;
933 }
934 layers.push(layer);
935 }
936 ConfigArgKind::File => {
937 for (_, path) in chunk {
938 layers.push(ConfigLayer::load_from_file(source, path.into())?);
939 }
940 }
941 }
942 }
943 Ok(layers)
944}
945
946fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
948 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
950 let Some((name, value_str)) = split_candidates
951 .map(|p| (&item_str[..p], &item_str[p + 1..]))
952 .map(|(name, value)| name.parse().map(|name| (name, value)))
953 .find_or_last(Result::is_ok)
954 .transpose()
955 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
956 else {
957 return Err(config_error("--config must be specified as NAME=VALUE"));
958 };
959 let value = parse_value_or_bare_string(value_str)
960 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
961 Ok((name, value))
962}
963
964pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
966 vec![]
967}
968
969#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
971#[serde(untagged)]
972pub enum CommandNameAndArgs {
973 String(String),
974 Vec(NonEmptyCommandArgsVec),
975 Structured {
976 env: HashMap<String, String>,
977 command: NonEmptyCommandArgsVec,
978 },
979}
980
981impl CommandNameAndArgs {
982 pub fn split_name(&self) -> Cow<'_, str> {
984 let (name, _) = self.split_name_and_args();
985 name
986 }
987
988 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
992 match self {
993 Self::String(s) => {
994 if s.contains('"') || s.contains('\'') {
995 let mut parts = shlex::Shlex::new(s);
996 let res = (
997 parts.next().unwrap_or_default().into(),
998 parts.by_ref().collect(),
999 );
1000 if !parts.had_error {
1001 return res;
1002 }
1003 }
1004 let mut args = s.split(' ').map(|s| s.to_owned());
1005 (args.next().unwrap().into(), args.collect())
1006 }
1007 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
1008 Self::Structured {
1009 env: _,
1010 command: cmd,
1011 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
1012 }
1013 }
1014
1015 pub fn as_str(&self) -> Option<&str> {
1020 match self {
1021 Self::String(s) => Some(s),
1022 Self::Vec(_) | Self::Structured { .. } => None,
1023 }
1024 }
1025
1026 pub fn to_command(&self) -> Command {
1028 let empty: HashMap<&str, &str> = HashMap::new();
1029 self.to_command_with_variables(&empty)
1030 }
1031
1032 pub fn to_command_with_variables<V: AsRef<str>>(
1035 &self,
1036 variables: &HashMap<&str, V>,
1037 ) -> Command {
1038 let (name, args) = self.split_name_and_args();
1039 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
1040 if let Self::Structured { env, .. } = self {
1041 cmd.envs(env);
1042 }
1043 cmd.args(interpolate_variables(&args, variables));
1044 cmd
1045 }
1046}
1047
1048impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
1049 fn from(s: &T) -> Self {
1050 Self::String(s.as_ref().to_owned())
1051 }
1052}
1053
1054impl fmt::Display for CommandNameAndArgs {
1055 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1056 match self {
1057 Self::String(s) => write!(f, "{s}"),
1058 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
1060 Self::Structured { env, command } => {
1061 for (k, v) in env {
1062 write!(f, "{k}={v} ")?;
1063 }
1064 write!(f, "{}", command.0.join(" "))
1065 }
1066 }
1067 }
1068}
1069
1070pub fn load_aliases_map<P>(
1071 ui: &Ui,
1072 config: &StackedConfig,
1073 table_name: &ConfigNamePathBuf,
1074) -> Result<AliasesMap<P, String>, CommandError>
1075where
1076 P: AliasDeclarationParser + Default,
1077 P::Error: fmt::Display,
1078{
1079 let mut aliases_map = AliasesMap::new();
1080 for layer in config.layers() {
1083 let table = match layer.look_up_table(table_name) {
1084 Ok(Some(table)) => table,
1085 Ok(None) => continue,
1086 Err(item) => {
1087 return Err(ConfigGetError::Type {
1088 name: table_name.to_string(),
1089 error: format!("Expected a table, but is {}", item.type_name()).into(),
1090 source_path: layer.path.clone(),
1091 }
1092 .into());
1093 }
1094 };
1095 for (decl, item) in table.iter() {
1096 let (definition, doc) = if let Some(t) = item.as_table_like() {
1097 let definition = t.get("definition").and_then(|i| i.as_str());
1098 let doc = t.get("doc").and_then(|i| i.as_str()).map(|s| s.to_owned());
1099 (definition, doc)
1100 } else {
1101 (item.as_str(), None)
1102 };
1103
1104 let r = definition
1105 .ok_or_else(|| {
1106 format!(
1107 "Expected a string or a table with a `definition` string key, but is {}",
1108 item.type_name()
1109 )
1110 })
1111 .and_then(|v| aliases_map.insert(decl, v, doc).map_err(|e| format!("{e}")));
1112 if let Err(s) = r {
1113 writeln!(
1114 ui.warning_default(),
1115 "Failed to load `{table_name}.{decl}`: {s}"
1116 )?;
1117 }
1118 }
1119 }
1120 Ok(aliases_map)
1121}
1122
1123static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
1125
1126pub fn interpolate_variables<V: AsRef<str>>(
1127 args: &[String],
1128 variables: &HashMap<&str, V>,
1129) -> Vec<String> {
1130 args.iter()
1131 .map(|arg| interpolate_variables_single(arg, variables))
1132 .collect()
1133}
1134
1135fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
1136 VARIABLE_REGEX
1137 .replace_all(arg, |caps: &Captures| {
1138 let name = &caps[1];
1139 if let Some(subst) = variables.get(name) {
1140 subst.as_ref().to_owned()
1141 } else {
1142 caps[0].to_owned()
1143 }
1144 })
1145 .into_owned()
1146}
1147
1148pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
1150 let regex = &*VARIABLE_REGEX;
1151 args.iter()
1152 .flat_map(|arg| regex.find_iter(arg))
1153 .map(|single_match| {
1154 let s = single_match.as_str();
1155 &s[1..]
1156 })
1157}
1158
1159#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
1162#[serde(try_from = "Vec<String>")]
1163pub struct NonEmptyCommandArgsVec(Vec<String>);
1164
1165impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
1166 type Error = &'static str;
1167
1168 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
1169 if args.is_empty() {
1170 Err("command arguments should not be empty")
1171 } else {
1172 Ok(Self(args))
1173 }
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use std::env::join_paths;
1180 use std::fmt::Write as _;
1181
1182 use indoc::indoc;
1183 use maplit::hashmap;
1184 use test_case::test_case;
1185 use testutils::TestResult;
1186
1187 use super::*;
1188
1189 fn insta_settings() -> insta::Settings {
1190 let mut settings = insta::Settings::clone_current();
1191 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1193 settings
1194 }
1195
1196 #[test]
1197 fn test_parse_value_or_bare_string() -> TestResult {
1198 let parse = |s: &str| parse_value_or_bare_string(s);
1199
1200 assert_eq!(parse("true")?.as_bool(), Some(true));
1202 assert_eq!(parse("42")?.as_integer(), Some(42));
1203 assert_eq!(parse("-1")?.as_integer(), Some(-1));
1204 assert_eq!(parse("'a'")?.as_str(), Some("a"));
1205 assert!(parse("[]")?.is_array());
1206 assert!(parse("{ a = 'b' }")?.is_inline_table());
1207
1208 assert_eq!(parse("")?.as_str(), Some(""));
1210 assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
1211 assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
1212 assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
1213 assert_eq!(
1214 parse("<foo+bar@example.org>")?.as_str(),
1215 Some("<foo+bar@example.org>")
1216 );
1217 assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
1218 assert_eq!(parse("all()")?.as_str(), Some("all()"));
1219 assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
1220 assert_eq!(parse("柔術")?.as_str(), Some("柔術"));
1221
1222 assert!(parse("'foo").is_err());
1224 assert!(parse(r#" bar" "#).is_err());
1225 assert!(parse("[0 1]").is_err());
1226 assert!(parse("{ x = y }").is_err());
1227 assert!(parse("\n { x").is_err());
1228 assert!(parse(" x ] ").is_err());
1229 assert!(parse("[table]\nkey = 'value'").is_err());
1230 Ok(())
1231 }
1232
1233 #[test]
1234 fn test_parse_config_arg_item() {
1235 assert!(parse_config_arg_item("").is_err());
1236 assert!(parse_config_arg_item("a").is_err());
1237 assert!(parse_config_arg_item("=").is_err());
1238 assert!(parse_config_arg_item("a = 'b'").is_err());
1241
1242 let (name, value) = parse_config_arg_item("a=b").unwrap();
1243 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1244 assert_eq!(value.as_str(), Some("b"));
1245
1246 let (name, value) = parse_config_arg_item("a=").unwrap();
1247 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1248 assert_eq!(value.as_str(), Some(""));
1249
1250 let (name, value) = parse_config_arg_item("a= ").unwrap();
1251 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1252 assert_eq!(value.as_str(), Some(" "));
1253
1254 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1256 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1257 assert_eq!(value.as_str(), Some("b=c"));
1258
1259 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1260 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1261 assert_eq!(value.as_bool(), Some(true));
1262
1263 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1264 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1265 assert_eq!(value.as_str(), Some("b=c"));
1266
1267 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1268 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1269 assert_eq!(value.as_str(), Some("c"));
1270
1271 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1272 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1273 assert!(value.is_inline_table());
1274 assert_eq!(value.to_string(), "{d = 'e=f'}");
1275 }
1276
1277 #[test]
1278 fn test_command_args() -> TestResult {
1279 let mut config = StackedConfig::empty();
1280 config.add_layer(ConfigLayer::parse(
1281 ConfigSource::User,
1282 indoc! {"
1283 empty_array = []
1284 empty_string = ''
1285 array = ['emacs', '-nw']
1286 string = 'emacs -nw'
1287 string_quoted = '\"spaced path/to/emacs\" -nw'
1288 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1289 structured.command = ['emacs', '-nw']
1290 "},
1291 )?);
1292
1293 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1294
1295 let command_args: CommandNameAndArgs = config.get("empty_string")?;
1296 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1297 let (name, args) = command_args.split_name_and_args();
1298 assert_eq!(name, "");
1299 assert!(args.is_empty());
1300
1301 let command_args: CommandNameAndArgs = config.get("array")?;
1302 assert_eq!(
1303 command_args,
1304 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1305 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1306 ))
1307 );
1308 let (name, args) = command_args.split_name_and_args();
1309 assert_eq!(name, "emacs");
1310 assert_eq!(args, ["-nw"].as_ref());
1311
1312 let command_args: CommandNameAndArgs = config.get("string")?;
1313 assert_eq!(
1314 command_args,
1315 CommandNameAndArgs::String("emacs -nw".to_owned())
1316 );
1317 let (name, args) = command_args.split_name_and_args();
1318 assert_eq!(name, "emacs");
1319 assert_eq!(args, ["-nw"].as_ref());
1320
1321 let command_args: CommandNameAndArgs = config.get("string_quoted")?;
1322 assert_eq!(
1323 command_args,
1324 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1325 );
1326 let (name, args) = command_args.split_name_and_args();
1327 assert_eq!(name, "spaced path/to/emacs");
1328 assert_eq!(args, ["-nw"].as_ref());
1329
1330 let command_args: CommandNameAndArgs = config.get("structured")?;
1331 assert_eq!(
1332 command_args,
1333 CommandNameAndArgs::Structured {
1334 env: hashmap! {
1335 "KEY1".to_string() => "value1".to_string(),
1336 "KEY2".to_string() => "value2".to_string(),
1337 },
1338 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1339 }
1340 );
1341 let (name, args) = command_args.split_name_and_args();
1342 assert_eq!(name, "emacs");
1343 assert_eq!(args, ["-nw"].as_ref());
1344 Ok(())
1345 }
1346
1347 #[test]
1348 fn test_resolved_config_values_empty() {
1349 let config = StackedConfig::empty();
1350 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1351 }
1352
1353 #[test]
1354 fn test_resolved_config_values_single_key() -> TestResult {
1355 let settings = insta_settings();
1356 let _guard = settings.bind_to_scope();
1357 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1358 env_base_layer.set_value("user.name", "base-user-name")?;
1359 env_base_layer.set_value("user.email", "base@user.email")?;
1360 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1361 repo_layer.set_value("user.email", "repo@user.email")?;
1362 let mut config = StackedConfig::empty();
1363 config.add_layer(env_base_layer);
1364 config.add_layer(repo_layer);
1365 insta::assert_debug_snapshot!(
1367 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1368 @r#"
1369 [
1370 AnnotatedValue {
1371 name: ConfigNamePathBuf(
1372 [
1373 Key {
1374 key: "user",
1375 repr: None,
1376 leaf_decor: Decor { .. },
1377 dotted_decor: Decor { .. },
1378 },
1379 Key {
1380 key: "name",
1381 repr: None,
1382 leaf_decor: Decor { .. },
1383 dotted_decor: Decor { .. },
1384 },
1385 ],
1386 ),
1387 value: String(
1388 Formatted {
1389 value: "base-user-name",
1390 repr: "default",
1391 decor: Decor { .. },
1392 },
1393 ),
1394 source: EnvBase,
1395 path: None,
1396 is_overridden: false,
1397 },
1398 AnnotatedValue {
1399 name: ConfigNamePathBuf(
1400 [
1401 Key {
1402 key: "user",
1403 repr: None,
1404 leaf_decor: Decor { .. },
1405 dotted_decor: Decor { .. },
1406 },
1407 Key {
1408 key: "email",
1409 repr: None,
1410 leaf_decor: Decor { .. },
1411 dotted_decor: Decor { .. },
1412 },
1413 ],
1414 ),
1415 value: String(
1416 Formatted {
1417 value: "base@user.email",
1418 repr: "default",
1419 decor: Decor { .. },
1420 },
1421 ),
1422 source: EnvBase,
1423 path: None,
1424 is_overridden: true,
1425 },
1426 AnnotatedValue {
1427 name: ConfigNamePathBuf(
1428 [
1429 Key {
1430 key: "user",
1431 repr: None,
1432 leaf_decor: Decor { .. },
1433 dotted_decor: Decor { .. },
1434 },
1435 Key {
1436 key: "email",
1437 repr: None,
1438 leaf_decor: Decor { .. },
1439 dotted_decor: Decor { .. },
1440 },
1441 ],
1442 ),
1443 value: String(
1444 Formatted {
1445 value: "repo@user.email",
1446 repr: "default",
1447 decor: Decor { .. },
1448 },
1449 ),
1450 source: Repo,
1451 path: None,
1452 is_overridden: false,
1453 },
1454 ]
1455 "#
1456 );
1457 Ok(())
1458 }
1459
1460 #[test]
1461 fn test_resolved_config_values_filter_path() -> TestResult {
1462 let settings = insta_settings();
1463 let _guard = settings.bind_to_scope();
1464 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1465 user_layer.set_value("test-table1.foo", "user-FOO")?;
1466 user_layer.set_value("test-table2.bar", "user-BAR")?;
1467 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1468 repo_layer.set_value("test-table1.bar", "repo-BAR")?;
1469 let mut config = StackedConfig::empty();
1470 config.add_layer(user_layer);
1471 config.add_layer(repo_layer);
1472 insta::assert_debug_snapshot!(
1473 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1474 @r#"
1475 [
1476 AnnotatedValue {
1477 name: ConfigNamePathBuf(
1478 [
1479 Key {
1480 key: "test-table1",
1481 repr: None,
1482 leaf_decor: Decor { .. },
1483 dotted_decor: Decor { .. },
1484 },
1485 Key {
1486 key: "foo",
1487 repr: None,
1488 leaf_decor: Decor { .. },
1489 dotted_decor: Decor { .. },
1490 },
1491 ],
1492 ),
1493 value: String(
1494 Formatted {
1495 value: "user-FOO",
1496 repr: "default",
1497 decor: Decor { .. },
1498 },
1499 ),
1500 source: User,
1501 path: None,
1502 is_overridden: false,
1503 },
1504 AnnotatedValue {
1505 name: ConfigNamePathBuf(
1506 [
1507 Key {
1508 key: "test-table1",
1509 repr: None,
1510 leaf_decor: Decor { .. },
1511 dotted_decor: Decor { .. },
1512 },
1513 Key {
1514 key: "bar",
1515 repr: None,
1516 leaf_decor: Decor { .. },
1517 dotted_decor: Decor { .. },
1518 },
1519 ],
1520 ),
1521 value: String(
1522 Formatted {
1523 value: "repo-BAR",
1524 repr: "default",
1525 decor: Decor { .. },
1526 },
1527 ),
1528 source: Repo,
1529 path: None,
1530 is_overridden: false,
1531 },
1532 ]
1533 "#
1534 );
1535 Ok(())
1536 }
1537
1538 #[test]
1539 fn test_resolved_config_values_overridden() -> TestResult {
1540 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1541 let mut config = StackedConfig::empty();
1542 config.extend_layers(layers.iter().copied().cloned());
1543 let prefix = if prefix.is_empty() {
1544 ConfigNamePathBuf::root()
1545 } else {
1546 prefix.parse().unwrap()
1547 };
1548 let mut output = String::new();
1549 for annotated in resolved_config_values(&config, &prefix) {
1550 let AnnotatedValue { name, value, .. } = &annotated;
1551 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1552 writeln!(output, "{sigil}{name} = {value}").unwrap();
1553 }
1554 output
1555 };
1556
1557 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1558 layer0.set_value("a.b.e", "0.0")?;
1559 layer0.set_value("a.b.c.f", "0.1")?;
1560 layer0.set_value("a.b.d", "0.2")?;
1561 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1562 layer1.set_value("a.b", "1.0")?;
1563 layer1.set_value("a.c", "1.1")?;
1564 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1565 layer2.set_value("a.b.g", "2.0")?;
1566 layer2.set_value("a.b.d", "2.1")?;
1567
1568 let layers = [&layer0, &layer1];
1570 insta::assert_snapshot!(list(&layers, ""), @r#"
1571 !a.b.e = "0.0"
1572 !a.b.c.f = "0.1"
1573 !a.b.d = "0.2"
1574 a.b = "1.0"
1575 a.c = "1.1"
1576 "#);
1577 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1578 !a.b.e = "0.0"
1579 !a.b.c.f = "0.1"
1580 !a.b.d = "0.2"
1581 a.b = "1.0"
1582 "#);
1583 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1584 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1585
1586 let layers = [&layer1, &layer2];
1588 insta::assert_snapshot!(list(&layers, ""), @r#"
1589 !a.b = "1.0"
1590 a.c = "1.1"
1591 a.b.g = "2.0"
1592 a.b.d = "2.1"
1593 "#);
1594 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1595 !a.b = "1.0"
1596 a.b.g = "2.0"
1597 a.b.d = "2.1"
1598 "#);
1599
1600 let layers = [&layer0, &layer2];
1602 insta::assert_snapshot!(list(&layers, ""), @r#"
1603 a.b.e = "0.0"
1604 a.b.c.f = "0.1"
1605 !a.b.d = "0.2"
1606 a.b.g = "2.0"
1607 a.b.d = "2.1"
1608 "#);
1609 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1610 a.b.e = "0.0"
1611 a.b.c.f = "0.1"
1612 !a.b.d = "0.2"
1613 a.b.g = "2.0"
1614 a.b.d = "2.1"
1615 "#);
1616 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1617 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1618 !a.b.d = "0.2"
1619 a.b.d = "2.1"
1620 "#);
1621
1622 let layers = [&layer0, &layer1, &layer2];
1624 insta::assert_snapshot!(list(&layers, ""), @r#"
1625 !a.b.e = "0.0"
1626 !a.b.c.f = "0.1"
1627 !a.b.d = "0.2"
1628 !a.b = "1.0"
1629 a.c = "1.1"
1630 a.b.g = "2.0"
1631 a.b.d = "2.1"
1632 "#);
1633 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1634 !a.b.e = "0.0"
1635 !a.b.c.f = "0.1"
1636 !a.b.d = "0.2"
1637 !a.b = "1.0"
1638 a.b.g = "2.0"
1639 a.b.d = "2.1"
1640 "#);
1641 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1642 Ok(())
1643 }
1644
1645 struct TestCase {
1646 files: &'static [&'static str],
1647 env: UnresolvedConfigEnv,
1648 wants: Vec<Want>,
1649 }
1650
1651 #[derive(Debug)]
1652 enum WantState {
1653 New,
1654 Existing,
1655 }
1656 #[derive(Debug)]
1657 struct Want {
1658 path: &'static str,
1659 state: WantState,
1660 }
1661
1662 impl Want {
1663 const fn new(path: &'static str) -> Self {
1664 Self {
1665 path,
1666 state: WantState::New,
1667 }
1668 }
1669
1670 const fn existing(path: &'static str) -> Self {
1671 Self {
1672 path,
1673 state: WantState::Existing,
1674 }
1675 }
1676
1677 fn rooted_path(&self, root: &Path) -> PathBuf {
1678 root.join(self.path)
1679 }
1680
1681 fn exists(&self) -> bool {
1682 matches!(self.state, WantState::Existing)
1683 }
1684 }
1685
1686 fn config_path_home_existing() -> TestCase {
1687 TestCase {
1688 files: &["home/.jjconfig.toml"],
1689 env: UnresolvedConfigEnv {
1690 home_dir: Some("home".into()),
1691 ..Default::default()
1692 },
1693 wants: vec![Want::existing("home/.jjconfig.toml")],
1694 }
1695 }
1696
1697 fn config_path_home_new() -> TestCase {
1698 TestCase {
1699 files: &[],
1700 env: UnresolvedConfigEnv {
1701 home_dir: Some("home".into()),
1702 ..Default::default()
1703 },
1704 wants: vec![Want::new("home/.jjconfig.toml")],
1705 }
1706 }
1707
1708 fn config_path_home_existing_platform_new() -> TestCase {
1709 TestCase {
1710 files: &["home/.jjconfig.toml"],
1711 env: UnresolvedConfigEnv {
1712 home_dir: Some("home".into()),
1713 user_config_dir: Some("config".into()),
1714 ..Default::default()
1715 },
1716 wants: vec![
1717 Want::existing("home/.jjconfig.toml"),
1718 Want::new("config/jj/config.toml"),
1719 ],
1720 }
1721 }
1722
1723 fn config_path_platform_existing() -> TestCase {
1724 TestCase {
1725 files: &["config/jj/config.toml"],
1726 env: UnresolvedConfigEnv {
1727 home_dir: Some("home".into()),
1728 user_config_dir: Some("config".into()),
1729 ..Default::default()
1730 },
1731 wants: vec![Want::existing("config/jj/config.toml")],
1732 }
1733 }
1734
1735 fn config_path_platform_new() -> TestCase {
1736 TestCase {
1737 files: &[],
1738 env: UnresolvedConfigEnv {
1739 user_config_dir: Some("config".into()),
1740 ..Default::default()
1741 },
1742 wants: vec![Want::new("config/jj/config.toml")],
1743 }
1744 }
1745
1746 fn config_path_new_prefer_platform() -> TestCase {
1747 TestCase {
1748 files: &[],
1749 env: UnresolvedConfigEnv {
1750 home_dir: Some("home".into()),
1751 user_config_dir: Some("config".into()),
1752 ..Default::default()
1753 },
1754 wants: vec![Want::new("config/jj/config.toml")],
1755 }
1756 }
1757
1758 fn config_path_jj_config_existing() -> TestCase {
1759 TestCase {
1760 files: &["custom.toml"],
1761 env: UnresolvedConfigEnv {
1762 jj_config: Some("custom.toml".into()),
1763 ..Default::default()
1764 },
1765 wants: vec![Want::existing("custom.toml")],
1766 }
1767 }
1768
1769 fn config_path_jj_config_new() -> TestCase {
1770 TestCase {
1771 files: &[],
1772 env: UnresolvedConfigEnv {
1773 jj_config: Some("custom.toml".into()),
1774 ..Default::default()
1775 },
1776 wants: vec![Want::new("custom.toml")],
1777 }
1778 }
1779
1780 fn config_path_jj_config_existing_multiple() -> TestCase {
1781 TestCase {
1782 files: &["custom1.toml", "custom2.toml"],
1783 env: UnresolvedConfigEnv {
1784 jj_config: Some(
1785 join_paths(["custom1.toml", "custom2.toml"])
1786 .unwrap()
1787 .into_string()
1788 .unwrap(),
1789 ),
1790 ..Default::default()
1791 },
1792 wants: vec![
1793 Want::existing("custom1.toml"),
1794 Want::existing("custom2.toml"),
1795 ],
1796 }
1797 }
1798
1799 fn config_path_jj_config_new_multiple() -> TestCase {
1800 TestCase {
1801 files: &["custom1.toml"],
1802 env: UnresolvedConfigEnv {
1803 jj_config: Some(
1804 join_paths(["custom1.toml", "custom2.toml"])
1805 .unwrap()
1806 .into_string()
1807 .unwrap(),
1808 ),
1809 ..Default::default()
1810 },
1811 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1812 }
1813 }
1814
1815 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1816 TestCase {
1817 files: &["custom1.toml"],
1818 env: UnresolvedConfigEnv {
1819 jj_config: Some(
1820 join_paths(["custom1.toml", "", "custom2.toml"])
1821 .unwrap()
1822 .into_string()
1823 .unwrap(),
1824 ),
1825 ..Default::default()
1826 },
1827 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1828 }
1829 }
1830
1831 fn config_path_jj_config_empty() -> TestCase {
1832 TestCase {
1833 files: &[],
1834 env: UnresolvedConfigEnv {
1835 jj_config: Some("".to_owned()),
1836 ..Default::default()
1837 },
1838 wants: vec![],
1839 }
1840 }
1841
1842 fn config_path_config_pick_platform() -> TestCase {
1843 TestCase {
1844 files: &["config/jj/config.toml"],
1845 env: UnresolvedConfigEnv {
1846 home_dir: Some("home".into()),
1847 user_config_dir: Some("config".into()),
1848 ..Default::default()
1849 },
1850 wants: vec![Want::existing("config/jj/config.toml")],
1851 }
1852 }
1853
1854 fn config_path_config_pick_home() -> TestCase {
1855 TestCase {
1856 files: &["home/.jjconfig.toml"],
1857 env: UnresolvedConfigEnv {
1858 home_dir: Some("home".into()),
1859 user_config_dir: Some("config".into()),
1860 ..Default::default()
1861 },
1862 wants: vec![
1863 Want::existing("home/.jjconfig.toml"),
1864 Want::new("config/jj/config.toml"),
1865 ],
1866 }
1867 }
1868
1869 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1870 TestCase {
1871 files: &["config/jj/conf.d/_"],
1872 env: UnresolvedConfigEnv {
1873 home_dir: Some("home".into()),
1874 user_config_dir: Some("config".into()),
1875 ..Default::default()
1876 },
1877 wants: vec![
1878 Want::new("config/jj/config.toml"),
1879 Want::existing("config/jj/conf.d"),
1880 ],
1881 }
1882 }
1883
1884 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1885 TestCase {
1886 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1887 env: UnresolvedConfigEnv {
1888 home_dir: Some("home".into()),
1889 user_config_dir: Some("config".into()),
1890 ..Default::default()
1891 },
1892 wants: vec![
1893 Want::existing("config/jj/config.toml"),
1894 Want::existing("config/jj/conf.d"),
1895 ],
1896 }
1897 }
1898
1899 fn config_path_all_existing() -> TestCase {
1900 TestCase {
1901 files: &[
1902 "config/jj/conf.d/_",
1903 "config/jj/config.toml",
1904 "home/.jjconfig.toml",
1905 ],
1906 env: UnresolvedConfigEnv {
1907 home_dir: Some("home".into()),
1908 user_config_dir: Some("config".into()),
1909 ..Default::default()
1910 },
1911 wants: vec![
1913 Want::existing("home/.jjconfig.toml"),
1914 Want::existing("config/jj/config.toml"),
1915 Want::existing("config/jj/conf.d"),
1916 ],
1917 }
1918 }
1919
1920 fn config_path_none() -> TestCase {
1921 TestCase {
1922 files: &[],
1923 env: Default::default(),
1924 wants: vec![],
1925 }
1926 }
1927
1928 #[test_case(config_path_home_existing())]
1929 #[test_case(config_path_home_new())]
1930 #[test_case(config_path_home_existing_platform_new())]
1931 #[test_case(config_path_platform_existing())]
1932 #[test_case(config_path_platform_new())]
1933 #[test_case(config_path_new_prefer_platform())]
1934 #[test_case(config_path_jj_config_existing())]
1935 #[test_case(config_path_jj_config_new())]
1936 #[test_case(config_path_jj_config_existing_multiple())]
1937 #[test_case(config_path_jj_config_new_multiple())]
1938 #[test_case(config_path_jj_config_empty_paths_filtered())]
1939 #[test_case(config_path_jj_config_empty())]
1940 #[test_case(config_path_config_pick_platform())]
1941 #[test_case(config_path_config_pick_home())]
1942 #[test_case(config_path_platform_new_conf_dir_existing())]
1943 #[test_case(config_path_platform_existing_conf_dir_existing())]
1944 #[test_case(config_path_all_existing())]
1945 #[test_case(config_path_none())]
1946 fn test_config_path(case: TestCase) {
1947 let tmp = setup_config_fs(case.files);
1948 let env = resolve_config_env(&case.env, tmp.path());
1949
1950 let all_expected_paths = case
1951 .wants
1952 .iter()
1953 .map(|w| w.rooted_path(tmp.path()))
1954 .collect_vec();
1955 let exists_expected_paths = case
1956 .wants
1957 .iter()
1958 .filter(|w| w.exists())
1959 .map(|w| w.rooted_path(tmp.path()))
1960 .collect_vec();
1961
1962 let all_paths = env.user_config_paths().collect_vec();
1963 let exists_paths = env.existing_user_config_paths().collect_vec();
1964
1965 assert_eq!(all_paths, all_expected_paths);
1966 assert_eq!(exists_paths, exists_expected_paths);
1967 }
1968
1969 fn system_config_path_none() -> TestCase {
1970 TestCase {
1971 files: &["etc/jj/config.toml", "system/jj/conf.d/_"],
1972 env: Default::default(),
1973 wants: vec![],
1974 }
1975 }
1976
1977 fn system_config_path_existing() -> TestCase {
1978 TestCase {
1979 files: &["system/jj/config.toml", "system/jj/conf.d/_"],
1980 env: UnresolvedConfigEnv {
1981 system_config_dir: Some("system".into()),
1982 ..Default::default()
1983 },
1984 wants: vec![
1985 Want::existing("system/jj/config.toml"),
1986 Want::existing("system/jj/conf.d"),
1987 ],
1988 }
1989 }
1990
1991 fn system_config_path_jj_config() -> TestCase {
1992 TestCase {
1993 files: &["system/jj/config.toml"],
1994 env: UnresolvedConfigEnv {
1995 jj_config: Some("custom.toml".into()),
1996 system_config_dir: Some("system".into()),
1997 ..Default::default()
1998 },
1999 wants: vec![],
2000 }
2001 }
2002
2003 #[test_case(system_config_path_none())]
2004 #[test_case(system_config_path_existing())]
2005 #[test_case(system_config_path_jj_config())]
2006 fn test_system_config_path(case: TestCase) {
2007 let tmp = setup_config_fs(case.files);
2008 let env = resolve_config_env(&case.env, tmp.path());
2009
2010 let all_expected_paths = case
2011 .wants
2012 .iter()
2013 .map(|w| w.rooted_path(tmp.path()))
2014 .collect_vec();
2015 let exists_expected_paths = case
2016 .wants
2017 .iter()
2018 .filter(|w| w.exists())
2019 .map(|w| w.rooted_path(tmp.path()))
2020 .collect_vec();
2021
2022 let all_paths = env
2023 .system_config_paths
2024 .iter()
2025 .map(ConfigPath::as_path)
2026 .collect_vec();
2027 let exists_paths = env.existing_system_config_paths().collect_vec();
2028
2029 assert_eq!(all_paths, all_expected_paths);
2030 assert_eq!(exists_paths, exists_expected_paths);
2031 }
2032
2033 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
2034 let tmp = testutils::new_temp_dir();
2035 for file in files {
2036 let path = tmp.path().join(file);
2037 if let Some(parent) = path.parent() {
2038 std::fs::create_dir_all(parent).unwrap();
2039 }
2040 std::fs::File::create(path).unwrap();
2041 }
2042 tmp
2043 }
2044
2045 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
2046 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
2047 let env = UnresolvedConfigEnv {
2048 user_config_dir: env.user_config_dir.as_ref().map(|p| root.join(p)),
2049 home_dir: home_dir.clone(),
2050 jj_config: env.jj_config.as_ref().map(|p| {
2051 join_paths(split_paths(p).map(|p| {
2052 if p.as_os_str().is_empty() {
2053 return p;
2054 }
2055 root.join(p)
2056 }))
2057 .unwrap()
2058 .into_string()
2059 .unwrap()
2060 }),
2061 system_config_dir: env.system_config_dir.as_ref().map(|p| root.join(p)),
2062 };
2063 ConfigEnv {
2064 home_dir,
2065 root_config_dir: None,
2066 repo_path: None,
2067 workspace_path: None,
2068 system_config_paths: env.resolve_system(),
2069 user_config_paths: env.resolve_user(),
2070 repo_config: None,
2071 workspace_config: None,
2072 command: None,
2073 hostname: None,
2074 environment: HashMap::new(),
2075 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
2076 }
2077 }
2078}